Module adcp.decisioning.state
Sync workflow-state reader for :class:RequestContext.
Defines:
- :class:
StateReader— Protocol for sync reads of framework-owned in-flight workflow state. Platform methods read this without re-querying their own DB; the framework owns the cache. - :class:
WorkflowStep, :class:WorkflowObjectType, :data:GovernanceContextJWS— framework-internal types referenced by :class:StateReadermethods. Defined here (not inadcp.types.generated_poc/) because they're framework-only — not on the wire. - :class:
_NotYetWiredStateReader— v6.0 stub. Returns type-correct empty values; emits a one-time :class:UserWarningper method on first call so adopters notice they're reading uninitialized state. Backing store lands in v6.1.
The asymmetry between this stub (returns empty) and
:class:adcp.decisioning.resolve._NotYetWiredResolver (raises) is
deliberate. state.* reads are read-only inspections of
framework-owned state — an empty workflow-steps list IS the correct
answer for a fresh tenant. resolve.* fetches are validated
lookups — an empty :class:PropertyList in v6.0 vs. a real one in
v6.1 is divergence the framework cannot silently paper over. See
docs/proposals/decisioning-platform-dispatch-design.md#d15 for
the full rationale.
Global variables
var GovernanceContextJWS-
JWS-signed governance context. The framework verifies signature, plan-binding, seller-binding, and phase-binding before exposing the token to platform code; adopters can trust the value. Don't unwrap or modify — re-pass to downstream framework calls instead.
var WorkflowObjectType-
Object types a workflow step can touch. Framework-internal — not on the wire (the wire-side
status-change-resource-type.jsonenum covers a different surface).
Classes
class Proposal (**data: Any)-
Expand source code
class Proposal(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) proposal_id: Annotated[ str, Field( description='Unique identifier for this proposal. Used to finalize a draft proposal and to execute a committed proposal via create_media_buy.', max_length=255, ), ] name: Annotated[ str, Field(description='Human-readable name for this media plan proposal', max_length=500) ] description: Annotated[ str | None, Field( description='Explanation of the proposal strategy and what it achieves', max_length=2000 ), ] = None allocations: Annotated[ list[product_allocation.ProductAllocation], Field( description='Budget allocations across products. Allocation percentages MUST sum to 100. Publishers are responsible for ensuring the sum equals 100; buyers SHOULD validate this before execution.', min_length=1, ), ] proposal_status: Annotated[ proposal_status_1.ProposalStatus | None, Field( description="Lifecycle status of this proposal and the per-proposal source of truth for whether finalization is required before create_media_buy. When absent, the proposal is ready to buy (backward compatible). 'draft' means indicative pricing — finalize via refine before purchasing. 'committed' means firm pricing with inventory reserved until expires_at and executable via create_media_buy." ), ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='When this proposal expires and can no longer be executed. For draft proposals, indicates when indicative pricing becomes stale. For committed proposals, indicates when the inventory hold lapses — the buyer must call create_media_buy before this time.' ), ] = None insertion_order: Annotated[ insertion_order_1.InsertionOrder | None, Field( description='Formal insertion order attached to a committed proposal. Present when the seller requires a signed agreement before the media buy can proceed. The buyer references the io_id in io_acceptance on create_media_buy.' ), ] = None total_budget_guidance: Annotated[ TotalBudgetGuidance | None, Field(description='Optional budget guidance for this proposal') ] = None brief_alignment: Annotated[ str | None, Field( description='Explanation of how this proposal aligns with the campaign brief', max_length=2000, ), ] = None forecast: Annotated[ delivery_forecast.DeliveryForecast | None, Field( description='Aggregate forecasted delivery metrics for the entire proposal. When both proposal-level and allocation-level forecasts are present, the proposal-level forecast is authoritative for total delivery estimation.' ), ] = None ext: ext_1.ExtensionObject | None = NoneBase model for AdCP types with spec-compliant serialization.
Defaults to
extra='ignore'so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas setadditionalProperties: trueoverride this withextra='allow'in their ownmodel_config.Set
ADCP_STRICT_VALIDATION=1in the environment ("1","true","yes","on"are accepted) to flip the default toextra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.Important
The env var is resolved once at module import time. Set it in your shell or CI environment before
import adcpruns — mutatingos.environ["ADCP_STRICT_VALIDATION"]after the firstadcpimport has no effect on already-imported model classes (they captured the policy at class-body evaluation).Consumers who want per-model strict validation can override
model_configon their subclass.Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.Ancestors
- AdCPBaseModel
- pydantic.main.BaseModel
Class variables
var allocations : list[adcp.types.generated_poc.core.product_allocation.ProductAllocation]var brief_alignment : str | Nonevar description : str | Nonevar expires_at : pydantic.types.AwareDatetime | Nonevar ext : adcp.types.generated_poc.core.ext.ExtensionObject | Nonevar forecast : adcp.types.generated_poc.core.delivery_forecast.DeliveryForecast | Nonevar insertion_order : adcp.types.generated_poc.core.insertion_order.InsertionOrder | Nonevar model_configvar name : strvar proposal_id : strvar proposal_status : adcp.types.generated_poc.enums.proposal_status.ProposalStatus | Nonevar total_budget_guidance : adcp.types.generated_poc.core.proposal.TotalBudgetGuidance | None
Inherited members
class StateReader (*args, **kwargs)-
Expand source code
@runtime_checkable class StateReader(Protocol): """Sync reads of framework-owned in-flight workflow state. Platform methods read prior workflow context (recent media-buy transitions, related proposals, in-flight governance bindings) without re-querying their own DB. The framework owns the cache; the Protocol surface is purely read. Framework-supplied; never constructed by adopter code. The ``RequestContext.state`` field is populated by the dispatch hydration helper. Adopters substituting test doubles use :func:`dataclasses.replace` on the context, not direct construction. Mirrors the TS-side ``WorkflowStateReader`` interface in ``src/lib/server/decisioning/context.ts``. v6.0 ships the contract + the no-op stub; v6.1 lands the backing store. .. note:: :class:`runtime_checkable` Protocols match by attribute *name* only — return types (including :data:`GovernanceContextJWS`, which is a :func:`typing.NewType` invisible at runtime) and method signatures are NOT enforced by ``isinstance``. A custom impl that returns ``int`` from ``governance_context()`` will pass the structural check; mypy is the only enforcement for return-type contracts. Coverage gap is acceptable for v6.0. """ def find_by_object( self, object_type: WorkflowObjectType, object_id: str, ) -> Sequence[WorkflowStep]: """Return workflow steps that touched the given object, chronological. Used for "what's happened to this buy?" reads without a platform-side fetch.""" ... def find_proposal_by_id(self, proposal_id: str) -> Proposal | None: """Resolve a ``proposal_id`` threaded across ``get_products → refine → create_media_buy`` without platform code. Returns ``None`` if the framework doesn't recognize the id.""" ... def governance_context(self) -> GovernanceContextJWS | None: """Currently in-flight verified governance context (the JWS token). ``None`` for non-governance flows. Framework verifies before exposure; platform code can trust the value. Adopters claiming ``governance-*`` specialisms in ``capabilities.specialisms`` MUST set ``capabilities.governance_aware=True`` and wire a real ``StateReader`` that returns real JWS tokens. The default stub returns ``None``, which would silently skip the gate — server boot fails fast if a governance specialism is claimed without the opt-in. See ``docs/proposals/decisioning-platform-dispatch-design.md#d15``. """ ... def workflow_steps(self) -> Sequence[WorkflowStep]: """All chronological steps for this request's account. Audit-read shape.""" ...Sync reads of framework-owned in-flight workflow state.
Platform methods read prior workflow context (recent media-buy transitions, related proposals, in-flight governance bindings) without re-querying their own DB. The framework owns the cache; the Protocol surface is purely read.
Framework-supplied; never constructed by adopter code. The
RequestContext.statefield is populated by the dispatch hydration helper. Adopters substituting test doubles use :func:dataclasses.replaceon the context, not direct construction.Mirrors the TS-side
WorkflowStateReaderinterface insrc/lib/server/decisioning/context.ts. v6.0 ships the contract + the no-op stub; v6.1 lands the backing store.Note
:class:
runtime_checkableProtocols match by attribute name only — return types (including :data:GovernanceContextJWS, which is a :func:typing.NewTypeinvisible at runtime) and method signatures are NOT enforced byisinstance. A custom impl that returnsintfromgovernance_context()will pass the structural check; mypy is the only enforcement for return-type contracts. Coverage gap is acceptable for v6.0.Ancestors
- typing.Protocol
- typing.Generic
Methods
def find_by_object(self,
object_type: WorkflowObjectType,
object_id: str) ‑> Sequence[WorkflowStep]-
Expand source code
def find_by_object( self, object_type: WorkflowObjectType, object_id: str, ) -> Sequence[WorkflowStep]: """Return workflow steps that touched the given object, chronological. Used for "what's happened to this buy?" reads without a platform-side fetch.""" ...Return workflow steps that touched the given object, chronological. Used for "what's happened to this buy?" reads without a platform-side fetch.
def find_proposal_by_id(self, proposal_id: str) ‑> adcp.types.generated_poc.core.proposal.Proposal | None-
Expand source code
def find_proposal_by_id(self, proposal_id: str) -> Proposal | None: """Resolve a ``proposal_id`` threaded across ``get_products → refine → create_media_buy`` without platform code. Returns ``None`` if the framework doesn't recognize the id.""" ...Resolve a
proposal_idthreaded acrossget_products → refine → create_media_buywithout platform code. ReturnsNoneif the framework doesn't recognize the id. def governance_context(self) ‑> GovernanceContextJWS | None-
Expand source code
def governance_context(self) -> GovernanceContextJWS | None: """Currently in-flight verified governance context (the JWS token). ``None`` for non-governance flows. Framework verifies before exposure; platform code can trust the value. Adopters claiming ``governance-*`` specialisms in ``capabilities.specialisms`` MUST set ``capabilities.governance_aware=True`` and wire a real ``StateReader`` that returns real JWS tokens. The default stub returns ``None``, which would silently skip the gate — server boot fails fast if a governance specialism is claimed without the opt-in. See ``docs/proposals/decisioning-platform-dispatch-design.md#d15``. """ ...Currently in-flight verified governance context (the JWS token).
Nonefor non-governance flows. Framework verifies before exposure; platform code can trust the value.Adopters claiming
governance-*specialisms incapabilities.specialismsMUST setcapabilities.governance_aware=Trueand wire a realStateReaderthat returns real JWS tokens. The default stub returnsNone, which would silently skip the gate — server boot fails fast if a governance specialism is claimed without the opt-in. Seedocs/proposals/decisioning-platform-dispatch-design.md#d15. def workflow_steps(self) ‑> Sequence[WorkflowStep]-
Expand source code
def workflow_steps(self) -> Sequence[WorkflowStep]: """All chronological steps for this request's account. Audit-read shape.""" ...All chronological steps for this request's account. Audit-read shape.
class WorkflowStep (id: str,
object_type: WorkflowObjectType,
object_id: str,
tool: str,
at: str,
actor: dict[str, str],
status: "Literal['submitted', 'completed', 'failed', 'progress']")-
Expand source code
@dataclass(frozen=True) class WorkflowStep: """A chronological event the framework recorded against an object. Frozen because the framework writes the step record once at the transition; platform code reads but does not mutate. The shape mirrors the TS-side ``WorkflowStep`` interface so cross-language adopters get the same fields. :param id: Stable step identifier (framework-allocated UUID). :param object_type: The object this step touched. :param object_id: Stable id of the touched object within :attr:`object_type`. :param tool: Wire verb that ran the step (e.g. ``'create_media_buy'``, ``'sync_creatives'``). :param at: ISO 8601 timestamp of the step. :param actor: Who initiated the step. ``agent_url`` for an agent principal, ``principal`` for a service-account principal, possibly both. :param status: Step outcome. ``'submitted'`` for a kicked-off task, ``'completed'``/``'failed'`` for terminal states, ``'progress'`` for a mid-flight update. """ id: str object_type: WorkflowObjectType object_id: str tool: str at: str actor: dict[str, str] status: Literal["submitted", "completed", "failed", "progress"]A chronological event the framework recorded against an object.
Frozen because the framework writes the step record once at the transition; platform code reads but does not mutate. The shape mirrors the TS-side
WorkflowStepinterface so cross-language adopters get the same fields.:param id: Stable step identifier (framework-allocated UUID). :param object_type: The object this step touched. :param object_id: Stable id of the touched object within :attr:
object_type. :param tool: Wire verb that ran the step (e.g.'create_media_buy','sync_creatives'). :param at: ISO 8601 timestamp of the step. :param actor: Who initiated the step.agent_urlfor an agent principal,principalfor a service-account principal, possibly both. :param status: Step outcome.'submitted'for a kicked-off task,'completed'/'failed'for terminal states,'progress'for a mid-flight update.Instance variables
var actor : dict[str, str]var at : strvar id : strvar object_id : strvar object_type : Literal['media_buy', 'creative', 'product', 'plan', 'audience', 'rights_grant', 'task']var status : Literal['submitted', 'completed', 'failed', 'progress']var tool : str