Add MongoDB query utilities and update batch size for document insertion

This commit is contained in:
2026-02-06 14:49:56 -05:00
parent 1283bd282a
commit 8ed2a77365
5 changed files with 406 additions and 4 deletions
+270
View File
@@ -0,0 +1,270 @@
"""MongoDB query utilities for filtering documents by update timestamp."""
__author__ = 'Wendell Jones'
from typing import Any, Dict, List, Optional
from datetime import datetime, timedelta, timezone
import logging
logger = logging.getLogger(__name__)
class MongoQueryUtils:
"""Utilities for querying MongoDB collections."""
@staticmethod
def get_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""
Find documents updated in the last N minutes.
Handles both epoch timestamps (int/float) and ISO datetime strings.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter to apply (optional).
Returns:
List of documents updated within the time window.
Example:
from db.query_utils import MongoQueryUtils
recent = MongoQueryUtils.get_recent_updates(
collection=mongo_db['series'],
minutes=60,
updated_field='updated',
)
print(f"Found {len(recent)} recently updated documents")
"""
if query_filter is None:
query_filter = {}
# Calculate cutoff time (60 minutes ago)
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
cutoff_epoch = int(cutoff_time.timestamp())
logger.info(
f"Searching for documents updated in last {minutes} minutes "
f"(since {cutoff_time.isoformat()})"
)
# Build query for documents updated after cutoff
# Support both epoch timestamps and ISO datetime strings
query = {
**query_filter,
'$or': [
# Epoch timestamp (int/float)
{updated_field: {'$gte': cutoff_epoch}},
# ISO datetime string
{updated_field: {'$gte': cutoff_time.isoformat()}},
]
}
documents = list(collection.find(query))
logger.info(f"Found {len(documents)} documents matching criteria")
return documents
@staticmethod
def get_recent_updates_by_id(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
id_field: str = 'id',
query_filter: Optional[Dict] = None,
) -> List[int]:
"""
Get IDs of documents updated in the last N minutes.
Useful for batch processing or sync operations.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
id_field: Name of the ID field (default: 'id').
query_filter: Additional MongoDB query filter (optional).
Returns:
List of document IDs that were recently updated.
Example:
ids = MongoQueryUtils.get_recent_updates_by_id(
collection=mongo_db['series'],
minutes=60,
)
print(f"Series IDs to update: {ids}")
"""
docs = MongoQueryUtils.get_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
ids = [doc.get(id_field) for doc in docs if id_field in doc]
return ids
@staticmethod
def get_updates_since(
collection: Any,
since_timestamp: Optional[Any] = None,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""
Find documents updated since a specific timestamp or N minutes ago.
Args:
collection: PyMongo collection object.
since_timestamp: Epoch int, float, or ISO datetime string (optional).
If None, defaults to N minutes ago (see minutes parameter).
minutes: If since_timestamp is None, look back this many minutes (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter (optional).
Returns:
List of documents updated since the timestamp.
Example:
# Get updates since a specific timestamp
cutoff = 1704067200 # Epoch timestamp
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
since_timestamp=cutoff,
)
# Or use default 60 minutes ago
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
)
# Or specify different time window
docs = MongoQueryUtils.get_updates_since(
collection=mongo_db['series'],
minutes=120, # Last 2 hours
)
"""
if query_filter is None:
query_filter = {}
# If no timestamp provided, use minutes parameter to calculate cutoff
if since_timestamp is None:
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
since_timestamp = int(cutoff_time.timestamp())
logger.info(f"Using default {minutes} minutes ago: {cutoff_time.isoformat()}")
query = {
**query_filter,
'$or': [
{updated_field: {'$gte': since_timestamp}},
{updated_field: {'$gte': str(since_timestamp)}},
]
}
documents = list(collection.find(query))
logger.info(f"Found {len(documents)} documents updated since {since_timestamp}")
return documents
@staticmethod
def count_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> int:
"""
Count documents updated in the last N minutes.
Args:
collection: PyMongo collection object.
minutes: Number of minutes to look back (default: 60).
updated_field: Name of the updated timestamp field (default: 'updated').
query_filter: Additional MongoDB query filter (optional).
Returns:
Count of recently updated documents.
Example:
count = MongoQueryUtils.count_recent_updates(
collection=mongo_db['series'],
minutes=60,
)
print(f"{count} series were updated in the last hour")
"""
if query_filter is None:
query_filter = {}
now = datetime.now(timezone.utc)
cutoff_time = now - timedelta(minutes=minutes)
cutoff_epoch = int(cutoff_time.timestamp())
query = {
**query_filter,
'$or': [
{updated_field: {'$gte': cutoff_epoch}},
{updated_field: {'$gte': cutoff_time.isoformat()}},
]
}
count = collection.count_documents(query)
return count
# Convenience functions
def get_recent_updates(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
"""Find documents updated in the last N minutes."""
return MongoQueryUtils.get_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
def get_recent_ids(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
id_field: str = 'id',
query_filter: Optional[Dict] = None,
) -> List[int]:
"""Get IDs of documents updated in the last N minutes."""
return MongoQueryUtils.get_recent_updates_by_id(
collection=collection,
minutes=minutes,
updated_field=updated_field,
id_field=id_field,
query_filter=query_filter,
)
def count_recent(
collection: Any,
minutes: int = 60,
updated_field: str = 'updated',
query_filter: Optional[Dict] = None,
) -> int:
"""Count documents updated in the last N minutes."""
return MongoQueryUtils.count_recent_updates(
collection=collection,
minutes=minutes,
updated_field=updated_field,
query_filter=query_filter,
)
+1 -1
View File
@@ -335,7 +335,7 @@ def create_postgres_table_from_mongo(
class MongoDocumentInserter:
"""Insert MongoDB documents into PostgreSQL tables with type conversion."""
def __init__(self, batch_size: int = 1000):
def __init__(self, batch_size: int = 10000):
"""
Initialize the document inserter.
+2 -2
View File
@@ -44,12 +44,12 @@ class LPrint:
"""Check the log file size and rotate if necessary."""
log_size = os.path.getsize(self.logfile)
print(f'Logfile Size: {log_size} ({self.humanbytes(log_size)})')
if force_rotate or log_size > 10_000_000: # 10 MB
if force_rotate or log_size > 50_000_000: # 10 MB
self.rotate_logs()
def rotate_logs(self):
"""Rotate the log file."""
from logrotater import LogRotate # Assuming logrotater is a custom module
from logs.logrotater import LogRotate # Assuming logrotater is a custom module
rotater = LogRotate(prefix=self.logfile, verbose=True)
try:
rotater.rotate()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python
''' A python logrotate library'''
import os
import glob
__author__ = 'Kyle Laplante'
__copyright__ = 'Copyright 2013, Kyle Laplante'
__date__ = '07-07-2013'
__license__ = 'GPL'
__version__ = '1.3'
__email__ = 'kyle.laplante@gmail.com'
class LogRotateException(Exception):
pass
class LogRotate:
"""This library is a simple logrotater (or file rotater).
Simply pass in the path to the main logfile and this
library will rotate all the logs by an increment of 1.
Example:
import logrotater
rotater = logrotater.LogRotate(prefix='/home/kyle/p4.log', verbose=True)
rotater.rotate()
The prefix path should be the path of the main logfile without the .N extension.
The previous example would rotate /home/kyle/p4.log.N to /home/kyle/p4.log.N+1,
move /home/kyle/p4.log to /home/kyle/p4.log.1 and create a new empty /home/kyle/p4.log."""
def __init__(self, *args, **kwargs):
'''Required kwargs: prefix="some path"
Optional kwargs: verbose=True
If verbose is True then LogRotate will print what it is doing with each file.'''
self.verbose = kwargs.get('verbose', False)
for k, v in kwargs.items():
setattr(self, k, v)
if not os.path.isfile(self.prefix):
raise LogRotateException("Must use a valid prefix filename")
def rotate(self, *args, **kwargs):
self.files = glob.glob(self.prefix + "*")
if not len(self.files) > 0:
raise LogRotateException(
"No files with the prefix %s found" % self.prefix)
self.nums = []
for file in self.files:
try:
self.nums.append(int(file.split(self.prefix + '.')[-1]))
except ValueError:
pass
self.nums.sort()
self.nums.reverse()
for num in self.nums:
new_num = num + 1
if self.verbose:
print("moving %s.%d to %s.%d" % (self.prefix, num, self.prefix, new_num))
os.rename("%s.%d" % (self.prefix, num), "%s.%d" %
(self.prefix, new_num))
if self.verbose:
print("moving %s to %s.1" % (self.prefix, self.prefix))
os.rename(self.prefix, self.prefix + '.1')
self.touch(self.prefix)
def touch(self, path):
'''a simple method to touch a file'''
if self.verbose:
print("Creating file: %s" % path)
f = open(path, 'a')
os.utime(path, None)
if __name__ == '__main__':
from optparse import OptionParser
import sys
usage = '''%prog [options] <file prefix>
File prefix is the path to the main logfile.'''
parser = OptionParser(usage=usage)
parser.add_option(
'-p', '--prefix', dest='prefix', default=None, help='Filename prefix.')
parser.add_option('-v', '--verbose', dest='verbose',
action='store_true', default=False, help='Enable verbose output')
(opts, args) = parser.parse_args()
if opts.prefix is None:
print("Please enter a filename prefix")
sys.exit(1)
try:
rotater = LogRotate(prefix=opts.prefix, verbose=opts.verbose)
except LogRotateException as e:
print(e)
sys.exit(1)
try:
rotater.rotate()
except LogRotateException as e:
print(e)
sys.exit(1)
+21 -1
View File
@@ -73,7 +73,7 @@ for tableName in tableNames:
inserter = MongoDocumentInserter(batch_size=1000)
inserter = MongoDocumentInserter(batch_size=10000)
# Map table names to their MongoDB collections and primary keys
collection_map = {
@@ -118,3 +118,23 @@ for tableName in tableNames:
# if __name__ == "__main__":
# sys.exit(main())
# Below are the relevant methods from db/functions.py and db/query_utils.py that are used in the above code. They are included here for completeness and context.
# Needs to be incorporated into logic so that only releveant documents are updated.
# from db.query_utils import MongoQueryUtils
# # With additional filters
# recent = MongoQueryUtils.get_recent_updates(
# collection=mongo_updater.mgseries,
# minutes=60,
# updated_field='updated',
# query_filter={'status': 'Ended'}, # Only ended shows
# )
# # Since a specific timestamp
# docs = MongoQueryUtils.get_updates_since(
# collection=mongo_updater.mgseries,
# since_timestamp=None, # Epoch or ISO string
# )
# for doc in docs:
# print(doc)