diff --git a/core/models/database.py b/core/models/database.py index 15327ea..936c26b 100644 --- a/core/models/database.py +++ b/core/models/database.py @@ -22,6 +22,7 @@ def init_db(app): from . import qc_report from . import usage_log from . import campaign_presentation + from core.utils.progress_tracker import ProgressSession # noqa: F401 # Create all tables (if they don't exist) # Tables are created automatically on first access diff --git a/core/utils/progress_tracker.py b/core/utils/progress_tracker.py index 1532734..bc37efb 100644 --- a/core/utils/progress_tracker.py +++ b/core/utils/progress_tracker.py @@ -8,7 +8,6 @@ import json import time from datetime import datetime from typing import Optional, Dict, Any -from flask import session from core.models.database import db diff --git a/modules/hm_qc/batch_executor.py b/modules/hm_qc/batch_executor.py index 6b8806f..c59e0b2 100644 --- a/modules/hm_qc/batch_executor.py +++ b/modules/hm_qc/batch_executor.py @@ -37,7 +37,8 @@ class BatchQCExecutor: job_number: str = None, batch_size: int = DEFAULT_BATCH_SIZE, campaign_id: str = None, - batch_id: str = None + batch_id: str = None, + app=None ): """ Initialize batch executor. @@ -50,6 +51,7 @@ class BatchQCExecutor: batch_size: Number of files per batch (default 10) campaign_id: Optional campaign ID to load presentation guidelines batch_id: Optional batch ID for grouping reports from the same upload + app: Flask app instance (required for ThreadPoolExecutor child threads) """ self.session_id = session_id self.file_paths = file_paths[:MAX_FILES] @@ -58,6 +60,7 @@ class BatchQCExecutor: self.campaign_id = campaign_id self.batch_id = batch_id self.batch_size = batch_size + self.app = app self.progress = UnifiedProgressTracker(session_id) self.results = [] @@ -165,6 +168,9 @@ class BatchQCExecutor: """ Process a single file using QCExecutor. + Runs inside a ThreadPoolExecutor child thread, so we must push + an app context explicitly (child threads don't inherit it). + Errors are captured and returned, not raised. Args: @@ -177,31 +183,32 @@ class BatchQCExecutor: """ filename = os.path.basename(file_path) try: - # Create a per-file executor with a sub-session ID - # We don't use its progress tracker—we track at the batch level - file_session_id = f"{self.session_id}_file_{completed}" + with self.app.app_context(): + # Create a per-file executor with a sub-session ID + # We don't use its progress tracker—we track at the batch level + file_session_id = f"{self.session_id}_file_{completed}" - executor = QCExecutor( - session_id=file_session_id, - file_path=file_path, - profile=self.profile, - job_number=self.job_number, - campaign_id=self.campaign_id, - batch_id=self.batch_id - ) + executor = QCExecutor( + session_id=file_session_id, + file_path=file_path, + profile=self.profile, + job_number=self.job_number, + campaign_id=self.campaign_id, + batch_id=self.batch_id + ) - result = executor.execute() + result = executor.execute() - return { - 'filename': filename, - 'file_path': file_path, - 'success': result.get('success', False), - 'score': result.get('overall_score'), - 'status': result.get('overall_status', 'error'), - 'report_path': result.get('report_path'), - 'report_id': result.get('report_id'), - 'error': result.get('error') - } + return { + 'filename': filename, + 'file_path': file_path, + 'success': result.get('success', False), + 'score': result.get('overall_score'), + 'status': result.get('overall_status', 'error'), + 'report_path': result.get('report_path'), + 'report_id': result.get('report_id'), + 'error': result.get('error') + } except Exception as e: logger.error(f"Error processing file {filename}: {e}") diff --git a/modules/hm_qc/routes.py b/modules/hm_qc/routes.py index fbf0cde..6cb5708 100644 --- a/modules/hm_qc/routes.py +++ b/modules/hm_qc/routes.py @@ -317,17 +317,18 @@ def execute_batch(): campaign_id = data.get('campaign_id') batch_id = str(uuid.uuid4()) + app = current_app._get_current_object() + batch_executor = BatchQCExecutor( session_id=session_id, file_paths=file_paths, profile=profile, job_number=job_number, campaign_id=campaign_id, - batch_id=batch_id + batch_id=batch_id, + app=app ) - app = current_app._get_current_object() - def run_batch(): with app.app_context(): result = batch_executor.execute()