126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
|
|
"""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())
|