symfonic.platform.budget¶
budget ¶
BUD-1..7 — the admission decision, and nothing else.
Three shipped defects retire here.
- The string-prefix branch.
str(exc).startswith("Budget exceeded:")appears in/chat,/streamand/stream/typed. Reword the message and a 429 becomes a 500. :class:BudgetExceededErrorcarries the decision in its class and itscode. - The raw header read. The shipped dependency reads
X-Tenant-IDdirectly so it can run before scope construction. That let an unauthenticated caller probe another tenant's budget state; BUD-3 retires it, and this service takes an already-derived principal so the shortcut cannot be spelled. - The silent no-op. With no tracker bound the gate does nothing — correct,
and preserved — but says nothing either. :meth:
BudgetService.describemakes "budgets are off" an observable deployment property (BUD-7).
BudgetService ¶
BudgetService(*, budget_check: BudgetCheck | None, cost_read_model: CostReadModel | None = None, audit: AuditSeam | None = None)
Admission from the narrow port, plus the cost read models.
Source code in src/symfonic/platform/budget.py
admit
async
¶
The decision, without raising. enforce is the gate.
Split because BUD-1 has two consumers: the platform admits a request,
and the invocation re-checks before its own effects. Both read the same
:class:BudgetDecision; only the first turns it into a refusal status.
Source code in src/symfonic/platform/budget.py
describe ¶
BUD-7: what this host does about budgets, stated rather than implied.
enforce
async
¶
SCOPE-14 step 5: fail-closed, evented, typed.
A broken ledger denies rather than admits (SEC-FCP-1). The alternative — "the accountant is down, so everything is free" — is the failure mode a budget exists to prevent.
Source code in src/symfonic/platform/budget.py
summarize
async
¶
BUD-6: a projection, scoped to the caller like every other route.
Returns None when no read model is bound — an absent projection, not
an empty one. Reporting zeros for a ledger nobody is keeping is how a
cost dashboard learns to lie.