Add MongoDB query utilities and update batch size for document insertion
This commit is contained in:
@@ -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,
|
||||||
|
)
|
||||||
@@ -335,7 +335,7 @@ def create_postgres_table_from_mongo(
|
|||||||
class MongoDocumentInserter:
|
class MongoDocumentInserter:
|
||||||
"""Insert MongoDB documents into PostgreSQL tables with type conversion."""
|
"""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.
|
Initialize the document inserter.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -44,12 +44,12 @@ class LPrint:
|
|||||||
"""Check the log file size and rotate if necessary."""
|
"""Check the log file size and rotate if necessary."""
|
||||||
log_size = os.path.getsize(self.logfile)
|
log_size = os.path.getsize(self.logfile)
|
||||||
print(f'Logfile Size: {log_size} ({self.humanbytes(log_size)})')
|
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()
|
self.rotate_logs()
|
||||||
|
|
||||||
def rotate_logs(self):
|
def rotate_logs(self):
|
||||||
"""Rotate the log file."""
|
"""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)
|
rotater = LogRotate(prefix=self.logfile, verbose=True)
|
||||||
try:
|
try:
|
||||||
rotater.rotate()
|
rotater.rotate()
|
||||||
|
|||||||
@@ -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
@@ -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
|
# Map table names to their MongoDB collections and primary keys
|
||||||
collection_map = {
|
collection_map = {
|
||||||
@@ -118,3 +118,23 @@ for tableName in tableNames:
|
|||||||
# if __name__ == "__main__":
|
# if __name__ == "__main__":
|
||||||
# sys.exit(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)
|
||||||
Reference in New Issue
Block a user