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:
tegwick 2026-07-08 00:59:39 +02:00
parent b6993d4a05
commit 1abbbe85e4
27 changed files with 835 additions and 3 deletions

View file

@ -0,0 +1,52 @@
"""HostEurope invoice CSV ingestion."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from fin_hub.ingest._csv import parse_amount, pick, read_csv_rows
@dataclass(frozen=True)
class HostEuropeCostRow:
service_id: str
title: str
amount: float
currency: str
period_month: str
incurred_on: date | None
environment: str = "production"
source: str = "hosteurope"
def parse_hosteurope_csv(path: Path, *, default_currency: str = "EUR") -> list[HostEuropeCostRow]:
rows: list[HostEuropeCostRow] = []
for row in read_csv_rows(path):
title = pick(row, "product", "description", "service", "title")
amount_raw = pick(row, "amount", "net", "total", "price")
if not title or not amount_raw:
continue
service_id = pick(row, "service_id", "product_id") or title.lower().replace(" ", "-")[:128]
period = pick(row, "period_month", "month", "billing_period")
incurred_raw = pick(row, "date", "invoice_date", "incurred_on")
incurred_on = date.fromisoformat(incurred_raw[:10]) if incurred_raw else None
if not period and incurred_on:
period = incurred_on.strftime("%Y-%m")
if not period:
continue
currency = pick(row, "currency") or default_currency
environment = pick(row, "environment", "env") or "production"
rows.append(
HostEuropeCostRow(
service_id=service_id,
title=title,
amount=parse_amount(amount_raw),
currency=currency.upper()[:3],
period_month=period[:7],
incurred_on=incurred_on,
environment=environment,
)
)
return rows