"""Fin-hub operator CLI.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from fin_hub.coupling.canon import emit_viability_alert from fin_hub.coupling.dev_hub import emit_resource_pressure from fin_hub.coupling.ops_hub import ServiceCostLine, build_service_cost_report from fin_hub.ingest.anthropic import parse_anthropic_billing_csv from fin_hub.ingest.cloud import parse_cloud_cost_csv from fin_hub.ingest.hosteurope import parse_hosteurope_csv from fin_hub.services.alerts import evaluate_budget_alerts from fin_hub.services.runway import compute_runway def _cmd_import_cloud(args: argparse.Namespace) -> int: rows = parse_cloud_cost_csv(Path(args.path)) print(json.dumps([row.__dict__ for row in rows], indent=2, default=str)) return 0 def _cmd_import_anthropic(args: argparse.Namespace) -> int: rows = parse_anthropic_billing_csv(Path(args.path)) payload = [ { **row.__dict__, "recorded_at": row.recorded_at.isoformat(), } for row in rows ] print(json.dumps(payload, indent=2)) return 0 def _cmd_import_hosteurope(args: argparse.Namespace) -> int: rows = parse_hosteurope_csv(Path(args.path)) print(json.dumps([row.__dict__ for row in rows], indent=2, default=str)) return 0 def _cmd_runway(args: argparse.Namespace) -> int: burns = [float(v) for v in args.monthly_burn.split(",") if v.strip()] runway = compute_runway( current_balance=args.balance, monthly_burns=burns, alert_threshold_months=args.threshold, currency=args.currency, ) alerts = evaluate_budget_alerts( runway=runway, allocated=args.allocated, spent=args.spent, ) report = { "runway": runway.as_dict(), "alerts": [alert.as_dict() for alert in alerts], } if args.emit: report["dev_hub"] = emit_resource_pressure(alerts, api_base=args.api_base) report["canon"] = emit_viability_alert(runway, api_base=args.api_base) print(json.dumps(report, indent=2, default=str)) return 0 def _cmd_ops_costs(args: argparse.Namespace) -> int: lines = [ ServiceCostLine( service_id=row.service_id, environment=row.environment, period_month=row.period_month, amount=row.amount, currency=row.currency, source=row.source, ) for row in parse_hosteurope_csv(Path(args.path)) ] print(json.dumps(build_service_cost_report(lines), indent=2)) return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Fin Hub operator CLI") sub = parser.add_subparsers(dest="command", required=True) cloud = sub.add_parser("import-cloud", help="Parse generic cloud cost CSV") cloud.add_argument("path") cloud.set_defaults(func=_cmd_import_cloud) anthropic = sub.add_parser("import-anthropic", help="Parse Anthropic billing CSV") anthropic.add_argument("path") anthropic.set_defaults(func=_cmd_import_anthropic) hosteurope = sub.add_parser("import-hosteurope", help="Parse HostEurope invoice CSV") hosteurope.add_argument("path") hosteurope.set_defaults(func=_cmd_import_hosteurope) runway = sub.add_parser("runway", help="Compute runway and optional alerts") runway.add_argument("--balance", type=float, required=True) runway.add_argument("--monthly-burn", required=True, help="Comma-separated monthly burn values") runway.add_argument("--threshold", type=float, default=3.0) runway.add_argument("--currency", default="EUR") runway.add_argument("--allocated", type=float) runway.add_argument("--spent", type=float) runway.add_argument("--emit", action="store_true", help="Emit fin→dev and fin→canon signals") runway.add_argument("--api-base") runway.set_defaults(func=_cmd_runway) ops = sub.add_parser("ops-costs", help="Build per-service cost attribution report") ops.add_argument("path") ops.set_defaults(func=_cmd_ops_costs) return parser def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) return args.func(args) if __name__ == "__main__": sys.exit(main())