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
3
tests/fixtures/anthropic-billing.csv
vendored
Normal file
3
tests/fixtures/anthropic-billing.csv
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
model,input_tokens,output_tokens,cost,currency,usage_date
|
||||
claude-sonnet-4-20250514,12000,3000,1.25,USD,2026-06-10
|
||||
claude-haiku-3-20240307,50000,2000,0.40,USD,2026-06-11
|
||||
|
4
tests/fixtures/cloud-costs.csv
vendored
Normal file
4
tests/fixtures/cloud-costs.csv
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
service,amount,currency,date
|
||||
state-hub,42.50,EUR,2026-06-01
|
||||
core-hub,18.00,EUR,2026-06-15
|
||||
gitea,25.00,EUR,2026-06-20
|
||||
|
3
tests/fixtures/hosteurope.csv
vendored
Normal file
3
tests/fixtures/hosteurope.csv
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
product,amount,currency,invoice_date,environment
|
||||
Dedicated Server M,89.90,EUR,2026-06-01,production
|
||||
Backup Storage,9.90,EUR,2026-06-01,production
|
||||
|
24
tests/test_coupling.py
Normal file
24
tests/test_coupling.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import pytest
|
||||
|
||||
from fin_hub.coupling.ops_hub import ServiceCostLine, build_service_cost_report
|
||||
from fin_hub.ingest.hosteurope import parse_hosteurope_csv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_build_service_cost_report_groups_by_service():
|
||||
rows = parse_hosteurope_csv(Path(__file__).parent / "fixtures" / "hosteurope.csv")
|
||||
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 rows
|
||||
]
|
||||
report = build_service_cost_report(lines)
|
||||
assert report["signal"] == "service_cost_attribution"
|
||||
assert len(report["services"]) == 2
|
||||
assert report["totals_by_month"]["2026-06"] == pytest.approx(99.8)
|
||||
28
tests/test_ingest.py
Normal file
28
tests/test_ingest.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
def test_parse_cloud_cost_csv():
|
||||
rows = parse_cloud_cost_csv(FIXTURES / "cloud-costs.csv")
|
||||
assert len(rows) == 3
|
||||
assert rows[0].service == "state-hub"
|
||||
assert rows[0].amount == 42.5
|
||||
|
||||
|
||||
def test_parse_anthropic_billing_csv():
|
||||
rows = parse_anthropic_billing_csv(FIXTURES / "anthropic-billing.csv")
|
||||
assert len(rows) == 2
|
||||
assert rows[0].provider == "anthropic"
|
||||
assert rows[0].tokens_in == 12000
|
||||
|
||||
|
||||
def test_parse_hosteurope_csv():
|
||||
rows = parse_hosteurope_csv(FIXTURES / "hosteurope.csv")
|
||||
assert len(rows) == 2
|
||||
assert rows[0].service_id == "dedicated-server-m"
|
||||
assert rows[0].period_month == "2026-06"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"""Smoke tests for fin-hub model registration."""
|
||||
|
||||
from fin_hub.models import Budget, BurnRate, Commitment, RunwayProjection, TokenSpend
|
||||
from fin_hub.models import Budget, BurnRate, Commitment, RunwayProjection, ServiceCost, TokenSpend
|
||||
from hub_core.models.base import Base
|
||||
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ def test_fin_models_register_on_metadata():
|
|||
assert "fin_burn_rates" in tables
|
||||
assert "fin_runway_projections" in tables
|
||||
assert "fin_token_spends" in tables
|
||||
assert "fin_service_costs" in tables
|
||||
|
||||
|
||||
def test_model_classes_importable():
|
||||
|
|
@ -18,4 +19,5 @@ def test_model_classes_importable():
|
|||
assert Commitment.__tablename__ == "fin_commitments"
|
||||
assert BurnRate.__tablename__ == "fin_burn_rates"
|
||||
assert RunwayProjection.__tablename__ == "fin_runway_projections"
|
||||
assert TokenSpend.__tablename__ == "fin_token_spends"
|
||||
assert TokenSpend.__tablename__ == "fin_token_spends"
|
||||
assert ServiceCost.__tablename__ == "fin_service_costs"
|
||||
16
tests/test_runway.py
Normal file
16
tests/test_runway.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from fin_hub.services.alerts import evaluate_budget_alerts
|
||||
from fin_hub.services.runway import compute_runway
|
||||
|
||||
|
||||
def test_compute_runway_below_threshold():
|
||||
runway = compute_runway(current_balance=6000, monthly_burns=[2000, 2200, 2100], alert_threshold_months=3.0)
|
||||
assert runway.months_remaining < 3.0
|
||||
assert runway.below_threshold is True
|
||||
|
||||
|
||||
def test_budget_alerts_include_runway_and_utilisation():
|
||||
runway = compute_runway(current_balance=6000, monthly_burns=[2000, 2200, 2100])
|
||||
alerts = evaluate_budget_alerts(runway=runway, allocated=10000, spent=8500)
|
||||
codes = {alert.code for alert in alerts}
|
||||
assert "runway_below_threshold" in codes
|
||||
assert "budget_pressure" in codes
|
||||
Loading…
Add table
Add a link
Reference in a new issue