Skip to content

symfonic.services.cost.ports

ports

Port-registry row 5, finally expressed as a protocol.

BudgetCheck is the narrow admission read; BudgetStore (persistence) stays exactly where it is, in symfonic.core.observability. BUD-5 is the dividing line: limits, ledgers, upserts and degraded-mode reads are the runtime service's, and the platform owns only the admission decision and the read models.

Both protocols take a :class:SubjectScope rather than a tenant string. The platform used to hand the budget gate a raw X-Tenant-ID header value so the check could run before scope construction; BUD-3 retires that, and a port whose signature cannot express "an unauthenticated string" is how the retirement stays retired.

BudgetCheck

Bases: Protocol

The admission read. One method, because it answers one question.

check async

check(scope: SubjectScope, *, is_admin: bool = False) -> BudgetDecision

May this scope incur another billable call?

is_admin is a policy input, not a caller-side branch (BUD-4): the port decides whether an administrator bypasses, and the transport never skips the call.

Source code in src/symfonic/services/cost/ports.py
async def check(
    self, scope: SubjectScope, *, is_admin: bool = False
) -> BudgetDecision:
    """May this scope incur another billable call?

    ``is_admin`` is a policy *input*, not a caller-side branch (BUD-4): the
    port decides whether an administrator bypasses, and the transport never
    skips the call.
    """
    ...

CostReadModel

Bases: Protocol

The projection /metrics/* renders (BUD-6).

summarize async

summarize(scope: SubjectScope, *, window: str) -> Any

Aggregated spend for a scope and window. Tenant-scoped, always.

SEC-TEN-2 makes isolation unconditional — never gated on a collector being enabled — so an implementation that cannot scope a summary must refuse it rather than widen it.

Source code in src/symfonic/services/cost/ports.py
async def summarize(self, scope: SubjectScope, *, window: str) -> Any:
    """Aggregated spend for a scope and window. Tenant-scoped, always.

    SEC-TEN-2 makes isolation unconditional — never gated on a collector
    being enabled — so an implementation that cannot scope a summary must
    refuse it rather than widen it.
    """
    ...