#!/usr/bin/env python3 """Apply the Catalog-owned Credits approval Seed module.""" from __future__ import annotations import argparse import json import os from pathlib import Path import sys import time from typing import Any SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = SCRIPT_DIR.parent RUNTIME_DIR = REPO_ROOT / "deployments" / "offline" / "runtime" if str(RUNTIME_DIR) not in sys.path: sys.path.insert(0, str(RUNTIME_DIR)) import apply_credits_approval_seed as credits_seed # noqa: E402 import deploy_offline_env # noqa: E402 MODULE_ID = "credits_approval_seed" SUMMARY_FILENAME = "summary.json" SUMMARY_FORMAT = "deploy-seed-script-v1" PLANNED_COUNTS = { "email_templates": 4, "process_actions": 18, "process_definitions": 2, "process_nodes": 6, } INVOCATION_REASONS = { "test.credits_approval.post_quote_approval": "test credits approval seed", "watchdog.credits_approval.post_quote_approval": "watchdog credits approval seed", "exp.credits_approval.post_quote_approval": "exp credits approval seed", "staging.credits_approval.post_quote_approval": "staging credits approval seed", } class CreditsSeedAdapterError(RuntimeError): """Raised when the scripted Seed adapter contract is violated.""" def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Apply Credits approval Seed data.") parser.add_argument("--source-root", required=True) parser.add_argument("--module-id", required=True) parser.add_argument("--invocation-id", required=True) parser.add_argument("--data-source", action="append", required=True) parser.add_argument("--selection-json", required=True) parser.add_argument("--output-dir", required=True) parser.add_argument("--dry-run", action="store_true") return parser.parse_args(argv) def _selection(raw: str) -> dict[str, Any]: try: selection = json.loads(raw) except json.JSONDecodeError as exc: raise CreditsSeedAdapterError("selection must be valid JSON") from exc if selection != {}: raise CreditsSeedAdapterError("Credits approval Seed does not accept a selection") return selection def _seed_file(source_root: Path, data_sources: list[str]) -> Path: if len(data_sources) != 1: raise CreditsSeedAdapterError("Credits approval Seed requires one data source") expected = ( source_root / "deployments" / "offline" / "runtime" / "seeds" / "credits-approvals.seed.json" ).resolve(strict=True) actual = Path(data_sources[0]).expanduser().resolve(strict=True) if actual != expected: raise CreditsSeedAdapterError("data source is not the reviewed Credits approval Seed") return actual def _seed_counts(seed: dict[str, Any]) -> dict[str, int]: return { "email_templates": len(seed.get("emailTemplates") or []), "process_actions": len(seed.get("processActions") or []), "process_definitions": len(seed.get("processDefinitions") or []), "process_nodes": len(seed.get("processNodes") or []), } def _write_summary(path: Path, payload: dict[str, Any]) -> None: path.write_text( json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) def run_adapter(args: argparse.Namespace) -> tuple[dict[str, Any], Path]: started = time.perf_counter() source_root = Path(args.source_root).expanduser().resolve(strict=True) if args.module_id != MODULE_ID: raise CreditsSeedAdapterError("module id is not the Credits approval Seed module") reason = INVOCATION_REASONS.get(args.invocation_id) if reason is None: raise CreditsSeedAdapterError("invocation id is not a reviewed Credits approval Seed route") _selection(args.selection_json) seed_file = _seed_file(source_root, args.data_source) seed = credits_seed.load_seed(seed_file) if _seed_counts(seed) != PLANNED_COUNTS: raise CreditsSeedAdapterError("Credits approval Seed counts do not match the contract") output_dir = Path(args.output_dir).expanduser().resolve() output_dir.mkdir(parents=True, exist_ok=False) applied_counts = {name: 0 for name in PLANNED_COUNTS} verification_status = "planned" if not args.dry_run: applied_counts = credits_seed.apply_seed(seed) deploy_offline_env.refresh_runtime_metadata_cache(dict(os.environ), reason=reason) verification_status = "skipped" summary = { "schema_version": 1, "format": SUMMARY_FORMAT, "module_id": MODULE_ID, "dry_run": args.dry_run, "apply_engine": "direct_sql", "planned_counts": dict(PLANNED_COUNTS), "applied_counts": applied_counts, "coverage_complete": applied_counts == PLANNED_COUNTS if not args.dry_run else True, "verification_status": verification_status, "duration_ms": round((time.perf_counter() - started) * 1000), } summary_path = output_dir / SUMMARY_FILENAME _write_summary(summary_path, summary) print(json.dumps(summary, ensure_ascii=False, indent=2, sort_keys=True)) return summary, summary_path def main(argv: list[str] | None = None) -> int: args = parse_args(argv) try: run_adapter(args) except (OSError, RuntimeError, deploy_offline_env.DeployError) as exc: print(f"Credits approval Seed adapter failed: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": raise SystemExit(main())