| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888 |
- """MySQL persistence for versioned ROI runs, snapshots, and action audit."""
- from __future__ import annotations
- import json
- from datetime import date, datetime
- from decimal import Decimal
- from typing import Any, Iterable
- from storage import connect
- FINAL_STATUSES = {
- "COMPLETED",
- "PARTIAL",
- "REJECTED",
- "EXPIRED",
- }
- def _is_expired(expires_at: datetime | None, now: datetime) -> bool:
- if expires_at is None:
- return False
- if expires_at.tzinfo is None and now.tzinfo is not None:
- now = now.replace(tzinfo=None)
- elif expires_at.tzinfo is not None and now.tzinfo is None:
- expires_at = expires_at.replace(tzinfo=None)
- return now > expires_at
- def json_dumps(value: Any) -> str:
- def default(item: Any) -> Any:
- if isinstance(item, (datetime, date)):
- return item.isoformat()
- if isinstance(item, Decimal):
- return str(item)
- if hasattr(item, "item"):
- return item.item()
- if hasattr(item, "tolist"):
- return item.tolist()
- raise TypeError(f"Unsupported JSON value: {type(item)!r}")
- return json.dumps(value, ensure_ascii=False, default=default)
- def publish_fission_parameter_release(
- release: dict[str, Any],
- values: Iterable[dict[str, Any]],
- *,
- published_by: str,
- ) -> bool:
- """Publish an immutable parameter version; return False if already present."""
- value_rows = list(values)
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT content_sha256 FROM roi_fission_parameter_release "
- "WHERE version=%s FOR UPDATE",
- (release["version"],),
- )
- existing = cursor.fetchone()
- if existing:
- if existing["content_sha256"] != release["content_sha256"]:
- raise ValueError(
- "传播裂变系数版本已存在且内容不同,禁止覆盖: "
- f"{release['version']}"
- )
- connection.rollback()
- return False
- cursor.execute(
- """
- INSERT INTO roi_fission_parameter_release
- (version, cohort_date, observation_end_date, horizon_days,
- run_suffix, content_sha256, metadata_json, published_by)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
- """,
- (
- release["version"],
- release["cohort_date"],
- release["observation_end_date"],
- release["horizon_days"],
- release["run_suffix"],
- release["content_sha256"],
- json_dumps(release["metadata"]),
- published_by,
- ),
- )
- cursor.executemany(
- """
- INSERT INTO roi_fission_parameter_value
- (version, entity_type, match_level, key_primary,
- key_secondary, multiplier, multiplier_vs_first)
- VALUES (%s,%s,%s,%s,%s,%s,%s)
- """,
- [
- (
- release["version"],
- row["entity_type"],
- row["match_level"],
- row["key_primary"],
- row["key_secondary"],
- row["multiplier"],
- row.get("multiplier_vs_first"),
- )
- for row in value_rows
- ],
- )
- connection.commit()
- return True
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def load_fission_parameter_release(
- version: str,
- ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM roi_fission_parameter_release WHERE version=%s",
- (version,),
- )
- release = cursor.fetchone()
- if not release:
- raise ValueError(f"数据库不存在传播裂变系数版本: {version}")
- cursor.execute(
- """
- SELECT entity_type, match_level, key_primary,
- key_secondary, multiplier, multiplier_vs_first
- FROM roi_fission_parameter_value
- WHERE version=%s
- ORDER BY entity_type, match_level, key_primary, key_secondary
- """,
- (version,),
- )
- return release, list(cursor.fetchall())
- finally:
- connection.close()
- def create_or_load_run(record: dict[str, Any]) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT IGNORE INTO roi_metric_run
- (run_id, run_key, metric_version, policy_version,
- fission_parameter_version, fission_cohort_date,
- start_date, end_date, config_json, status)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'COMPUTING')
- """,
- (
- record["run_id"],
- record["run_key"],
- record["metric_version"],
- record["policy_version"],
- record["fission_parameter_version"],
- record["fission_cohort_date"],
- record["start_date"],
- record["end_date"],
- json_dumps(record["config"]),
- ),
- )
- cursor.execute(
- "SELECT * FROM roi_metric_run WHERE run_key=%s",
- (record["run_key"],),
- )
- run = cursor.fetchone()
- if run:
- return run
- cursor.execute(
- "SELECT * FROM roi_metric_run WHERE run_id=%s",
- (record["run_id"],),
- )
- collision = cursor.fetchone()
- if collision:
- raise RuntimeError(
- f"ROI run_id already exists with a different run_key: "
- f"{record['run_id']}"
- )
- return {}
- finally:
- connection.close()
- def load_run(run_id: str) -> dict[str, Any] | None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute("SELECT * FROM roi_metric_run WHERE run_id=%s", (run_id,))
- return cursor.fetchone()
- finally:
- connection.close()
- def replace_run_results(
- run_id: str,
- *,
- snapshots: Iterable[dict[str, Any]],
- actions: Iterable[dict[str, Any]],
- thresholds: dict[str, Any],
- ) -> tuple[int, int]:
- snapshot_rows = list(snapshots)
- action_rows = list(actions)
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT status FROM roi_metric_run WHERE run_id=%s FOR UPDATE",
- (run_id,),
- )
- run = cursor.fetchone()
- if not run:
- raise ValueError(f"ROI run not found: {run_id}")
- if run["status"] not in {"COMPUTING", "COMPUTED", "FAILED"}:
- raise RuntimeError(
- f"ROI run {run_id} cannot be replaced in status {run['status']}"
- )
- cursor.execute("DELETE FROM roi_action_item WHERE run_id=%s", (run_id,))
- cursor.execute("DELETE FROM roi_entity_snapshot WHERE run_id=%s", (run_id,))
- snapshot_columns = [
- "run_id", "entity_hash", "entity_type", "channel",
- "account_id", "account_name", "adgroup_id", "adgroup_name",
- "dynamic_creative_id", "audience_name", "conversion_goal",
- "partner_name", "official_account_name",
- "fission_parameter_version", "fission_cohort_date",
- "fission_multiplier_vs_t0", "fission_multiplier_vs_first",
- "fission_match_level",
- "fission_source", "ad_age",
- "avg_first_uv", "first_uv",
- "t0_fission_count", "t0_fission_rate", "cost",
- "efficiency_revenue", "fission_revenue", "actual_total_revenue",
- "actual_roi", "predicted_tail_revenue",
- "predicted_fission_revenue", "total_revenue", "roi",
- "stop_threshold", "scale_threshold", "recommended_action",
- "action_reason", "execution_mode", "ineligible_reason",
- "daily_metrics_json",
- ]
- if snapshot_rows:
- cursor.executemany(
- f"INSERT INTO roi_entity_snapshot ({', '.join(snapshot_columns)}) "
- f"VALUES ({', '.join(['%s'] * len(snapshot_columns))})",
- [
- [
- json_dumps(row[column])
- if column == "daily_metrics_json"
- else row.get(column)
- for column in snapshot_columns
- ]
- for row in snapshot_rows
- ],
- )
- action_columns = [
- "run_id", "idempotency_key", "action_type", "account_id",
- "adgroup_id", "dynamic_creative_id", "execution_status",
- ]
- if action_rows:
- cursor.executemany(
- f"INSERT INTO roi_action_item ({', '.join(action_columns)}) "
- f"VALUES ({', '.join(['%s'] * len(action_columns))})",
- [
- [row.get(column) for column in action_columns]
- for row in action_rows
- ],
- )
- cursor.execute(
- """
- UPDATE roi_metric_run
- SET thresholds_json=%s,
- status='COMPUTED',
- entity_count=%s,
- candidate_count=%s,
- actionable_count=%s,
- error_message=NULL
- WHERE run_id=%s
- """,
- (
- json_dumps(thresholds),
- len(snapshot_rows),
- sum(bool(row.get("recommended_action")) for row in snapshot_rows),
- len(action_rows),
- run_id,
- ),
- )
- connection.commit()
- return len(snapshot_rows), len(action_rows)
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def mark_published(
- run_id: str,
- *,
- sheet_token: str,
- sheet_url: str,
- message_id: str,
- expires_at: datetime | None,
- requires_approval: bool,
- ) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- affected = cursor.execute(
- """
- UPDATE roi_metric_run
- SET status=%s, sheet_token=%s, sheet_url=%s,
- message_id=%s, expires_at=%s, error_message=NULL
- WHERE run_id=%s AND status IN ('COMPUTED','PENDING_APPROVAL')
- """,
- (
- "PENDING_APPROVAL" if requires_approval else "COMPLETED",
- sheet_token,
- sheet_url,
- message_id,
- expires_at,
- run_id,
- ),
- )
- if not affected:
- raise RuntimeError(f"ROI run cannot be published: {run_id}")
- finally:
- connection.close()
- def mark_failed(run_id: str, error_message: str) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE roi_metric_run
- SET status='FAILED', error_message=%s
- WHERE run_id=%s AND status NOT IN ('COMPLETED','PARTIAL','REJECTED')
- """,
- (error_message[:4000], run_id),
- )
- finally:
- connection.close()
- def upsert_agency_delivery(record: dict[str, Any]) -> dict[str, Any]:
- """Create or refresh one idempotent agency delivery record."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO roi_agency_delivery
- (run_id, agency_name, agency_report_version, file_path,
- file_sha256, creative_rows, ad_rows,
- route_fingerprint, status)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'PENDING')
- ON DUPLICATE KEY UPDATE
- status=CASE
- WHEN status='SENT' THEN status
- WHEN NOT (route_fingerprint <=> VALUES(route_fingerprint))
- THEN 'PENDING'
- ELSE status
- END,
- error_message=CASE
- WHEN NOT (route_fingerprint <=> VALUES(route_fingerprint))
- THEN NULL
- ELSE error_message
- END,
- file_path=IF(status='SENT', file_path, VALUES(file_path)),
- file_sha256=IF(status='SENT', file_sha256, VALUES(file_sha256)),
- creative_rows=VALUES(creative_rows),
- ad_rows=VALUES(ad_rows),
- route_fingerprint=VALUES(route_fingerprint)
- """,
- (
- record["run_id"],
- record["agency_name"],
- record["agency_report_version"],
- record["file_path"],
- record["file_sha256"],
- int(record.get("creative_rows") or 0),
- int(record.get("ad_rows") or 0),
- record.get("route_fingerprint"),
- ),
- )
- cursor.execute(
- """
- SELECT * FROM roi_agency_delivery
- WHERE run_id=%s AND agency_name=%s AND agency_report_version=%s
- """,
- (
- record["run_id"],
- record["agency_name"],
- record["agency_report_version"],
- ),
- )
- return cursor.fetchone() or {}
- finally:
- connection.close()
- def load_agency_deliveries(run_id: str) -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT * FROM roi_agency_delivery
- WHERE run_id=%s
- ORDER BY agency_name
- """,
- (run_id,),
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def update_agency_delivery(
- delivery_id: int,
- *,
- status: str,
- sheet_token: str | None = None,
- sheet_url: str | None = None,
- response_code: str | None = None,
- error_message: str | None = None,
- sent_at: datetime | None = None,
- increment_attempt: bool = False,
- ) -> None:
- allowed_statuses = {"PENDING", "UPLOADED", "SENT", "SKIPPED", "FAILED"}
- if status not in allowed_statuses:
- raise ValueError(f"Unsupported agency delivery status: {status}")
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE roi_agency_delivery
- SET status=%s,
- sheet_token=COALESCE(%s, sheet_token),
- sheet_url=COALESCE(%s, sheet_url),
- response_code=%s,
- error_message=%s,
- sent_at=COALESCE(%s, sent_at),
- attempt_count=attempt_count+%s
- WHERE id=%s
- """,
- (
- status,
- sheet_token,
- sheet_url,
- response_code,
- error_message[:4000] if error_message else None,
- sent_at,
- 1 if increment_attempt else 0,
- delivery_id,
- ),
- )
- finally:
- connection.close()
- def reject_run(run_id: str, *, sender_open_id: str, now: datetime) -> dict[str, Any]:
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM roi_metric_run WHERE run_id=%s FOR UPDATE",
- (run_id,),
- )
- run = cursor.fetchone()
- if not run:
- raise ValueError(f"ROI批次不存在: {run_id}")
- if run["status"] in FINAL_STATUSES:
- connection.commit()
- return run
- if run["status"] != "PENDING_APPROVAL":
- raise RuntimeError(f"ROI批次当前不可拒绝: {run['status']}")
- if _is_expired(run.get("expires_at"), now):
- cursor.execute(
- "UPDATE roi_metric_run SET status='EXPIRED' WHERE run_id=%s",
- (run_id,),
- )
- else:
- cursor.execute(
- """
- UPDATE roi_metric_run
- SET status='REJECTED', rejected_by=%s, rejected_at=%s
- WHERE run_id=%s
- """,
- (sender_open_id, now, run_id),
- )
- connection.commit()
- return load_run(run_id) or run
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def claim_run_for_execution(
- run_id: str,
- *,
- sender_open_id: str,
- now: datetime,
- ) -> dict[str, Any]:
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM roi_metric_run WHERE run_id=%s FOR UPDATE",
- (run_id,),
- )
- run = cursor.fetchone()
- if not run:
- raise ValueError(f"ROI批次不存在: {run_id}")
- if run["status"] in FINAL_STATUSES:
- connection.commit()
- return run
- if run["status"] == "PENDING_APPROVAL":
- if _is_expired(run.get("expires_at"), now):
- cursor.execute(
- "UPDATE roi_metric_run SET status='EXPIRED' WHERE run_id=%s",
- (run_id,),
- )
- connection.commit()
- return load_run(run_id) or run
- cursor.execute(
- """
- UPDATE roi_metric_run
- SET status='EXECUTING', approved_by=%s, approved_at=%s
- WHERE run_id=%s
- """,
- (sender_open_id, now, run_id),
- )
- elif run["status"] != "EXECUTING":
- raise RuntimeError(f"ROI批次当前不可确认: {run['status']}")
- connection.commit()
- return load_run(run_id) or run
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def load_action_items(run_id: str) -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM roi_action_item WHERE run_id=%s ORDER BY id",
- (run_id,),
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_pending_sheet_runs(now: datetime) -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT * FROM roi_metric_run
- WHERE status='PENDING_APPROVAL'
- AND sheet_token IS NOT NULL
- AND (expires_at IS NULL OR expires_at>=%s)
- ORDER BY created_at
- """,
- (now,),
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def expire_pending_sheet_runs(now: datetime) -> int:
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT run_id FROM roi_metric_run
- WHERE status='PENDING_APPROVAL'
- AND expires_at IS NOT NULL
- AND expires_at<%s
- FOR UPDATE
- """,
- (now,),
- )
- run_ids = [row["run_id"] for row in cursor.fetchall()]
- if run_ids:
- placeholders = ",".join(["%s"] * len(run_ids))
- cursor.execute(
- f"""
- UPDATE roi_action_item
- SET approval_status='EXPIRED', execution_status='EXPIRED',
- executed_at=%s
- WHERE run_id IN ({placeholders})
- AND approval_status='PENDING'
- """,
- [now, *run_ids],
- )
- cursor.execute(
- f"""
- UPDATE roi_metric_run SET status='EXPIRED'
- WHERE run_id IN ({placeholders})
- """,
- run_ids,
- )
- connection.commit()
- return len(run_ids)
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def record_sheet_decision(
- *,
- run_id: str,
- idempotency_key: str,
- decision: str,
- sheet_row_number: int,
- now: datetime,
- ) -> dict[str, Any] | None:
- if decision not in {"APPROVED", "REJECTED"}:
- raise ValueError(f"Unsupported ROI sheet decision: {decision}")
- connection = connect()
- connection.autocommit(False)
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT status, expires_at FROM roi_metric_run "
- "WHERE run_id=%s FOR UPDATE",
- (run_id,),
- )
- run = cursor.fetchone()
- if not run or run["status"] != "PENDING_APPROVAL":
- connection.rollback()
- return None
- if _is_expired(run.get("expires_at"), now):
- connection.rollback()
- return None
- cursor.execute(
- """
- SELECT * FROM roi_action_item
- WHERE run_id=%s AND idempotency_key=%s
- FOR UPDATE
- """,
- (run_id, idempotency_key),
- )
- item = cursor.fetchone()
- if not item:
- connection.rollback()
- return None
- if item["approval_status"] != "PENDING":
- connection.commit()
- return {**item, "decision_changed": False}
- if decision == "APPROVED":
- cursor.execute(
- """
- UPDATE roi_action_item
- SET approval_status='APPROVED', approval_source='FEISHU_SHEET',
- approved_at=%s, sheet_row_number=%s
- WHERE id=%s AND approval_status='PENDING'
- """,
- (now, sheet_row_number, item["id"]),
- )
- else:
- cursor.execute(
- """
- UPDATE roi_action_item
- SET approval_status='REJECTED', approval_source='FEISHU_SHEET',
- rejected_at=%s, sheet_row_number=%s,
- execution_status='REJECTED', executed_at=%s
- WHERE id=%s AND approval_status='PENDING'
- """,
- (now, sheet_row_number, now, item["id"]),
- )
- connection.commit()
- return {
- **item,
- "approval_status": decision,
- "sheet_row_number": sheet_row_number,
- "decision_changed": True,
- }
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def load_actions_by_ids(item_ids: Iterable[int]) -> list[dict[str, Any]]:
- ids = [int(value) for value in item_ids]
- if not ids:
- return []
- connection = connect()
- try:
- with connection.cursor() as cursor:
- placeholders = ",".join(["%s"] * len(ids))
- cursor.execute(
- f"SELECT * FROM roi_action_item WHERE id IN ({placeholders}) ORDER BY id",
- ids,
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_unnotified_action_details(
- run_ids: Iterable[str] | None = None,
- ) -> list[dict[str, Any]]:
- values = [str(value) for value in run_ids] if run_ids is not None else []
- run_filter = ""
- params: list[Any] = []
- if run_ids is not None:
- if not values:
- return []
- placeholders = ",".join(["%s"] * len(values))
- run_filter = f"AND a.run_id IN ({placeholders})"
- params.extend(values)
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"""
- SELECT a.*, r.sheet_url, s.adgroup_name, s.audience_name,
- s.cost, s.roi
- FROM roi_action_item a
- JOIN roi_metric_run r ON r.run_id=a.run_id
- LEFT JOIN roi_entity_snapshot s ON s.id=(
- SELECT MIN(s2.id) FROM roi_entity_snapshot s2
- WHERE s2.run_id=a.run_id
- AND s2.account_id=a.account_id
- AND s2.adgroup_id=a.adgroup_id
- AND (
- a.dynamic_creative_id IS NULL
- OR s2.dynamic_creative_id=a.dynamic_creative_id
- )
- )
- WHERE a.approval_status IN ('APPROVED','REJECTED')
- AND a.execution_status NOT IN ('PENDING','PREPARED')
- AND a.result_notified_at IS NULL
- {run_filter}
- ORDER BY a.run_id, a.id
- LIMIT 100
- """,
- params,
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def mark_action_notifications(
- item_ids: Iterable[int],
- *,
- notified_at: datetime | None,
- error: str | None,
- ) -> None:
- ids = [int(value) for value in item_ids]
- if not ids:
- return
- connection = connect()
- try:
- with connection.cursor() as cursor:
- placeholders = ",".join(["%s"] * len(ids))
- cursor.execute(
- f"""
- UPDATE roi_action_item
- SET result_notified_at=%s, notification_error=%s
- WHERE id IN ({placeholders})
- """,
- [notified_at, error[:4000] if error else None, *ids],
- )
- finally:
- connection.close()
- def finalize_sheet_run_if_resolved(run_id: str, *, now: datetime) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT
- SUM(approval_status='PENDING') AS pending_approvals,
- SUM(execution_status='SUCCESS') AS successes,
- SUM(execution_status IN ('FAILED','OUTCOME_UNKNOWN','VERIFY_FAILED')) AS failures,
- SUM(execution_status LIKE 'SKIPPED%%') AS skipped,
- SUM(approval_status='REJECTED') AS rejected
- FROM roi_action_item WHERE run_id=%s
- """,
- (run_id,),
- )
- counts = cursor.fetchone() or {}
- normalized = {key: int(value or 0) for key, value in counts.items()}
- if normalized["pending_approvals"]:
- return {"status": "PENDING_APPROVAL", **normalized}
- status = "PARTIAL" if normalized["failures"] else "COMPLETED"
- cursor.execute(
- "UPDATE roi_metric_run SET status=%s, executed_at=%s WHERE run_id=%s",
- (status, now, run_id),
- )
- return {"status": status, **normalized}
- finally:
- connection.close()
- def update_action_item(item_id: int, **values: Any) -> None:
- allowed = {
- "bid_field", "initial_base_bid_fen", "base_bid_fen", "before_bid_fen",
- "target_bid_fen", "before_status", "target_status", "readback_status",
- "execution_status", "skip_reason", "error_message", "pre_state_json",
- "readback_json", "executed_at", "notification_error",
- }
- unknown = set(values) - allowed
- if unknown:
- raise ValueError(f"Unsupported ROI action fields: {sorted(unknown)}")
- assignments: list[str] = []
- params: list[Any] = []
- for column, value in values.items():
- assignments.append(f"{column}=%s")
- params.append(
- json_dumps(value)
- if column in {"pre_state_json", "readback_json"} and value is not None
- else value
- )
- if not assignments:
- return
- params.append(item_id)
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"UPDATE roi_action_item SET {', '.join(assignments)} WHERE id=%s",
- params,
- )
- finally:
- connection.close()
- def finalize_run(run_id: str, *, now: datetime) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT
- SUM(execution_status='SUCCESS') AS successes,
- SUM(execution_status IN ('FAILED','OUTCOME_UNKNOWN','VERIFY_FAILED')) AS failures,
- SUM(execution_status LIKE 'SKIPPED%%') AS skipped
- FROM roi_action_item WHERE run_id=%s
- """,
- (run_id,),
- )
- counts = cursor.fetchone() or {}
- failures = int(counts.get("failures") or 0)
- status = "PARTIAL" if failures else "COMPLETED"
- cursor.execute(
- "UPDATE roi_metric_run SET status=%s, executed_at=%s WHERE run_id=%s",
- (status, now, run_id),
- )
- return {"status": status, **{key: int(value or 0) for key, value in counts.items()}}
- finally:
- connection.close()
|