# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS translate,orchestrator,lifecycle,run] # @BRIEF Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events. # @LAYER Domain # @RELATION DEPENDS_ON -> [TranslationRun] # @RELATION DEPENDS_ON -> [TranslationJob] # @RELATION DEPENDS_ON -> [TranslationExecutor] # @RELATION DEPENDS_ON -> [SQLGenerator] # @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor] # @RELATION DEPENDS_ON -> [TranslationEventLog] # @RELATION DEPENDS_ON -> [TranslationPreviewSession] # @PRE Valid job and accepted preview (for manual runs). Superset and LLM are reachable. # @POST Translation run is executed, SQL generated and submitted, events recorded. # @SIDE_EFFECT Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events. # @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun] # @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run. # @RATIONALE C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary. # @REJECTED Distributed actor model (Celery) — eventual-consistency challenges at current scale. # @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant. import json import time import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Callable, Tuple from sqlalchemy.orm import Session from ...core.logger import logger, belief_scope from ...core.config_manager import ConfigManager from ...models.translate import ( TranslationJob, TranslationRun, TranslationBatch, TranslationRecord, TranslationPreviewSession, ) from ...schemas.translate import TranslationRunResponse from .executor import TranslationExecutor from .sql_generator import SQLGenerator from .superset_executor import SupersetSqlLabExecutor from .events import TranslationEventLog from ..translate.service import TranslateJobService # #region TranslationOrchestrator [C:5] [TYPE Class] [SEMANTICS translate,orchestrator] # @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging. # @PRE DB session and config manager are available. # @POST Runs are created, executed, and finalized with event records. # @SIDE_EFFECT Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows. # @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. # @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun] class TranslationOrchestrator: def __init__( self, db: Session, config_manager: ConfigManager, current_user: Optional[str] = None, ): self.db = db self.config_manager = config_manager self.current_user = current_user self.event_log = TranslationEventLog(db) self._job: Optional[TranslationJob] = None # #region start_run [C:5] [TYPE Function] [SEMANTICS translate,run,create] # @BRIEF Start a new translation run for a job with config snapshot and hash computation. # @PRE job_id exists. For manual runs, there must be an accepted preview session. # @POST TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded. # @SIDE_EFFECT DB writes; event logging. def start_run( self, job_id: str, is_scheduled: bool = False, trigger_type: Optional[str] = None, ) -> TranslationRun: with belief_scope("TranslationOrchestrator.start_run"): # Load and validate job job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first() if not job: raise ValueError(f"Translation job '{job_id}' not found") self._job = job logger.reason("Starting translation run", { "job_id": job_id, "is_scheduled": is_scheduled, }) # Validate preconditions self._validate_preconditions(job, is_scheduled=is_scheduled) # Compute hashes config_hash = self._compute_config_hash(job) dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id) # Build config snapshot config_snapshot = { "source_dialect": job.source_dialect, "target_dialect": job.target_dialect, "database_dialect": job.database_dialect, "source_datasource_id": job.source_datasource_id, "source_table": job.source_table, "target_schema": job.target_schema, "target_table": job.target_table, "source_key_cols": job.source_key_cols, "target_key_cols": job.target_key_cols, "translation_column": job.translation_column, "context_columns": job.context_columns, "target_language": job.target_language, "provider_id": job.provider_id, "batch_size": job.batch_size, "upsert_strategy": job.upsert_strategy, "dictionary_ids": self._compute_dict_snapshot_hash(job_id), } # Compute key_hash from source_key_cols import hashlib key_hash_input = json.dumps({ "source_key_cols": job.source_key_cols, "source_datasource_id": job.source_datasource_id, "source_table": job.source_table, }, sort_keys=True) key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16] # Create run record with all hash/snapshot fields run = TranslationRun( id=str(uuid.uuid4()), job_id=job_id, status="PENDING", trigger_type=trigger_type or ("scheduled" if is_scheduled else "manual"), config_snapshot=config_snapshot, key_hash=key_hash, config_hash=config_hash, dict_snapshot_hash=dict_snapshot_hash, created_by=self.current_user, created_at=datetime.now(timezone.utc), ) self.db.add(run) self.db.flush() # Record event self.event_log.log_event( job_id=job_id, run_id=run.id, event_type="RUN_STARTED", payload={ "is_scheduled": is_scheduled, "trigger_type": run.trigger_type, "config_hash": config_hash, "dict_snapshot_hash": dict_snapshot_hash, "key_hash": key_hash, }, created_by=self.current_user, ) self.db.commit() self.db.refresh(run) logger.reflect("Run created", { "run_id": run.id, "job_id": job_id, "status": run.status, "trigger_type": run.trigger_type, }) return run # #endregion start_run # #region execute_run [C:5] [TYPE Function] [SEMANTICS translate,run,execute] # @BRIEF Execute a translation run: dispatch executor, generate SQL, submit to Superset. # @PRE run is in PENDING status. # @POST Run is executed, SQL generated, Superset submission attempted. # @SIDE_EFFECT LLM calls, DB writes, Superset API calls. def execute_run( self, run: TranslationRun, on_batch_progress: Optional[Callable[[str, int, int, int, int], None]] = None, skip_insert: bool = False, full_translation: bool = False, ) -> TranslationRun: with belief_scope("TranslationOrchestrator.execute_run"): job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first() if not job: raise ValueError(f"Job '{run.job_id}' not found") self._job = job if run.status != "PENDING": raise ValueError( f"Cannot execute run in status '{run.status}'. " f"Run must be in PENDING status." ) logger.reason("Executing run", { "run_id": run.id, "job_id": job.id, "skip_insert": skip_insert, }) # Record translation phase start self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="TRANSLATION_PHASE_STARTED", payload={}, created_by=self.current_user, ) # Dispatch executor executor = TranslationExecutor( self.db, self.config_manager, self.current_user, on_batch_progress=on_batch_progress, ) try: run = executor.execute_run(run, llm_progress_callback=None, full_translation=full_translation) except Exception as e: logger.explore("Translation execution failed", { "run_id": run.id, "error": str(e), }) run.status = "FAILED" run.error_message = f"Translation execution failed: {e}" run.completed_at = datetime.now(timezone.utc) self.db.flush() self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="RUN_FAILED", payload={"error": str(e), "phase": "translation"}, created_by=self.current_user, ) self.db.commit() return run # Record translation phase complete self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="TRANSLATION_PHASE_COMPLETED", payload={ "total": run.total_records, "successful": run.successful_records, "failed": run.failed_records, "skipped": run.skipped_records, }, created_by=self.current_user, ) # Skip insert phase if requested (e.g., for preview-only execution) if skip_insert: run.status = "COMPLETED" run.completed_at = datetime.now(timezone.utc) self.db.flush() self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="RUN_COMPLETED", payload={"skip_insert": True}, created_by=self.current_user, ) self.db.commit() return run # Generate SQL and submit to Superset insert_result = self._generate_and_insert_sql(job, run) run.insert_status = insert_result.get("status") run.superset_execution_id = str(insert_result.get("query_id") or "") run.superset_execution_log = insert_result run.status = "COMPLETED" if insert_result.get("status") == "success" else "COMPLETED" if insert_result.get("error_message"): run.error_message = insert_result["error_message"] run.completed_at = datetime.now(timezone.utc) self.db.flush() # Record terminal event terminal_event = "RUN_COMPLETED" self.event_log.log_event( job_id=job.id, run_id=run.id, event_type=terminal_event, payload={ "insert_status": insert_result.get("status"), "query_id": insert_result.get("query_id"), "rows_affected": insert_result.get("rows_affected"), "total_records": run.total_records, "successful": run.successful_records, "failed": run.failed_records, "skipped": run.skipped_records, }, created_by=self.current_user, ) self.db.commit() self.db.refresh(run) logger.reflect("Run execution complete", { "run_id": run.id, "status": run.status, "insert_status": run.insert_status, }) return run # #endregion execute_run # #region _generate_and_insert_sql [C:5] [TYPE Function] [SEMANTICS translate,sql,insert] # @BRIEF Generate INSERT SQL from successful records and submit to Superset SQL Lab. # @PRE job has target table configured. run has successful records. # @POST SQL is generated and submitted. Returns execution result. # @SIDE_EFFECT Superset API call; event logging. def _generate_and_insert_sql( self, job: TranslationJob, run: TranslationRun, ) -> Dict[str, Any]: with belief_scope("TranslationOrchestrator._generate_and_insert_sql"): # Fetch successful records records = ( self.db.query(TranslationRecord) .filter( TranslationRecord.run_id == run.id, TranslationRecord.status == "SUCCESS", TranslationRecord.target_sql.isnot(None), ) .all() ) if not records: logger.reason("No successful records to insert", {"run_id": run.id}) return {"status": "skipped", "reason": "no_records", "query_id": None} logger.reason(f"Generating SQL for {len(records)} records", { "run_id": run.id, "dialect": job.database_dialect or job.target_dialect, }) # Build rows for SQL generation # Only include the translation column — context columns are for LLM only columns = [] if job.translation_column: columns.append(job.translation_column) # Include key columns if present (for UPSERT matching) if job.target_key_cols: for k in job.target_key_cols: if k not in columns: columns.append(k) rows_for_sql = [] for rec in records: row_data = {} if job.translation_column: row_data[job.translation_column] = rec.target_sql or "" if job.target_key_cols: source_data = rec.source_data or {} for k in job.target_key_cols: # Use source_data value if available, fall back to empty string row_data[k] = source_data.get(k, "") rows_for_sql.append(row_data) if not columns: # Use target_sql as the sole column columns = [job.translation_column or "translated_text"] rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records] # Generate SQL try: sql, row_count = SQLGenerator.generate( dialect=job.database_dialect or job.target_dialect or "postgresql", target_schema=job.target_schema, target_table=job.target_table or "translated_data", columns=columns, rows=rows_for_sql, key_columns=job.target_key_cols, upsert_strategy=job.upsert_strategy or "MERGE", ) except ValueError as e: logger.explore("SQL generation failed", {"error": str(e)}) return {"status": "failed", "error_message": str(e), "query_id": None} # Log insert phase start logger.reason("Insert SQL generated", { "run_id": run.id, "sql_preview": sql[:500], "sql_length": len(sql), "row_count": row_count, "dialect": job.database_dialect or job.target_dialect or "postgresql", "columns": columns, "has_key_cols": bool(job.target_key_cols), }) self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="INSERT_PHASE_STARTED", payload={"sql_length": len(sql), "row_count": row_count}, created_by=self.current_user, ) # Submit to Superset try: env_id = job.environment_id or job.source_dialect or "" # Resolve database_id: explicit target_database_id from job (int or UUID), or resolve from environment target_db_id = None if job.target_database_id: try: target_db_id = int(job.target_database_id) except (ValueError, TypeError): # Could be a UUID — pass as-is, executor will resolve it target_db_id = job.target_database_id logger.reason("target_database_id is not an integer, will resolve as UUID", { "target_database_id": job.target_database_id, }) executor = SupersetSqlLabExecutor(self.config_manager, env_id, database_id=target_db_id) result = executor.execute_and_poll( sql=sql, max_polls=30, poll_interval_seconds=2.0, ) except Exception as e: logger.explore("Superset SQL submission failed", { "run_id": run.id, "error": str(e), }) result = {"status": "failed", "error_message": str(e), "query_id": None} # Log insert phase complete self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="INSERT_PHASE_COMPLETED", payload=result, created_by=self.current_user, ) return result # #endregion _generate_and_insert_sql # #region _validate_preconditions [C:5] [TYPE Function] [SEMANTICS translate,validation] # @BRIEF Validate preconditions before starting a translation run. # @PRE None. # @POST Raises ValueError if preconditions are not met. # @SIDE_EFFECT None. def _validate_preconditions( self, job: TranslationJob, is_scheduled: bool = False, ) -> None: with belief_scope("TranslationOrchestrator._validate_preconditions"): # Job must be in valid status if job.status in ("DRAFT",): raise ValueError( f"Cannot run job '{job.id}' in status '{job.status}'. " f"Job must be READY, ACTIVE, or COMPLETED." ) # Must have target table configured if not job.target_table: raise ValueError( f"Job '{job.id}' has no target table configured. " "Configure a target table before running." ) # Must have LLM provider configured if not job.provider_id: raise ValueError( f"Job '{job.id}' has no LLM provider configured. " "Select an LLM provider before running." ) # Must have a translation column if not job.translation_column: raise ValueError( f"Job '{job.id}' has no translation column configured. " "Select a translation column before running." ) # For manual runs, must have accepted preview if not is_scheduled: accepted_session = ( self.db.query(TranslationPreviewSession) .filter( TranslationPreviewSession.job_id == job.id, TranslationPreviewSession.status == "APPLIED", ) .order_by(TranslationPreviewSession.created_at.desc()) .first() ) if not accepted_session: raise ValueError( f"Job '{job.id}' has no accepted preview session. " "Run and accept a preview before executing a manual translation run." ) logger.reason("Preconditions validated", { "job_id": job.id, "is_scheduled": is_scheduled, }) # #endregion _validate_preconditions # #region retry_failed_batches [C:5] [TYPE Function] [SEMANTICS translate,retry,batch] # @BRIEF Retry failed batches in a run by re-processing failed records. # @PRE run exists and has failed batches. # @POST Failed batches are re-processed; run status and stats updated. # @SIDE_EFFECT LLM calls; DB writes; event logging. def retry_failed_batches( self, run_id: str, ) -> TranslationRun: with belief_scope("TranslationOrchestrator.retry_failed_batches"): run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first() if not run: raise ValueError(f"Run '{run_id}' not found") job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first() if not job: raise ValueError(f"Job '{run.job_id}' not found") self._job = job # Find failed batches failed_batches = ( self.db.query(TranslationBatch) .filter( TranslationBatch.run_id == run_id, TranslationBatch.status.in_(["FAILED", "COMPLETED_WITH_ERRORS"]), ) .all() ) if not failed_batches: raise ValueError(f"No failed batches found for run '{run_id}'") logger.reason("Retrying failed batches", { "run_id": run_id, "batch_count": len(failed_batches), }) self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="RUN_RETRYING", payload={"batch_count": len(failed_batches)}, created_by=self.current_user, ) # Re-process each failed batch executor = TranslationExecutor(self.db, self.config_manager, self.current_user) run.status = "RUNNING" self.db.flush() for batch in failed_batches: # Fetch failed records for this batch failed_records = ( self.db.query(TranslationRecord) .filter( TranslationRecord.batch_id == batch.id, TranslationRecord.status == "FAILED", ) .all() ) if not failed_records: continue # Build rows from failed records retry_rows = [] for rec in failed_records: retry_rows.append({ "row_index": rec.source_object_id or "0", "source_text": rec.source_sql or "", "approved_translation": None, "source_object_name": rec.source_object_name or "", }) # Process retry batch result = executor._process_batch( job=job, run_id=run_id, batch_index=batch.batch_index, batch_rows=retry_rows, ) # Update run stats run.successful_records = (run.successful_records or 0) + result["successful"] run.failed_records = (run.failed_records or 0) + result["failed"] run.skipped_records = (run.skipped_records or 0) + result["skipped"] self.db.flush() # Update run status if run.failed_records == 0: run.status = "COMPLETED" elif run.successful_records == 0: run.status = "FAILED" else: run.status = "COMPLETED" run.completed_at = datetime.now(timezone.utc) self.db.flush() self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="RUN_COMPLETED", payload={"retry": True}, created_by=self.current_user, ) self.db.commit() logger.reflect("Retry complete", { "run_id": run_id, "status": run.status, }) return run # #endregion retry_failed_batches # #region retry_insert [C:5] [TYPE Function] [SEMANTICS translate,retry,insert] # @BRIEF Retry the SQL insert phase for a completed run. # @PRE run exists and has successful records. # @POST SQL is regenerated and re-submitted to Superset. # @SIDE_EFFECT Superset API call; event logging. def retry_insert( self, run_id: str, ) -> TranslationRun: with belief_scope("TranslationOrchestrator.retry_insert"): run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first() if not run: raise ValueError(f"Run '{run_id}' not found") job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first() if not job: raise ValueError(f"Job '{run.job_id}' not found") self._job = job logger.reason("Retrying insert phase", { "run_id": run_id, }) self.event_log.log_event( job_id=job.id, run_id=run.id, event_type="RUN_RETRY_INSERT", payload={}, created_by=self.current_user, ) # Regenerate SQL and submit insert_result = self._generate_and_insert_sql(job, run) # Update run run.insert_status = insert_result.get("status") run.superset_execution_id = str(insert_result.get("query_id") or "") if insert_result.get("error_message"): run.error_message = insert_result["error_message"] self.db.flush() self.db.commit() self.db.refresh(run) logger.reflect("Insert retry complete", { "run_id": run_id, "insert_status": run.insert_status, }) return run # #endregion retry_insert # #region cancel_run [C:5] [TYPE Function] [SEMANTICS translate,run,cancel] # @BRIEF Cancel a running translation run. # @PRE run is in PENDING or RUNNING status. # @POST Run status is set to CANCELLED; event recorded. # @SIDE_EFFECT DB write; records event. def cancel_run(self, run_id: str) -> TranslationRun: with belief_scope("TranslationOrchestrator.cancel_run"): run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first() if not run: raise ValueError(f"Run '{run_id}' not found") if run.status not in ("PENDING", "RUNNING"): raise ValueError( f"Cannot cancel run in status '{run.status}'. " f"Only PENDING or RUNNING runs can be cancelled." ) run.status = "CANCELLED" run.completed_at = datetime.now(timezone.utc) self.db.flush() # Record terminal event directly — we are the source of the terminal event self.event_log._create_event_raw( job_id=run.job_id, run_id=run.id, event_type="RUN_CANCELLED", payload={}, created_by=self.current_user, ) self.db.commit() self.db.refresh(run) logger.reflect("Run cancelled", { "run_id": run_id, }) return run # #endregion cancel_run # #region get_run_status [C:5] [TYPE Function] [SEMANTICS translate,run,status] # @BRIEF Get run status with statistics and event invariant checks. # @PRE run_id exists. # @POST Returns dict with run details. def get_run_status(self, run_id: str) -> Dict[str, Any]: with belief_scope("TranslationOrchestrator.get_run_status"): run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first() if not run: raise ValueError(f"Run '{run_id}' not found") # Count batches batch_count = ( self.db.query(TranslationBatch) .filter(TranslationBatch.run_id == run_id) .count() ) # Get event invariants (lightweight — only fetches event_type column) invariants = self.event_log.get_run_event_invariants_lightweight(run_id) return { "id": run.id, "job_id": run.job_id, "status": run.status, "started_at": run.started_at.isoformat() if run.started_at else None, "completed_at": run.completed_at.isoformat() if run.completed_at else None, "error_message": run.error_message, "total_records": run.total_records or 0, "successful_records": run.successful_records or 0, "failed_records": run.failed_records or 0, "skipped_records": run.skipped_records or 0, "insert_status": run.insert_status, "superset_execution_id": run.superset_execution_id, "batch_count": batch_count, "event_invariants": invariants, "created_by": run.created_by, "created_at": run.created_at.isoformat() if run.created_at else None, } # #endregion get_run_status # #region get_run_records [C:5] [TYPE Function] [SEMANTICS translate,run,records] # @BRIEF Get paginated records for a run with optional status filter. # @PRE run_id exists. # @POST Returns dict with records and pagination info. def get_run_records( self, run_id: str, page: int = 1, page_size: int = 50, status_filter: Optional[str] = None, ) -> Dict[str, Any]: with belief_scope("TranslationOrchestrator.get_run_records"): query = self.db.query(TranslationRecord).filter( TranslationRecord.run_id == run_id ) if status_filter: query = query.filter(TranslationRecord.status == status_filter) total = query.count() offset = (page - 1) * page_size records = ( query.order_by(TranslationRecord.created_at.desc()) .offset(offset) .limit(page_size) .all() ) return { "items": [ { "id": r.id, "batch_id": r.batch_id, "source_sql": r.source_sql, "target_sql": r.target_sql, "source_object_type": r.source_object_type, "source_object_id": r.source_object_id, "source_object_name": r.source_object_name, "status": r.status, "error_message": r.error_message, "created_at": r.created_at.isoformat() if r.created_at else None, } for r in records ], "total": total, "page": page, "page_size": page_size, "status_filter": status_filter, } # #endregion get_run_records # #region get_run_history [C:5] [TYPE Function] [SEMANTICS translate,run,history] # @BRIEF Get paginated run history for a job. # @PRE job_id exists. # @POST Returns tuple of (total_count, list_of_runs). def get_run_history( self, job_id: str, page: int = 1, page_size: int = 20, ) -> Tuple[int, List[Dict[str, Any]]]: with belief_scope("TranslationOrchestrator.get_run_history"): query = self.db.query(TranslationRun).filter( TranslationRun.job_id == job_id ) total = query.count() runs = ( query.order_by(TranslationRun.created_at.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) return total, [ { "id": r.id, "job_id": r.job_id, "status": r.status, "started_at": r.started_at.isoformat() if r.started_at else None, "completed_at": r.completed_at.isoformat() if r.completed_at else None, "error_message": r.error_message, "total_records": r.total_records or 0, "successful_records": r.successful_records or 0, "failed_records": r.failed_records or 0, "skipped_records": r.skipped_records or 0, "insert_status": r.insert_status, "superset_execution_id": r.superset_execution_id, "created_by": r.created_by, "created_at": r.created_at.isoformat() if r.created_at else None, } for r in runs ] # #endregion get_run_history # #region _compute_config_hash [C:2] [TYPE Function] [SEMANTICS translate,hash] # @BRIEF Compute a SHA-256 hash of the job's current configuration for snapshot comparison. @staticmethod def _compute_config_hash(job: TranslationJob) -> str: import hashlib config_str = json.dumps({ "source_dialect": job.source_dialect, "target_dialect": job.target_dialect, "source_datasource_id": job.source_datasource_id, "translation_column": job.translation_column, "context_columns": job.context_columns, "target_language": job.target_language, "provider_id": job.provider_id, "batch_size": job.batch_size, "upsert_strategy": job.upsert_strategy, }, sort_keys=True) return hashlib.sha256(config_str.encode()).hexdigest()[:16] # #endregion _compute_config_hash # #region _compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS translate,hash] # @BRIEF Compute a SHA-256 hash of dictionary state for snapshot comparison. def _compute_dict_snapshot_hash(self, job_id: str) -> str: import hashlib from ...models.translate import TranslationJobDictionary dict_links = ( self.db.query(TranslationJobDictionary) .filter(TranslationJobDictionary.job_id == job_id) .all() ) dict_ids = sorted([dl.dictionary_id for dl in dict_links]) hash_input = ",".join(dict_ids) return hashlib.sha256(hash_input.encode()).hexdigest()[:16] # #endregion _compute_dict_snapshot_hash # #endregion TranslationOrchestrator # #endregion TranslationOrchestrator