Implement fin-hub T24-T26: ingest, runway, coupling, RaaS packaging
Add CSV importers, runway calculator, budget alerts, cross-hub coupling emitters, finhub CLI, and raas-mvp-packaging docs with full test coverage.
This commit is contained in:
parent
b6993d4a05
commit
1abbbe85e4
27 changed files with 835 additions and 3 deletions
126
src/fin_hub/cli.py
Normal file
126
src/fin_hub/cli.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue