Bootstrap fin-hub scaffold from hub-core (CUST-WP-0025-T22/T23)
Add hub-core editable dependency, fin-specific SQLAlchemy models, and smoke tests.
This commit is contained in:
commit
b6993d4a05
10 changed files with 1737 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.env
|
||||
8
INTENT.md
Normal file
8
INTENT.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Fin Hub Intent
|
||||
|
||||
Provide the **Financial Allocator** surface for the FOS federation: make
|
||||
resource pressure, burn rate, and runway visible so dev-hub can deprioritize
|
||||
work and canon can receive viability alerts when thresholds are breached.
|
||||
|
||||
Fin-hub does not execute financial transactions. It tracks, projects, and
|
||||
signals.
|
||||
28
README.md
Normal file
28
README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Fin Hub
|
||||
|
||||
Resource viability hub for the FOS federation — budgets, commitments, burn rate,
|
||||
runway projection, and token spend tracking.
|
||||
|
||||
Fin-hub extends `hub-core` with financial models and read surfaces. Generic hub
|
||||
primitives (domains, repos, messages, progress events) come from hub-core;
|
||||
fin-specific models (budget, commitment, burn rate, runway, token spend) live
|
||||
here.
|
||||
|
||||
## Status
|
||||
|
||||
Bootstrap scaffold (`CUST-WP-0025-T22`). Models and ingestion pipelines are
|
||||
tracked in `CUST-WP-0025-T23`–`T24`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd /home/worsch/fin-hub
|
||||
uv sync
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Related Workplans
|
||||
|
||||
- `the-custodian/workplans/CUST-WP-0025-fos-hub-bootstrap.md` — umbrella
|
||||
- `canon/constitution/bootstrap-protocol_v0.1.md` — funding and roles
|
||||
- `canon/projects/railiance/business-model-canvas_v0.1.md` — monetization path
|
||||
15
SCOPE.md
Normal file
15
SCOPE.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Fin Hub Scope
|
||||
|
||||
## In Scope
|
||||
|
||||
- Budget, commitment, burn rate, runway projection, and token spend models
|
||||
- Manual CSV import for cloud and API costs (v0.1)
|
||||
- Runway calculator with alert thresholds
|
||||
- FOS §9 cross-hub signals: fin→dev (budget pressure), fin→ops (cost attribution), fin→canon (viability alerts)
|
||||
- hub-core generic primitives (domains, repos, messages, progress)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Payment execution, invoicing, or banking integration
|
||||
- Tax filing or legal entity management
|
||||
- Multi-tenant customer billing (deferred to RaaS T26)
|
||||
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[project]
|
||||
name = "fin-hub"
|
||||
version = "0.1.0"
|
||||
description = "Financial viability hub for the FOS federation"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"hub-core",
|
||||
"fastapi>=0.115.0",
|
||||
"sqlalchemy[asyncio]>=2.0.0",
|
||||
"pydantic>=2.10.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
hub-core = { path = "../hub-core", editable = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["fin_hub"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
]
|
||||
3
src/fin_hub/__init__.py
Normal file
3
src/fin_hub/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""Fin Hub — resource viability models and surfaces for the FOS federation."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
11
src/fin_hub/models/__init__.py
Normal file
11
src/fin_hub/models/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""Fin-specific SQLAlchemy models (T23 implementation target)."""
|
||||
|
||||
from fin_hub.models.budget import Budget, BurnRate, Commitment, RunwayProjection, TokenSpend
|
||||
|
||||
__all__ = [
|
||||
"Budget",
|
||||
"Commitment",
|
||||
"BurnRate",
|
||||
"RunwayProjection",
|
||||
"TokenSpend",
|
||||
]
|
||||
77
src/fin_hub/models/budget.py
Normal file
77
src/fin_hub/models/budget.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""Financial viability models — initial schema for CUST-WP-0025-T23."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import Date, DateTime, Float, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from hub_core.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Budget(Base, TimestampMixin):
|
||||
__tablename__ = "fin_budgets"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
period_start: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
period_end: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
allocated: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
committed: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
spent: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
|
||||
class Commitment(Base, TimestampMixin):
|
||||
__tablename__ = "fin_commitments"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
commitment_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
amount: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
cadence: Mapped[str] = mapped_column(String(16), nullable=False, default="monthly")
|
||||
start_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
|
||||
|
||||
class BurnRate(Base, TimestampMixin):
|
||||
__tablename__ = "fin_burn_rates"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
period_month: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
|
||||
actual_spend: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
projected_spend: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
|
||||
|
||||
class RunwayProjection(Base, TimestampMixin):
|
||||
__tablename__ = "fin_runway_projections"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
current_balance: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
monthly_burn: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
months_remaining: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
alert_threshold_months: Mapped[float] = mapped_column(Float, nullable=False, default=3.0)
|
||||
computed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
|
||||
|
||||
class TokenSpend(Base, TimestampMixin):
|
||||
__tablename__ = "fin_token_spends"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
provider: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
||||
model: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
cost: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
||||
session_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
|
||||
21
tests/test_models.py
Normal file
21
tests/test_models.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Smoke tests for fin-hub model registration."""
|
||||
|
||||
from fin_hub.models import Budget, BurnRate, Commitment, RunwayProjection, TokenSpend
|
||||
from hub_core.models.base import Base
|
||||
|
||||
|
||||
def test_fin_models_register_on_metadata():
|
||||
tables = {table.name for table in Base.metadata.sorted_tables}
|
||||
assert "fin_budgets" in tables
|
||||
assert "fin_commitments" in tables
|
||||
assert "fin_burn_rates" in tables
|
||||
assert "fin_runway_projections" in tables
|
||||
assert "fin_token_spends" in tables
|
||||
|
||||
|
||||
def test_model_classes_importable():
|
||||
assert Budget.__tablename__ == "fin_budgets"
|
||||
assert Commitment.__tablename__ == "fin_commitments"
|
||||
assert BurnRate.__tablename__ == "fin_burn_rates"
|
||||
assert RunwayProjection.__tablename__ == "fin_runway_projections"
|
||||
assert TokenSpend.__tablename__ == "fin_token_spends"
|
||||
Loading…
Add table
Add a link
Reference in a new issue