| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823 |
- #!/usr/bin/env python
- """Run one idempotent real-time CPM control cycle."""
- from __future__ import annotations
- import argparse
- import json
- import logging
- import time
- import uuid
- from datetime import datetime, timedelta
- from decimal import Decimal, ROUND_HALF_UP
- from pathlib import Path
- from typing import Any
- from zoneinfo import ZoneInfo
- from dotenv import load_dotenv
- ROOT = Path(__file__).resolve().parent
- SHANGHAI = ZoneInfo("Asia/Shanghai")
- ACTIVE_STATUS = "AD_STATUS_NORMAL"
- SUSPEND_STATUS = "AD_STATUS_SUSPEND"
- DECISION_BOOST = "BOOST"
- DECISION_RESTORE = "RESTORE"
- DECISION_PAUSE = "PAUSE"
- DECISION_CUTOFF = "CUTOFF"
- DECISION_OFF_HOURS = "OFF_HOURS"
- DECISION_WAIT_DATA = "WAIT_DATA"
- logger = logging.getLogger("tencent_realtime_control")
- def load_environment() -> None:
- load_dotenv(ROOT.parent / "auto_put_ad_mini" / ".env", override=False)
- load_dotenv(Path.cwd() / ".env", override=False)
- def decide(cpm: Decimal, low_cpm: Decimal, high_cpm: Decimal) -> str:
- if cpm > high_cpm:
- return DECISION_BOOST
- if cpm < low_cpm:
- return DECISION_PAUSE
- return DECISION_RESTORE
- def boosted_bid(base_bid_fen: int, ratio: Decimal) -> int:
- return int(
- (Decimal(base_bid_fen) * ratio).quantize(
- Decimal("1"), rounding=ROUND_HALF_UP
- )
- )
- def partition_lag_hours(partition: str, now: datetime) -> float:
- try:
- partition_time = datetime.strptime(partition[:10], "%Y%m%d%H").replace(
- tzinfo=SHANGHAI
- )
- except (TypeError, ValueError) as exc:
- raise ValueError(f"Invalid CPM partition: {partition!r}") from exc
- return (now - partition_time).total_seconds() / 3600
- def should_refresh_inventory(
- daily_state: dict[str, Any],
- *,
- observed_partition: str,
- decision: str,
- now: datetime,
- refresh_minutes: int,
- ) -> bool:
- if (
- daily_state.get("last_observed_partition") != observed_partition
- or daily_state.get("last_decision") != decision
- ):
- return True
- refreshed_at = daily_state.get("last_inventory_refresh_at")
- if not refreshed_at:
- return True
- if refreshed_at.tzinfo is None:
- refreshed_at = refreshed_at.replace(tzinfo=SHANGHAI)
- return now - refreshed_at >= timedelta(minutes=refresh_minutes)
- def target_for_ad(
- *,
- decision: str,
- control_date: Any,
- current_status: str,
- current_bid: int,
- base_bid: int,
- boosted_date: Any,
- paused_by_strategy: bool,
- operator_pause_expired: bool,
- bid_up_ratio: Decimal,
- ) -> dict[str, Any] | None:
- managed_suspend = paused_by_strategy or operator_pause_expired
- manual_suspend = current_status != ACTIVE_STATUS and not managed_suspend
- target_bid = base_bid
- target_status: str | None = None
- next_boosted_date = boosted_date
- next_paused = False
- pause_reason = None
- limit_reached = False
- defer_to_next_day = False
- if manual_suspend:
- if decision not in (DECISION_PAUSE, DECISION_CUTOFF):
- return None
- return {
- "target_bid_fen": base_bid,
- "target_status": None,
- "boosted_date": boosted_date,
- "paused_by_strategy": False,
- "pause_reason": None,
- "boost_limit_reached": False,
- "bid_change_needed": current_bid != base_bid,
- "status_change_needed": False,
- "defer_to_next_day": False,
- }
- if decision == DECISION_BOOST:
- if boosted_date == control_date:
- limit_reached = True
- raised_bid = boosted_bid(base_bid, bid_up_ratio)
- if current_bid == raised_bid:
- target_bid = raised_bid
- else:
- target_bid = boosted_bid(base_bid, bid_up_ratio)
- next_boosted_date = control_date
- if managed_suspend:
- target_status = ACTIVE_STATUS
- elif decision == DECISION_RESTORE:
- if managed_suspend:
- target_status = ACTIVE_STATUS
- elif decision == DECISION_PAUSE:
- if managed_suspend:
- target_status = ACTIVE_STATUS
- defer_to_next_day = True
- elif decision == DECISION_CUTOFF:
- target_status = SUSPEND_STATUS if current_status != SUSPEND_STATUS else None
- next_paused = True
- pause_reason = "cutoff"
- else:
- raise ValueError(f"Unsupported decision: {decision}")
- return {
- "target_bid_fen": target_bid,
- "target_status": target_status,
- "boosted_date": next_boosted_date,
- "paused_by_strategy": next_paused,
- "pause_reason": pause_reason,
- "boost_limit_reached": limit_reached,
- "bid_change_needed": current_bid != target_bid,
- "status_change_needed": target_status is not None
- and target_status != current_status,
- "defer_to_next_day": defer_to_next_day,
- }
- def write_run_report(payload: dict[str, Any]) -> Path:
- output_dir = ROOT / "outputs"
- output_dir.mkdir(parents=True, exist_ok=True)
- timestamp = datetime.now(SHANGHAI).strftime("%Y%m%d_%H%M%S")
- mode = "apply" if payload["apply"] else "dry_run"
- path = output_dir / f"control_{mode}_{timestamp}.json"
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
- return path
- def execute_inventory_action(
- *,
- run_id: str,
- now: datetime,
- decision: str,
- observed_partition: str | None,
- observed_cpm: Decimal | None,
- apply: bool,
- accounts: list[dict[str, Any]],
- config: Any,
- tencent: Any,
- test_notification: bool = False,
- ) -> dict[str, Any]:
- from storage import (
- clear_operator_pause,
- insert_action_log,
- load_ad_states,
- upsert_ad_state,
- )
- from tencent_client import current_bid_fen, resolve_bid_field
- results: list[dict[str, Any]] = []
- failures = 0
- api_updates = 0
- for account in accounts:
- account_id = int(account["account_id"])
- control_mode = str(account.get("control_mode") or "FULL").upper()
- if control_mode == "PAUSE_ONLY" and decision not in (
- DECISION_PAUSE,
- DECISION_CUTOFF,
- ):
- continue
- states = load_ad_states(account_id)
- try:
- ads = tencent.get_ads(account_id)
- except Exception as exc:
- failures += 1
- results.append(
- {
- "account_id": account_id,
- "status": "account_fetch_failed",
- "error": str(exc),
- }
- )
- continue
- for ad in ads:
- adgroup_id = int(ad.get("adgroup_id") or 0)
- current_status = str(ad.get("configured_status") or "")
- state = states.get(adgroup_id)
- paused_by_strategy = bool(
- (state or {}).get("paused_by_strategy")
- )
- operator_pause_mode = str(
- (state or {}).get("operator_pause_mode") or ""
- )
- operator_resume_at = (state or {}).get("operator_resume_at")
- if operator_resume_at and operator_resume_at.tzinfo is None:
- operator_resume_at = operator_resume_at.replace(tzinfo=SHANGHAI)
- operator_manually_opened = bool(
- operator_pause_mode == "UNTIL_MANUAL"
- and current_status == ACTIVE_STATUS
- )
- operator_pause_expired = bool(
- operator_pause_mode == "UNTIL_NEXT_DELIVERY"
- and operator_resume_at
- and operator_resume_at <= now
- )
- begin_date = str(ad.get("begin_date") or "")
- operator_schedule_manually_resumed = bool(
- operator_pause_mode == "UNTIL_NEXT_DELIVERY"
- and operator_resume_at
- and operator_resume_at > now
- and begin_date
- and begin_date < operator_resume_at.date().isoformat()
- )
- operator_pause_active = bool(
- operator_pause_mode
- and not operator_manually_opened
- and not operator_schedule_manually_resumed
- and not operator_pause_expired
- )
- if (
- operator_manually_opened
- or operator_schedule_manually_resumed
- ) and apply:
- clear_operator_pause(
- account_id,
- adgroup_id,
- action="OPERATOR_MANUAL_OVERRIDE",
- action_at=now,
- )
- results.append(
- {
- "account_id": account_id,
- "audience_name": account.get("audience_name"),
- "adgroup_id": adgroup_id,
- "adgroup_name": ad.get("adgroup_name"),
- "decision": "OPERATOR_MANUAL_OVERRIDE",
- "before_status": current_status,
- "target_status": current_status,
- "status": "success",
- "bid_change_needed": False,
- "status_change_needed": False,
- }
- )
- continue
- if operator_pause_expired:
- if apply and current_status == ACTIVE_STATUS:
- clear_operator_pause(
- account_id,
- adgroup_id,
- action="OPERATOR_DAY_PAUSE_EXPIRED",
- action_at=now,
- )
- # Legacy DAY_PAUSE rows used SUSPEND. Keep the old one-time
- # recovery plan only for those rows; new rows stay NORMAL and
- # resume automatically through begin_date/time_series.
- operator_pause_expired = current_status != ACTIVE_STATUS
- if operator_pause_active:
- continue
- if (
- current_status != ACTIVE_STATUS
- and not paused_by_strategy
- and not operator_pause_expired
- and (
- state is None
- or decision not in (DECISION_PAUSE, DECISION_CUTOFF)
- )
- ):
- continue
- bid_field = (
- str(state["bid_field"])
- if state
- else resolve_bid_field(ad, account.get("bid_scene"))
- )
- current_bid = current_bid_fen(ad, bid_field)
- if current_bid is None or current_bid <= 0:
- failures += 1
- results.append(
- {
- "account_id": account_id,
- "adgroup_id": adgroup_id,
- "status": "invalid_current_bid",
- "bid_field": bid_field,
- }
- )
- continue
- base_bid = int(state["base_bid_fen"]) if state else current_bid
- plan = target_for_ad(
- decision=decision,
- control_date=now.date(),
- current_status=current_status,
- current_bid=current_bid,
- base_bid=base_bid,
- boosted_date=(state or {}).get("boosted_date"),
- paused_by_strategy=paused_by_strategy,
- operator_pause_expired=operator_pause_expired,
- bid_up_ratio=config.bid_up_ratio,
- )
- if plan is None:
- continue
- bid_hold = bool((state or {}).get("bid_hold"))
- if bid_hold:
- plan["target_bid_fen"] = current_bid
- plan["bid_change_needed"] = False
- before_begin_date = str(ad.get("begin_date") or "")
- target_begin_date = None
- begin_date_change_needed = False
- if plan["defer_to_next_day"]:
- next_day = (now.date() + timedelta(days=1)).isoformat()
- target_begin_date = (
- before_begin_date
- if before_begin_date > next_day
- else next_day
- )
- begin_date_change_needed = before_begin_date != target_begin_date
- needs_api = (
- plan["bid_change_needed"]
- or plan["status_change_needed"]
- or begin_date_change_needed
- )
- execution_status = "planned" if needs_api else "noop"
- error = None
- if apply and state is None:
- upsert_ad_state(
- account_id=account_id,
- adgroup_id=adgroup_id,
- adgroup_name=str(ad.get("adgroup_name") or ""),
- bid_field=bid_field,
- base_bid_fen=base_bid,
- boosted_date=None,
- paused_by_strategy=plan["paused_by_strategy"],
- pause_reason=plan["pause_reason"],
- last_action="REGISTER_BASE",
- action_at=now,
- )
- elif (
- apply
- and decision in (DECISION_PAUSE, DECISION_CUTOFF)
- and plan["paused_by_strategy"]
- ):
- upsert_ad_state(
- account_id=account_id,
- adgroup_id=adgroup_id,
- adgroup_name=str(ad.get("adgroup_name") or ""),
- bid_field=bid_field,
- base_bid_fen=base_bid,
- boosted_date=(state or {}).get("boosted_date"),
- paused_by_strategy=True,
- pause_reason=plan["pause_reason"],
- last_action=f"{decision}_PENDING",
- action_at=now,
- )
- if apply and needs_api:
- try:
- if begin_date_change_needed:
- tencent.update_ad_begin_dates(
- account_id,
- [adgroup_id],
- str(target_begin_date),
- )
- if plan["bid_change_needed"] or plan["status_change_needed"]:
- tencent.update_ad(
- account_id,
- adgroup_id,
- bid_field=bid_field
- if plan["bid_change_needed"]
- else None,
- target_bid_fen=plan["target_bid_fen"]
- if plan["bid_change_needed"]
- else None,
- target_status=plan["target_status"]
- if plan["status_change_needed"]
- else None,
- )
- execution_status = "success"
- api_updates += 1
- time.sleep(0.15)
- except Exception as exc:
- execution_status = "failed"
- error = str(exc)
- failures += 1
- result = {
- "account_id": account_id,
- "audience_name": account.get("audience_name"),
- "adgroup_id": adgroup_id,
- "adgroup_name": ad.get("adgroup_name"),
- "decision": decision,
- "bid_field": bid_field,
- "base_bid_fen": base_bid,
- "before_bid_fen": current_bid,
- "target_bid_fen": plan["target_bid_fen"],
- "before_status": current_status,
- "target_status": plan["target_status"],
- "boost_limit_reached": plan["boost_limit_reached"],
- "bid_hold": bid_hold,
- "bid_change_needed": plan["bid_change_needed"],
- "status_change_needed": plan["status_change_needed"],
- "before_begin_date": before_begin_date,
- "target_begin_date": target_begin_date,
- "begin_date_change_needed": begin_date_change_needed,
- "status": execution_status,
- "error": error,
- }
- results.append(result)
- if apply and execution_status != "failed":
- upsert_ad_state(
- account_id=account_id,
- adgroup_id=adgroup_id,
- adgroup_name=str(ad.get("adgroup_name") or ""),
- bid_field=bid_field,
- base_bid_fen=base_bid,
- boosted_date=plan["boosted_date"],
- paused_by_strategy=plan["paused_by_strategy"],
- pause_reason=plan["pause_reason"],
- last_action=decision,
- action_at=now,
- )
- if operator_pause_expired:
- clear_operator_pause(
- account_id,
- adgroup_id,
- action="OPERATOR_DAY_PAUSE_EXPIRED",
- action_at=now,
- )
- if apply and (needs_api or state is None or execution_status == "failed"):
- insert_action_log(
- {
- "run_id": run_id,
- "control_date": now.date(),
- "observed_partition": observed_partition,
- "observed_cpm": observed_cpm,
- "decision": decision,
- "account_id": account_id,
- "adgroup_id": adgroup_id,
- "adgroup_name": ad.get("adgroup_name"),
- "bid_field": bid_field,
- "base_bid_fen": base_bid,
- "before_bid_fen": current_bid,
- "target_bid_fen": plan["target_bid_fen"],
- "before_status": current_status,
- "target_status": plan["target_status"],
- "apply_mode": True,
- "execution_status": execution_status,
- "error_message": error,
- }
- )
- notification = {"status": "dry_run" if not apply else "no_actions"}
- if apply or test_notification:
- from feishu_notifier import send_action_notification
- report_actions: dict[int, list[dict[str, Any]]] = {}
- expected_statuses = {"success", "failed"} if apply else {"planned"}
- for result in results:
- if result.get("status") not in expected_statuses:
- continue
- if not (
- result.get("bid_change_needed")
- or result.get("begin_date_change_needed")
- or (
- result.get("decision") in (DECISION_PAUSE, DECISION_CUTOFF)
- and result.get("status_change_needed")
- )
- ):
- continue
- report_actions.setdefault(int(result["account_id"]), []).append(
- result
- )
- for account_id, account_results in report_actions.items():
- try:
- metrics = tencent.get_today_ad_metrics(
- account_id,
- [int(row["adgroup_id"]) for row in account_results],
- now.date(),
- )
- for result in account_results:
- ad_metrics = metrics.get(int(result["adgroup_id"]), {})
- result["today_cost_fen"] = int(
- ad_metrics.get("cost_fen") or 0
- )
- result["today_impressions"] = int(
- ad_metrics.get("impressions") or 0
- )
- result["today_clicks"] = int(
- ad_metrics.get("clicks") or 0
- )
- except Exception as exc:
- logger.warning(
- "Failed to fetch today's metrics account=%s: %s",
- account_id,
- exc,
- )
- notification = send_action_notification(
- run_id=run_id,
- now=now,
- results=results,
- bid_up_ratio=config.bid_up_ratio,
- decision=decision,
- observed_partition=observed_partition,
- observed_cpm=observed_cpm,
- test_mode=test_notification,
- )
- if apply:
- try:
- from feishu_notifier import (
- send_operator_reconciliation_notification,
- )
- send_operator_reconciliation_notification(results)
- except Exception:
- logger.exception(
- "Failed to notify operator manual override reconciliation"
- )
- return {
- "failures": failures,
- "api_updates": api_updates,
- "results": results,
- "notification": notification,
- }
- def run_cycle(
- *,
- now: datetime,
- apply: bool,
- cpm_override: Decimal | None = None,
- force_refresh: bool = False,
- test_notification: bool = False,
- ) -> dict[str, Any]:
- from odps_source import HourlyCpm, build_odps_client, fetch_latest_cpm
- from realtime_config import RealtimeControlConfig
- from storage import (
- advisory_lock,
- load_daily_state,
- load_realtime_accounts,
- save_daily_state,
- )
- from tencent_client import TencentClient
- config = RealtimeControlConfig.from_env()
- run_id = str(uuid.uuid4())
- payload: dict[str, Any] = {
- "run_id": run_id,
- "now": now.isoformat(),
- "apply": apply,
- "decision": None,
- "observed_partition": None,
- "observed_cpm": None,
- "summary": {},
- }
- with advisory_lock(config.lock_name) as acquired:
- if not acquired:
- payload["decision"] = "LOCKED_BY_OTHER_WORKER"
- return payload
- daily_state = load_daily_state(now.date())
- accounts = load_realtime_accounts()
- if now.hour < config.start_hour:
- payload["decision"] = DECISION_OFF_HOURS
- return payload
- if now.hour >= config.stop_hour:
- payload["decision"] = DECISION_CUTOFF
- if apply and bool(daily_state.get("cutoff_done")) and not force_refresh:
- payload["summary"] = {"status": "cutoff_already_done"}
- return payload
- execution = execute_inventory_action(
- run_id=run_id,
- now=now,
- decision=DECISION_RESTORE,
- observed_partition=None,
- observed_cpm=None,
- apply=apply,
- accounts=accounts,
- config=config,
- tencent=TencentClient(),
- test_notification=test_notification,
- )
- payload["summary"] = execution
- if apply and not execution["failures"]:
- save_daily_state(
- now.date(),
- cutoff_done=True,
- last_decision=DECISION_CUTOFF,
- last_inventory_refresh_at=now,
- last_evaluated_at=now,
- )
- return payload
- if apply and not bool(daily_state.get("morning_recovery_done")):
- execution = execute_inventory_action(
- run_id=run_id,
- now=now,
- decision=DECISION_RESTORE,
- observed_partition=None,
- observed_cpm=None,
- apply=True,
- accounts=accounts,
- config=config,
- tencent=TencentClient(),
- test_notification=test_notification,
- )
- payload["decision"] = "MORNING_RECOVERY"
- payload["summary"] = execution
- if not execution["failures"]:
- save_daily_state(
- now.date(),
- morning_recovery_done=True,
- last_inventory_refresh_at=now,
- last_evaluated_at=now,
- )
- else:
- return payload
- if cpm_override is not None:
- signal = HourlyCpm(
- partition=f"{now.strftime('%Y%m%d%H')}_override",
- hour_of_day=now.hour,
- impressions=0,
- cpm=float(cpm_override),
- )
- else:
- signal = fetch_latest_cpm(build_odps_client(), now.date())
- earliest_signal_hour = max(0, config.start_hour - 1)
- if signal is None or signal.hour_of_day < earliest_signal_hour:
- payload["decision"] = DECISION_WAIT_DATA
- payload["summary"] = {
- "reason": (
- "No same-day CPM partition at or after "
- f"{earliest_signal_hour:02d}:00"
- )
- }
- return payload
- observed_cpm = Decimal(str(signal.cpm))
- lag_hours = partition_lag_hours(signal.partition, now)
- payload.update(
- {
- "observed_partition": signal.partition,
- "observed_cpm": float(observed_cpm),
- "impressions": signal.impressions,
- "partition_lag_hours": round(lag_hours, 4),
- }
- )
- if lag_hours < 0 or lag_hours > config.max_partition_lag_hours:
- payload["decision"] = DECISION_WAIT_DATA
- payload["summary"] = {
- "reason": (
- f"CPM partition lag {lag_hours:.2f}h exceeds allowed "
- f"{config.max_partition_lag_hours}h"
- )
- }
- return payload
- decision = decide(observed_cpm, config.low_cpm, config.high_cpm)
- payload.update(
- {
- "decision": decision,
- }
- )
- refresh = force_refresh or should_refresh_inventory(
- daily_state,
- observed_partition=signal.partition,
- decision=decision,
- now=now,
- refresh_minutes=config.inventory_refresh_minutes,
- )
- if apply and not refresh:
- save_daily_state(now.date(), last_evaluated_at=now)
- payload["summary"] = {"status": "same_signal_noop"}
- return payload
- execution = execute_inventory_action(
- run_id=run_id,
- now=now,
- decision=decision,
- observed_partition=signal.partition,
- observed_cpm=observed_cpm,
- apply=apply,
- accounts=accounts,
- config=config,
- tencent=TencentClient(),
- test_notification=test_notification,
- )
- payload["summary"] = execution
- if apply and not execution["failures"]:
- save_daily_state(
- now.date(),
- last_observed_partition=signal.partition,
- last_observed_cpm=observed_cpm,
- last_decision=decision,
- last_inventory_refresh_at=now,
- last_evaluated_at=now,
- )
- return payload
- def parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- "--apply",
- action="store_true",
- help="Execute Tencent updates. Default is dry-run.",
- )
- parser.add_argument(
- "--at",
- help="Dry-run only: simulate Shanghai local time, ISO format.",
- )
- parser.add_argument(
- "--cpm-override",
- type=Decimal,
- help="Dry-run only: override latest CPM to validate decision branches.",
- )
- parser.add_argument("--force-refresh", action="store_true")
- parser.add_argument(
- "--test-notification",
- action="store_true",
- help=(
- "Dry-run only: use real inventory and metrics, then send a clearly "
- "marked simulated Feishu action sheet."
- ),
- )
- return parser.parse_args()
- def main() -> int:
- load_environment()
- args = parse_args()
- if args.apply and (args.at or args.cpm_override is not None):
- raise ValueError("--at and --cpm-override are forbidden with --apply")
- if args.apply and args.test_notification:
- raise ValueError("--test-notification is forbidden with --apply")
- now = (
- datetime.fromisoformat(args.at).replace(tzinfo=SHANGHAI)
- if args.at
- else datetime.now(SHANGHAI)
- )
- payload = run_cycle(
- now=now,
- apply=args.apply,
- cpm_override=args.cpm_override,
- force_refresh=args.force_refresh,
- test_notification=args.test_notification,
- )
- report = write_run_report(payload)
- compact = {
- "decision": payload["decision"],
- "observed_partition": payload.get("observed_partition"),
- "observed_cpm": payload.get("observed_cpm"),
- "apply": payload["apply"],
- "api_updates": payload.get("summary", {}).get("api_updates", 0),
- "failures": payload.get("summary", {}).get("failures", 0),
- "notification": payload.get("summary", {}).get("notification", {}).get(
- "status"
- ),
- "report": str(report),
- }
- print(json.dumps(compact, ensure_ascii=False))
- return 1 if compact["failures"] else 0
- if __name__ == "__main__":
- logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s %(levelname)s %(name)s %(message)s",
- )
- try:
- raise SystemExit(main())
- except Exception as exc:
- logger.exception("Real-time control failed: %s", exc)
- raise SystemExit(1)
|