#!/usr/bin/env python3 """Apply the Catalog-owned Credits permission 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_permissions_seed as permissions_seed # noqa: E402 import deploy_offline_env # noqa: E402 MODULE_ID = "credits_permissions_seed" SUMMARY_FILENAME = "summary.json" SUMMARY_FORMAT = "deploy-seed-script-v1" PLANNED_COUNTS = { "permission_sets": 5, "object_permissions": 5, "field_permissions": 29, } INVOCATION_REASONS = { "test.credits_permissions.governed_reference_data": "test credits permission seed", "watchdog.credits_permissions.post_quote_approval": "watchdog credits permission seed", "exp.credits_permissions.post_quote_approval": "exp credits permission seed", "staging.credits_permissions.post_quote_approval": "staging credits permission seed", } class CreditsPermissionsSeedAdapterError(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 permission 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 CreditsPermissionsSeedAdapterError("selection must be valid JSON") from exc if selection != {}: raise CreditsPermissionsSeedAdapterError( "Credits permission 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 CreditsPermissionsSeedAdapterError( "Credits permission Seed requires one data source" ) expected = ( source_root / "deployments" / "offline" / "runtime" / "seeds" / "credits-permissions.seed.json" ).resolve(strict=True) actual = Path(data_sources[0]).expanduser().resolve(strict=True) if actual != expected: raise CreditsPermissionsSeedAdapterError( "data source is not the reviewed Credits permission Seed" ) return actual def _seed_counts(seed: dict[str, Any]) -> dict[str, int]: return { "permission_sets": len(seed.get("permissionSets") or []), "object_permissions": len(seed.get("objectPermissions") or []), "field_permissions": len(seed.get("fieldPermissions") 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 CreditsPermissionsSeedAdapterError( "module id is not the Credits permission Seed module" ) reason = INVOCATION_REASONS.get(args.invocation_id) if reason is None: raise CreditsPermissionsSeedAdapterError( "invocation id is not a reviewed Credits permission Seed route" ) _selection(args.selection_json) seed_file = _seed_file(source_root, args.data_source) # load_seed also runs validate_seed: parent-controlled ObjectPermissions, # ModifyAll cascade, and the never-PermissionSetAssignment boundary are all # enforced before anything is written. seed = permissions_seed.load_seed(seed_file) if _seed_counts(seed) != PLANNED_COUNTS: raise CreditsPermissionsSeedAdapterError( "Credits permission 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 = permissions_seed.apply_seed(seed) if args.invocation_id == "test.credits_permissions.governed_reference_data": # Full D3 applies governed L2 data before it starts the selected # serving tuple. The new Runtime therefore observes these rows on # first load; there is no live process whose cache can or should be # refreshed at this transition boundary. verification_status = "skipped" else: 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 permission Seed adapter failed: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": raise SystemExit(main())