#!/usr/bin/env python3 """Roll up and optionally prune persisted admin health samples.""" from __future__ import annotations import argparse import asyncio import json import os import sys from dataclasses import asdict from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] API_ROOT = REPO_ROOT / "apps" / "api" if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") if hasattr(sys.stderr, "reconfigure"): sys.stderr.reconfigure(encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( "Roll up app.admin_health_event into app.admin_health_daily_rollup " "and optionally prune old raw samples." ) ) parser.add_argument("--rollup-days", type=int, required=True, help="Roll up raw samples older than this many days.") parser.add_argument("--retention-days", type=int, required=True, help="Prune raw samples older than this many days.") parser.add_argument("--apply", action="store_true", help="Mutate the database. Default is dry-run.") parser.add_argument( "--allow-non-dev-apply", action="store_true", help="Required with --apply outside ENVIRONMENT=dev.", ) parser.add_argument("--json", action="store_true", help="Print compact JSON.") return parser def _prepare_imports() -> None: os.chdir(API_ROOT) api_root = str(API_ROOT) if api_root not in sys.path: sys.path.insert(0, api_root) async def _run(args: argparse.Namespace) -> dict[str, object]: _prepare_imports() from app.auth_sessions import ensure_runtime_tables from app.config import settings from app.db import close_pool, init_pool from app.services.admin_health_maintenance import maintain_admin_health_events if args.apply and settings.environment != "dev" and not args.allow_non_dev_apply: raise RuntimeError("--apply outside ENVIRONMENT=dev requires --allow-non-dev-apply") await init_pool() try: await ensure_runtime_tables() result = await maintain_admin_health_events( rollup_days=args.rollup_days, retention_days=args.retention_days, apply=args.apply, ) finally: await close_pool() return asdict(result) def _print_result(result: dict[str, object], *, as_json: bool) -> None: if as_json: print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) return mode = "applied" if result["applied"] else "dry-run" print( "{mode}: rollup_events={rollup_event_count}, rollup_buckets={rollup_bucket_count}, " "upserted_rollups={upserted_rollups}, prunable_events={prunable_event_count}, " "deleted_events={deleted_events}".format(mode=mode, **result) ) def main() -> int: args = build_parser().parse_args() try: result = asyncio.run(_run(args)) except Exception as exc: # noqa: BLE001 - CLI should report compact failure. payload = {"ok": False, "error_type": exc.__class__.__name__, "error": str(exc)} print(json.dumps(payload, ensure_ascii=False, separators=(",", ":")), file=sys.stderr) return 2 _print_result(result, as_json=args.json) return 0 if __name__ == "__main__": raise SystemExit(main())