# -*- coding: utf-8 -*- """运行记录追踪器""" import json from datetime import datetime class RunTracker: """ETL运行记录管理""" def __init__(self, db_connection): self.db = db_connection def create_run(self, task_id: int, store_id: int, run_uuid: str, export_dir: str, log_path: str, status: str, window_start: datetime = None, window_end: datetime = None, window_minutes: int = None, overlap_seconds: int = None, request_params: dict = None) -> int: """创建运行记录""" sql = """ INSERT INTO etl_admin.etl_run( run_uuid, task_id, store_id, status, started_at, window_start, window_end, window_minutes, overlap_seconds, fetched_count, loaded_count, updated_count, skipped_count, error_count, unknown_fields, export_dir, log_path, request_params, manifest, error_message, extra ) VALUES ( %s, %s, %s, %s, now(), %s, %s, %s, %s, 0, 0, 0, 0, 0, 0, %s, %s, %s, '{}'::jsonb, NULL, '{}'::jsonb ) RETURNING run_id """ result = self.db.query( sql, (run_uuid, task_id, store_id, status, window_start, window_end, window_minutes, overlap_seconds, export_dir, log_path, json.dumps(request_params or {}, ensure_ascii=False)) ) run_id = result[0]["run_id"] self.db.commit() return run_id def update_run( self, run_id: int, counts: dict, status: str, ended_at: datetime = None, manifest: dict = None, error_message: str = None, window: dict | None = None, request_params: dict | None = None, overlap_seconds: int | None = None, ): """更新运行记录""" sql = """ UPDATE etl_admin.etl_run SET fetched_count = %s, loaded_count = %s, updated_count = %s, skipped_count = %s, error_count = %s, unknown_fields = %s, status = %s, ended_at = %s, manifest = %s, error_message = %s, window_start = COALESCE(%s, window_start), window_end = COALESCE(%s, window_end), window_minutes = COALESCE(%s, window_minutes), overlap_seconds = COALESCE(%s, overlap_seconds), request_params = CASE WHEN %s IS NULL THEN request_params ELSE %s::jsonb END WHERE run_id = %s """ def _count(v, default: int = 0) -> int: if v is None: return default if isinstance(v, bool): return int(v) if isinstance(v, int): return int(v) if isinstance(v, str): try: return int(v) except Exception: return default if isinstance(v, (list, tuple, set, dict)): try: return len(v) except Exception: return default return default safe_counts = counts or {} window_start = None window_end = None window_minutes = None if isinstance(window, dict): window_start = window.get("start") or window.get("window_start") window_end = window.get("end") or window.get("window_end") window_minutes = window.get("minutes") or window.get("window_minutes") request_json = None if request_params is None else json.dumps(request_params or {}, ensure_ascii=False) self.db.execute( sql, ( _count(safe_counts.get("fetched", 0)), _count(safe_counts.get("inserted", 0)), _count(safe_counts.get("updated", 0)), _count(safe_counts.get("skipped", 0)), _count(safe_counts.get("errors", 0)), _count(safe_counts.get("unknown_fields", 0)), status, ended_at, json.dumps(manifest or {}, ensure_ascii=False), error_message, window_start, window_end, window_minutes, overlap_seconds, request_json, request_json, run_id, ), ) self.db.commit()