""" Shared file query utilities for filtering files by processing status. This module contains reusable query logic for filtering FileRecord objects based on their processing status using the FileProcessingStep table. """ from typing import Optional from sqlalchemy import or_ from sqlalchemy.orm import Query, Session from app.models import FileProcessingStep, FileRecord from app.utils.step_manager import TERMINAL_STEP def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Query: """ Apply status filter to a FileRecord query using FileProcessingStep table. This function modifies a SQLAlchemy query to filter files based on their processing status by examining associated FileProcessingStep entries. Only tracks "real" processing steps that represent user-facing status: - Main steps: create_file_record, check_text, extract_text, process_with_ocr, process_with_azure_document_intelligence (legacy), extract_metadata_with_gpt, embed_metadata_into_pdf, finalize_document_storage, send_to_all_destinations - Upload steps: queue_*, upload_to_* Diagnostic/internal steps (poll_task, upload_file, set_custom_fields, etc.) are ignored as they may not complete properly and don't affect the actual status. Args: query: The base SQLAlchemy query for FileRecord objects db: Database session for creating subqueries status: Status filter to apply. Valid values: - "pending": Files with no real FileProcessingStep entries - "processing": Files with in_progress real steps - "failed": Files with failure real steps - "completed": Files with all real steps success/skipped - None: No filter applied (returns query unchanged) Returns: Modified query with status filter applied Examples: >>> query = db.query(FileRecord) >>> query = apply_status_filter(query, db, "completed") >>> files = query.all() """ if not status: return query # Define which steps are "real" status-determining steps # Only high-level logical steps and actual upload destinations (not queue_* steps) from app.config import settings REAL_STEPS = { "create_file_record", "check_text", "extract_text", "process_with_ocr", "process_with_azure_document_intelligence", "extract_metadata_with_gpt", "embed_metadata_into_pdf", "finalize_document_storage", "send_to_all_destinations", "upload_to_dropbox", "upload_to_paperless", "upload_to_google_drive", "upload_to_ftp", "upload_to_onedrive", "upload_to_webdav", "upload_to_sftp", "upload_to_nextcloud", "upload_to_paperless_ngx", "upload_to_email", "upload_to_s3", } # Add check_for_duplicates if deduplication is enabled if settings.enable_deduplication: REAL_STEPS.add("check_for_duplicates") # Filter to only real steps real_steps_subq = db.query(FileProcessingStep).filter(FileProcessingStep.step_name.in_(REAL_STEPS)) if status == "pending": # Files with no real steps (never started processing) subq = real_steps_subq.distinct().subquery() query = query.filter(~FileRecord.id.in_(db.query(subq.c.file_id))) elif status == "processing": # Files with in_progress real steps subq = real_steps_subq.filter(FileProcessingStep.status == "in_progress").distinct().subquery() query = query.filter(FileRecord.id.in_(db.query(subq.c.file_id))) elif status == "failed": # Files with failure real steps subq = real_steps_subq.filter(FileProcessingStep.status == "failure").distinct().subquery() query = query.filter(FileRecord.id.in_(db.query(subq.c.file_id))) elif status == "completed": # Files where all real steps are either success or skipped (no failures or in_progress) # and the terminal send_to_all_destinations step has been recorded. # Excluding the terminal-step requirement allows files that only completed the # first few pipeline stages to be falsely labelled as "completed". # Exclude duplicates from completed query = query.filter(FileRecord.is_duplicate.is_(False)) # Get files that have real steps files_with_real_steps = real_steps_subq.distinct().subquery() # Get files with failures or in_progress on real steps files_with_issues = ( real_steps_subq.filter( or_(FileProcessingStep.status == "failure", FileProcessingStep.status == "in_progress") ) .distinct() .subquery() ) # Get files that have the terminal processing step recorded files_with_terminal_step = ( db.query(FileProcessingStep.file_id) .filter(FileProcessingStep.step_name == TERMINAL_STEP) .distinct() .subquery() ) # Select files with real steps, no issues, and terminal step present query = ( query.filter(FileRecord.id.in_(db.query(files_with_real_steps.c.file_id))) .filter(~FileRecord.id.in_(db.query(files_with_issues.c.file_id))) .filter(FileRecord.id.in_(db.query(files_with_terminal_step.c.file_id))) ) elif status == "duplicate": # Files marked as duplicates query = query.filter(FileRecord.is_duplicate.is_(True)) return query