feat: ingest State Hub session tokens without booking them
Store mixed measured/estimated/superseded aggregates as session-token evidence. Map kinds onto the evidence-basis vocabulary, keep unknown months unknown, and refuse to post the snapshot as booked spend or resource-control usage.
This commit is contained in:
parent
22b21be552
commit
d343888a41
10 changed files with 578 additions and 1 deletions
87
src/fin_hub/schemas/session_tokens.py
Normal file
87
src/fin_hub/schemas/session_tokens.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Session-token evidence ingested from State Hub aggregates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
MEASUREMENT_KINDS = ("measured", "allocated", "estimated", "superseded")
|
||||
EVIDENCE_BASIS = {
|
||||
"measured": "measured",
|
||||
"allocated": "derived",
|
||||
"estimated": "estimated",
|
||||
"superseded": "excluded",
|
||||
}
|
||||
|
||||
|
||||
class SessionTokenSlice(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
tokens_in: int = Field(ge=0)
|
||||
tokens_out: int = Field(ge=0)
|
||||
event_count: int = Field(ge=0)
|
||||
confidence: Decimal | None = None
|
||||
method: str | None = None
|
||||
|
||||
@field_validator("confidence")
|
||||
@classmethod
|
||||
def validate_confidence(cls, value: Decimal | None) -> Decimal | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not value.is_finite() or value < 0 or value > 1:
|
||||
raise ValueError("confidence must be between 0 and 1")
|
||||
return value
|
||||
|
||||
|
||||
class SessionTokenAssociation(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
scope: Literal["repo", "workplan", "model"]
|
||||
scope_id: str = Field(min_length=1, max_length=256)
|
||||
label: str | None = None
|
||||
measurement_kind: Literal["measured", "allocated", "estimated", "superseded"]
|
||||
tokens_in: int = Field(ge=0)
|
||||
tokens_out: int = Field(ge=0)
|
||||
event_count: int = Field(ge=0, default=1)
|
||||
|
||||
|
||||
class SessionTokenEvidence(BaseModel):
|
||||
"""State Hub session-token snapshot. Not booked spend and not usage_observation."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
schema_version: Literal["0.1"] = "0.1"
|
||||
record_type: Literal["session_token_evidence"] = "session_token_evidence"
|
||||
record_id: str = Field(min_length=1, max_length=256)
|
||||
revision_of: str | None = None
|
||||
source_system: Literal["state-hub"] = "state-hub"
|
||||
period_start: date
|
||||
period_end: date
|
||||
captured_at: datetime
|
||||
source_evidence: list[str] = Field(min_length=1)
|
||||
totals: dict[str, SessionTokenSlice] = Field(default_factory=dict)
|
||||
associations: list[SessionTokenAssociation] = Field(default_factory=list)
|
||||
|
||||
@field_validator("totals")
|
||||
@classmethod
|
||||
def validate_totals(cls, value: dict[str, SessionTokenSlice]) -> dict[str, SessionTokenSlice]:
|
||||
unknown = set(value) - set(MEASUREMENT_KINDS)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown measurement_kind: {', '.join(sorted(unknown))}")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_period_and_methods(self) -> "SessionTokenEvidence":
|
||||
if self.period_end < self.period_start:
|
||||
raise ValueError("period_end cannot precede period_start")
|
||||
allocated = self.totals.get("allocated")
|
||||
if (
|
||||
allocated is not None
|
||||
and allocated.event_count > 0
|
||||
and not (allocated.method and allocated.method.strip())
|
||||
):
|
||||
raise ValueError("allocated totals with events require method")
|
||||
return self
|
||||
Loading…
Add table
Add a link
Reference in a new issue