Module adcp.media_buy_actions
Buyer and seller helpers for proposal-bound MediaBuy change rights.
The helpers in this module deliberately keep three protocol surfaces separate:
- product
allowed_actionsare advisory possibilities; - accepted proposal
commercial_terms.change_termsare binding rights; and - MediaBuy
available_actionsare the seller's current-state projection.
Legacy fields are readable, but never promoted into proposal authority.
In
particular, a 3.1 terms_ref is opaque unless this module itself emitted it
as the compatibility projection of a known change_term_id.
Functions
def action_update_fields(version: str | None = None) ‑> Mapping[str, tuple[str, ...]]-
Expand source code
@lru_cache(maxsize=16) def action_update_fields(version: str | None = None) -> Mapping[str, tuple[str, ...]]: """Which ``update_media_buy`` request fields each action covers. AdCP 3.2.0-rc.3 splits this normative map across **two** ``enumMetadata`` blocks and requires SDKs to merge them: the deprecated flat ``enums/media-buy-valid-action.json`` plus ``core/media-buy-available-action-id.json``, which carries the structured-only actions that were introduced after the flat surface was deprecated. Reading only the first silently omits ``update_media_buy_frequency_cap`` and its ``frequency_cap`` field, so a dispatcher built on it would route a valid rc.3 mutation nowhere. Read from the bundled schemas rather than transcribed, because the spec names this block -- not the task-reference table -- as the thing SDKs dispatch on. A hand-copied table is a second source of truth that drifts on the next release. ``version`` selects the bundle, so a client pinned to an older release dispatches on that release's map rather than on the SDK's default. Cached per version, because each is a pure function of an immutable bundle. Returns an empty mapping when the bundle predates the fields, so an older pin degrades rather than raising. """ merged: dict[str, tuple[str, ...]] = {} for name in ("enums/media-buy-valid-action.json", "core/media-buy-available-action-id.json"): block = _schema_enum_metadata(name, version=version) for action, metadata in block.items(): if action.startswith("$") or not isinstance(metadata, Mapping): continue fields = metadata.get("update_fields") if isinstance(fields, Sequence) and not isinstance(fields, str): merged[action] = tuple(str(item) for item in fields) return mergedWhich
update_media_buyrequest fields each action covers.AdCP 3.2.0-rc.3 splits this normative map across two
enumMetadatablocks and requires SDKs to merge them: the deprecated flatenums/media-buy-valid-action.jsonpluscore/media-buy-available-action-id.json, which carries the structured-only actions that were introduced after the flat surface was deprecated. Reading only the first silently omitsupdate_media_buy_frequency_capand itsfrequency_capfield, so a dispatcher built on it would route a valid rc.3 mutation nowhere.Read from the bundled schemas rather than transcribed, because the spec names this block – not the task-reference table – as the thing SDKs dispatch on. A hand-copied table is a second source of truth that drifts on the next release.
versionselects the bundle, so a client pinned to an older release dispatches on that release's map rather than on the SDK's default. Cached per version, because each is a pure function of an immutable bundle.Returns an empty mapping when the bundle predates the fields, so an older pin degrades rather than raising.
def assess_media_buy_action(action: str,
*,
product: Any | None = None,
proposal: Any | None = None,
media_buy: Any | None = None,
intent: ActionIntent | Mapping[str, Any] | None = None,
now: datetime | None = None) ‑> MediaBuyActionAssessment-
Expand source code
def assess_media_buy_action( action: str, *, product: Any | None = None, proposal: Any | None = None, media_buy: Any | None = None, intent: ActionIntent | Mapping[str, Any] | None = None, now: datetime | None = None, ) -> MediaBuyActionAssessment: """Join product, accepted proposal, and live-buy action surfaces. Inputs may be generated Pydantic models or wire mappings. Unknown future enum values and legacy coarse actions are retained as unknown evidence but are never treated as authority. """ if _ACTION_RE.fullmatch(action) is None: raise MediaBuyActionError("invalid_action", "action") diagnostics: list[ActionDiagnostic] = [] possible = _assess_product_support(action, product, diagnostics) change_terms_present, change_terms = _extract_change_terms(proposal, media_buy) term, term_diagnostics = _find_change_term(action, change_terms) diagnostics.extend(term_diagnostics) if not change_terms_present: promised = ActionKnowledge.unknown diagnostics.append(ActionDiagnostic(code=ActionDiagnosticCode.legacy_terms_unknown)) elif term is None: promised = ActionKnowledge.no else: promised = ActionKnowledge.yes status_value = _string_field(media_buy, "status") wrong_status = False if term is not None and status_value is not None: allowed_statuses = _string_sequence(term.get("allowed_statuses")) wrong_status = status_value in _TERMINAL_STATUSES or ( bool(allowed_statuses) and status_value not in allowed_statuses ) live_state, live_action = _assess_live_action(action, media_buy, diagnostics) task: ActionTask | None = None mode: str | None = None change_term_id: str | None = None constraints: tuple[ConstraintCheck, ...] = () if live_action is not None: task = _task_field(live_action) if "task" in live_action else route_media_buy_action(action) mode = _string_value(live_action.get("mode")) change_term_id = _string_value(live_action.get("change_term_id")) if term is not None and not _projection_matches_term( action, live_action, term, task, diagnostics ): live_state = ActionKnowledge.no elif term is not None: task = route_media_buy_action(action) mode = _string_value(term.get("service_mode")) change_term_id = _string_value(term.get("term_id")) if term is not None and intent is not None: parsed_intent = ( intent if isinstance(intent, ActionIntent) else ActionIntent.model_validate(intent) ) constraints = evaluate_action_constraints( action, term.get("constraints"), parsed_intent, now=now, ) if any(check.outcome is ConstraintOutcome.violated for check in constraints): diagnostics.append(ActionDiagnostic(code=ActionDiagnosticCode.constraint_violated)) live_state = ActionKnowledge.no integrity_failure = any( diagnostic.code in { ActionDiagnosticCode.alias_mismatch, ActionDiagnosticCode.duplicate_action, ActionDiagnosticCode.duplicate_term_id, ActionDiagnosticCode.invalid_projection, ActionDiagnosticCode.missing_change_term_link, ActionDiagnosticCode.mode_mismatch, ActionDiagnosticCode.route_mismatch, ActionDiagnosticCode.sla_mismatch, } for diagnostic in diagnostics ) if action not in _CANONICAL_ACTIONS: diagnostics.append(ActionDiagnostic(code=ActionDiagnosticCode.unknown_action)) final_status = ActionAvailabilityStatus.currently_unavailable live_state = ActionKnowledge.unknown elif integrity_failure: final_status = ActionAvailabilityStatus.currently_unavailable live_state = ActionKnowledge.no elif possible is ActionKnowledge.no: final_status = ActionAvailabilityStatus.unsupported_by_product elif promised is ActionKnowledge.no: final_status = ActionAvailabilityStatus.not_negotiated elif promised is ActionKnowledge.unknown: final_status = ActionAvailabilityStatus.legacy_unknown elif wrong_status: final_status = ActionAvailabilityStatus.wrong_status live_state = ActionKnowledge.no elif live_state is ActionKnowledge.yes: final_status = ActionAvailabilityStatus.available_now elif live_state is ActionKnowledge.unknown: final_status = ActionAvailabilityStatus.legacy_unknown else: final_status = ActionAvailabilityStatus.currently_unavailable return MediaBuyActionAssessment( action=action, status=final_status, possible=possible, promised=promised, available=live_state, task=task, mode=mode, change_term_id=change_term_id, async_processing=mode in {"seller_managed", "requires_approval"}, constraints=constraints, diagnostics=tuple(_dedupe_diagnostics(diagnostics)), )Join product, accepted proposal, and live-buy action surfaces.
Inputs may be generated Pydantic models or wire mappings. Unknown future enum values and legacy coarse actions are retained as unknown evidence but are never treated as authority.
def assess_update_media_buy_actions(patch: Any,
current_media_buy: Any,
*,
product: Any | None = None,
proposal: Any | None = None,
now: datetime | None = None) ‑> tuple[MediaBuyActionAssessment, ...]-
Expand source code
def assess_update_media_buy_actions( patch: Any, current_media_buy: Any, *, product: Any | None = None, proposal: Any | None = None, now: datetime | None = None, ) -> tuple[MediaBuyActionAssessment, ...]: """Assess every logical action represented by an update compatibility patch.""" assessments: list[MediaBuyActionAssessment] = [] for mutation in decompose_update_media_buy(patch, current_media_buy): if mutation.action == UNKNOWN_UPDATE_ACTION: continue intent = _intent_from_mutation(mutation, current_media_buy, now=now) assessments.append( assess_media_buy_action( mutation.action, product=product, proposal=proposal, media_buy=current_media_buy, intent=intent, now=now, ) ) return tuple(assessments)Assess every logical action represented by an update compatibility patch.
async def dispatch_media_buy_action(client: ActionDispatchClient,
assessment: MediaBuyActionAssessment,
request: BaseModel,
*,
options: Any | None = None) ‑> Any-
Expand source code
async def dispatch_media_buy_action( client: ActionDispatchClient, assessment: MediaBuyActionAssessment, request: BaseModel, *, options: Any | None = None, ) -> Any: """Dispatch an already-assessed action through its canonical task. The caller still supplies the task-specific generated request model. This helper owns only the action-to-task decision and refuses unavailable or unknown actions. ``seller_managed`` uses the same ordinary async task lifecycle as every other mode; no internal approval workflow is inferred. """ if assessment.status is not ActionAvailabilityStatus.available_now: raise MediaBuyActionError("action_not_available", "status") if assessment.task is None: raise MediaBuyActionError("unknown_action_route", "task") return await client.execute_task(assessment.task.value, request, options=options)Dispatch an already-assessed action through its canonical task.
The caller still supplies the task-specific generated request model. This helper owns only the action-to-task decision and refuses unavailable or unknown actions.
seller_manageduses the same ordinary async task lifecycle as every other mode; no internal approval workflow is inferred. def evaluate_action_constraints(action: str,
constraints: Any | None,
intent: ActionIntent,
*,
now: datetime | None = None) ‑> tuple[ConstraintCheck, ...]-
Expand source code
def evaluate_action_constraints( action: str, constraints: Any | None, intent: ActionIntent, *, now: datetime | None = None, ) -> tuple[ConstraintCheck, ...]: """Evaluate portable constraints without interpreting opaque conditions.""" constraint = _as_mapping(constraints) if not constraint: return () kind = _string_value(constraint.get("kind")) if kind is None or action not in _CONSTRAINT_ACTIONS.get(kind, frozenset()): return ( ConstraintCheck( kind=kind or "unknown", constraint="kind", outcome=ConstraintOutcome.unknown, field=intent.field, ), ) if kind == "budget": return _evaluate_budget_constraints(constraint, intent) if kind == "flight": return _evaluate_flight_constraints(constraint, intent, now=now) if kind == "package_count": return _evaluate_package_constraints(constraint, intent) if kind == "effective_timing": return _evaluate_effective_timing_constraints(constraint, intent, now=now) return ()Evaluate portable constraints without interpreting opaque conditions.
def materialize_change_terms(product_actions: Iterable[Any],
selections: Iterable[ChangeTermSelection | Mapping[str, Any]]) ‑> tuple[MediaBuyChangeTerm, ...]-
Expand source code
def materialize_change_terms( product_actions: Iterable[Any], selections: Iterable[ChangeTermSelection | Mapping[str, Any]], ) -> tuple[MediaBuyChangeTerm, ...]: """Materialize explicitly selected product templates as binding terms. Product declarations are never copied wholesale. Every returned term requires a :class:`ChangeTermSelection`, and all portable bounds, SLA, and status scope are copied from (or narrowed relative to) that template. """ product_index = _unique_action_index(product_actions, field="product_actions") parsed = [ ( value if isinstance(value, ChangeTermSelection) else ChangeTermSelection.model_validate(value) ) for value in selections ] if len({selection.action for selection in parsed}) != len(parsed): raise MediaBuyActionError("duplicate_action", "selections") if len({selection.term_id for selection in parsed}) != len(parsed): raise MediaBuyActionError("duplicate_term_id", "selections") terms: list[MediaBuyChangeTerm] = [] for index, selection in enumerate(parsed): product_action = product_index.get(selection.action) if product_action is None: raise MediaBuyActionError("action_not_advertised", f"selections[{index}].action") if selection.action not in _CANONICAL_ACTIONS: raise MediaBuyActionError("unknown_action", f"selections[{index}].action") modes = _string_sequence(product_action.get("modes")) mode = selection.service_mode if mode is None: if len(modes) != 1: raise MediaBuyActionError( "service_mode_required", f"selections[{index}].service_mode" ) mode = modes[0] if mode not in modes: raise MediaBuyActionError( "service_mode_not_advertised", f"selections[{index}].service_mode" ) product_statuses = _string_sequence(product_action.get("allowed_statuses")) selected_statuses = selection.allowed_statuses if ( selected_statuses is not None and product_statuses and not set(selected_statuses) <= set(product_statuses) ): raise MediaBuyActionError( "status_scope_expanded", f"selections[{index}].allowed_statuses" ) statuses = selected_statuses or (tuple(product_statuses) if product_statuses else None) constraint = _as_mapping(product_action.get("constraints")) or None if constraint is not None: kind = _string_value(constraint.get("kind")) if selection.action not in _CONSTRAINT_ACTIONS.get(kind or "", frozenset()): raise MediaBuyActionError( "constraint_action_mismatch", f"product_actions[{index}].constraints", ) term_payload: dict[str, Any] = { "term_id": selection.term_id, "action": selection.action, "service_mode": mode, } if statuses is not None: term_payload["allowed_statuses"] = list(statuses) sla = _as_mapping(product_action.get("sla")) if sla: term_payload["processing_sla"] = sla if constraint is not None: term_payload["constraints"] = constraint if selection.conditions is not None: term_payload["conditions"] = list(selection.conditions) terms_ref = selection.terms_ref or _string_value(product_action.get("terms_ref")) if terms_ref is not None: term_payload["terms_ref"] = terms_ref if selection.description is not None: term_payload["description"] = selection.description terms.append(MediaBuyChangeTerm.model_validate(term_payload)) return tuple(terms)Materialize explicitly selected product templates as binding terms.
Product declarations are never copied wholesale. Every returned term requires a :class:
ChangeTermSelection, and all portable bounds, SLA, and status scope are copied from (or narrowed relative to) that template. def project_available_actions(change_terms: Iterable[Any] | None,
status: str,
*,
product_actions: Iterable[Any] | None = None,
authorized_actions: Iterable[str] | None = None,
delegated_actions: Iterable[str] | None = None,
policy_actions: Iterable[str] | None = None,
resolved_conditions: Mapping[str, bool] | None = None,
protocol_version: str = '3.2') ‑> MediaBuyActionProjection-
Expand source code
def project_available_actions( change_terms: Iterable[Any] | None, status: str, *, product_actions: Iterable[Any] | None = None, authorized_actions: Iterable[str] | None = None, delegated_actions: Iterable[str] | None = None, policy_actions: Iterable[str] | None = None, resolved_conditions: Mapping[str, bool] | None = None, protocol_version: str = "3.2", ) -> MediaBuyActionProjection: """Derive a fail-closed seller ``available_actions`` projection. ``None`` for an optional narrowing set means that gate has already been satisfied or is not applicable. An explicit empty set denies every action. Conditions must be explicitly resolved ``True``; absent, false, or unknown conditions omit the action. """ diagnostics: list[ActionDiagnostic] = [] if change_terms is None: return MediaBuyActionProjection( diagnostics=(ActionDiagnostic(code=ActionDiagnosticCode.legacy_terms_unknown),) ) if status not in _NON_TERMINAL_STATUSES: return MediaBuyActionProjection() terms: list[Mapping[str, Any]] = [] for index, raw_term in enumerate(change_terms): try: parsed_term = MediaBuyChangeTerm.model_validate(raw_term) except ValidationError: diagnostics.append( ActionDiagnostic( code=ActionDiagnosticCode.invalid_projection, field=f"change_terms[{index}]", ) ) continue terms.append(parsed_term.model_dump(mode="python", by_alias=True, exclude_unset=True)) seen_actions: set[str] = set() seen_ids: set[str] = set() product_index = ( _unique_action_index(product_actions, field="product_actions") if product_actions is not None else None ) gates = [ set(values) if values is not None else None for values in (authorized_actions, delegated_actions, policy_actions) ] supports_link = _supports_change_term_id(protocol_version) supports_task = _supports_action_task(protocol_version) actions: list[ProjectedMediaBuyAction] = [] for index, term in enumerate(terms): action = _string_value(term.get("action")) term_id = _string_value(term.get("term_id")) if action is None or term_id is None or _TERM_ID_RE.fullmatch(term_id) is None: diagnostics.append( ActionDiagnostic( code=ActionDiagnosticCode.invalid_projection, field=f"change_terms[{index}]", ) ) continue if action in seen_actions: diagnostics.append(_diagnostic(ActionDiagnosticCode.duplicate_action, detail=action)) continue if term_id in seen_ids: diagnostics.append(_diagnostic(ActionDiagnosticCode.duplicate_term_id, detail=term_id)) continue seen_actions.add(action) seen_ids.add(term_id) task = route_media_buy_action(action) if task is None: diagnostics.append(_diagnostic(ActionDiagnosticCode.unknown_action, detail=action)) continue allowed_statuses = _string_sequence(term.get("allowed_statuses")) if allowed_statuses and status not in allowed_statuses: continue if any(gate is not None and action not in gate for gate in gates): continue product_action = product_index.get(action) if product_index is not None else None if product_index is not None and product_action is None: continue mode = _string_value(term.get("service_mode")) if mode is None: diagnostics.append( ActionDiagnostic( code=ActionDiagnosticCode.invalid_projection, field=f"change_terms[{index}].service_mode", ) ) continue if product_action is not None: if mode not in _string_sequence(product_action.get("modes")): continue product_statuses = _string_sequence(product_action.get("allowed_statuses")) if product_statuses and status not in product_statuses: continue conditions = _string_sequence(term.get("conditions")) if conditions and ( resolved_conditions is None or any(resolved_conditions.get(condition) is not True for condition in conditions) ): diagnostics.append( _diagnostic(ActionDiagnosticCode.condition_unresolved, detail=action) ) continue constraint = _as_mapping(term.get("constraints")) if constraint: kind = _string_value(constraint.get("kind")) if action not in _CONSTRAINT_ACTIONS.get(kind or "", frozenset()): diagnostics.append( _diagnostic(ActionDiagnosticCode.invalid_projection, detail=action) ) continue wire_mode = "requires_approval" if not supports_link and mode == "seller_managed" else mode action_payload: dict[str, Any] = { "action": action, "mode": wire_mode, "sla": _as_mapping(term.get("processing_sla")) or None, } if supports_task: action_payload["task"] = task if supports_link: action_payload["change_term_id"] = term_id else: # This is an explicit adapter-generated alias. Arbitrary inbound # 3.1 terms_ref values are never interpreted this way. action_payload["terms_ref"] = term_id actions.append(ProjectedMediaBuyAction.model_validate(action_payload)) return MediaBuyActionProjection( actions=tuple(actions), diagnostics=tuple(_dedupe_diagnostics(diagnostics)) )Derive a fail-closed seller
available_actionsprojection.Nonefor an optional narrowing set means that gate has already been satisfied or is not applicable. An explicit empty set denies every action. Conditions must be explicitly resolvedTrue; absent, false, or unknown conditions omit the action. def route_media_buy_action(action: str, *, version: str | None = None) ‑> ActionTask | None-
Expand source code
def route_media_buy_action(action: str, *, version: str | None = None) -> ActionTask | None: """Return the canonical compact task for an in-envelope action. Flight and package-addition changes require proposal refinement; creative lifecycle changes use ``sync_creatives``; the remaining accepted controls use ``control_media_buy``. A structured-only action the tables above have not been taught still routes when the bundle's merged ``enumMetadata`` says it mutates ``update_media_buy`` fields. The spec adds these additively and tells SDKs to dispatch on that block, so consulting it keeps a newer bundle working instead of failing closed on a name the tables predate -- which is the bug ``update_media_buy_frequency_cap`` exposed. Anything the bundle does not describe either still fails closed with ``None`` so callers do not guess a route. """ if action in _CONTROL_ACTIONS: return ActionTask.control_media_buy if action in _REFINEMENT_ACTIONS: return ActionTask.refine_proposals if action in _CREATIVE_ACTIONS: return ActionTask.sync_creatives if action_update_fields(version).get(action): return ActionTask.control_media_buy return NoneReturn the canonical compact task for an in-envelope action.
Flight and package-addition changes require proposal refinement; creative lifecycle changes use
sync_creatives; the remaining accepted controls usecontrol_media_buy.A structured-only action the tables above have not been taught still routes when the bundle's merged
enumMetadatasays it mutatesupdate_media_buyfields. The spec adds these additively and tells SDKs to dispatch on that block, so consulting it keeps a newer bundle working instead of failing closed on a name the tables predate – which is the bugupdate_media_buy_frequency_capexposed. Anything the bundle does not describe either still fails closed withNoneso callers do not guess a route.
Classes
class ActionAvailabilityStatus (*args, **kwds)-
Expand source code
class ActionAvailabilityStatus(StrEnum): """Normalized buyer-facing action assessment result.""" available_now = "available_now" wrong_status = "wrong_status" not_negotiated = "not_negotiated" unsupported_by_product = "unsupported_by_product" currently_unavailable = "currently_unavailable" legacy_unknown = "legacy_unknown"Normalized buyer-facing action assessment result.
Ancestors
- enum.StrEnum
- builtins.str
- enum.ReprEnum
- enum.Enum
Class variables
var available_nowvar legacy_unknownvar not_negotiatedvar unsupported_by_productvar wrong_status
class ActionDiagnostic (**data: Any)-
Expand source code
class ActionDiagnostic(BaseModel): """Bounded diagnostic suitable for UI, logs, and agent reasoning.""" model_config = ConfigDict(extra="forbid", frozen=True) code: ActionDiagnosticCode field: str | None = Field(default=None, max_length=128, pattern=r"^[A-Za-z0-9_.\[\]-]+$") detail: str | None = Field( default=None, max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]{0,127}$", )Bounded diagnostic suitable for UI, logs, and agent reasoning.
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
- pydantic.main.BaseModel
Class variables
var code : ActionDiagnosticCodevar detail : str | Nonevar field : str | Nonevar model_config
class ActionDiagnosticCode (*args, **kwds)-
Expand source code
class ActionDiagnosticCode(StrEnum): """Stable machine-readable explanations emitted by the resolver.""" alias_mismatch = "alias_mismatch" condition_unresolved = "condition_unresolved" constraint_violated = "constraint_violated" duplicate_action = "duplicate_action" duplicate_term_id = "duplicate_term_id" invalid_projection = "invalid_projection" legacy_coarse_action = "legacy_coarse_action" legacy_terms_unknown = "legacy_terms_unknown" missing_change_term_link = "missing_change_term_link" mode_mismatch = "mode_mismatch" route_mismatch = "route_mismatch" sla_mismatch = "sla_mismatch" unknown_action = "unknown_action"Stable machine-readable explanations emitted by the resolver.
Ancestors
- enum.StrEnum
- builtins.str
- enum.ReprEnum
- enum.Enum
Class variables
var alias_mismatchvar condition_unresolvedvar constraint_violatedvar duplicate_actionvar duplicate_term_idvar invalid_projectionvar legacy_coarse_actionvar legacy_terms_unknownvar missing_change_term_linkvar mode_mismatchvar route_mismatchvar sla_mismatchvar unknown_action
class ActionDispatchClient (*args, **kwargs)-
Expand source code
class ActionDispatchClient(Protocol): """Minimal client surface consumed by :func:`dispatch_media_buy_action`.""" async def execute_task( self, task_name: str, request: BaseModel, *, options: Any | None = None, ) -> Any: raise NotImplementedErrorMinimal client surface consumed by :func:
dispatch_media_buy_action().Ancestors
- typing.Protocol
- typing.Generic
Methods
async def execute_task(self, task_name: str, request: BaseModel, *, options: Any | None = None) ‑> Any-
Expand source code
async def execute_task( self, task_name: str, request: BaseModel, *, options: Any | None = None, ) -> Any: raise NotImplementedError
class ActionIntent (**data: Any)-
Expand source code
class ActionIntent(BaseModel): """Portable current/result state used to preflight typed constraints. Callers may provide this directly. :func:`assess_update_media_buy_actions` derives the same fields from an update patch when enough current state is available. """ model_config = ConfigDict(extra="forbid", frozen=True) current_amount: Decimal | None = None result_amount: Decimal | None = None currency: str | None = Field(default=None, pattern=r"^[A-Z]{3}$") current_time: datetime | None = None result_time: datetime | None = None effective_at: datetime | None = None additions: int | None = Field(default=None, ge=0) removals: int | None = Field(default=None, ge=0) current_package_count: int | None = Field(default=None, ge=0) result_package_count: int | None = Field(default=None, ge=0) field: str | None = Field(default=None, max_length=128, pattern=r"^[A-Za-z0-9_.\[\]-]+$")Portable current/result state used to preflight typed constraints.
Callers may provide this directly. :func:
assess_update_media_buy_actions()derives the same fields from an update patch when enough current state is available.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
- pydantic.main.BaseModel
Class variables
var additions : int | Nonevar currency : str | Nonevar current_amount : decimal.Decimal | Nonevar current_package_count : int | Nonevar current_time : datetime.datetime | Nonevar effective_at : datetime.datetime | Nonevar field : str | Nonevar model_configvar removals : int | Nonevar result_amount : decimal.Decimal | Nonevar result_package_count : int | Nonevar result_time : datetime.datetime | None
class ActionKnowledge (*args, **kwds)-
Expand source code
class ActionKnowledge(StrEnum): """Whether a protocol surface proves, disproves, or cannot answer a fact.""" yes = "yes" no = "no" unknown = "unknown"Whether a protocol surface proves, disproves, or cannot answer a fact.
Ancestors
- enum.StrEnum
- builtins.str
- enum.ReprEnum
- enum.Enum
Class variables
var novar unknownvar yes
class ActionTask (*args, **kwds)-
Expand source code
class ActionTask(StrEnum): """Compact-lifecycle task used to exercise an action.""" control_media_buy = "control_media_buy" refine_proposals = "refine_proposals" sync_creatives = "sync_creatives"Compact-lifecycle task used to exercise an action.
Ancestors
- enum.StrEnum
- builtins.str
- enum.ReprEnum
- enum.Enum
Class variables
var control_media_buyvar refine_proposalsvar sync_creatives
class ChangeTermSelection (**data: Any)-
Expand source code
class ChangeTermSelection(BaseModel): """A seller's explicit decision to bind one advertised product action.""" model_config = ConfigDict(extra="forbid", frozen=True) action: str = Field(max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]{0,127}$") term_id: str = Field(max_length=255, pattern=r"^[A-Za-z0-9_.:-]+$") service_mode: str | None = Field( default=None, max_length=64, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$" ) allowed_statuses: tuple[str, ...] | None = None conditions: tuple[str, ...] | None = None terms_ref: str | None = Field(default=None, min_length=1, max_length=1000) description: str | None = Field(default=None, min_length=1, max_length=1000) @model_validator(mode="after") def _validate_values(self) -> ChangeTermSelection: if self.allowed_statuses is not None: if not self.allowed_statuses or len(set(self.allowed_statuses)) != len( self.allowed_statuses ): raise ValueError("allowed_statuses must be non-empty and unique") if any(status not in _NON_TERMINAL_STATUSES for status in self.allowed_statuses): raise ValueError("allowed_statuses may contain only non-terminal statuses") if self.conditions is not None: if not self.conditions or len(set(self.conditions)) != len(self.conditions): raise ValueError("conditions must be non-empty and unique") if any( re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}", condition) is None for condition in self.conditions ): raise ValueError("condition identifiers must use the protocol token grammar") return selfA seller's explicit decision to bind one advertised product action.
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
- pydantic.main.BaseModel
Class variables
var action : strvar allowed_statuses : tuple[str, ...] | Nonevar conditions : tuple[str, ...] | Nonevar description : str | Nonevar model_configvar service_mode : str | Nonevar term_id : strvar terms_ref : str | None
class ConstraintCheck (**data: Any)-
Expand source code
class ConstraintCheck(BaseModel): """Evaluation of one portable constraint field.""" model_config = ConfigDict(extra="forbid", frozen=True) kind: str = Field(max_length=32, pattern=r"^[a-z_]+$") constraint: str = Field(max_length=64, pattern=r"^[a-z_]+$") outcome: ConstraintOutcome field: str | None = Field(default=None, max_length=128, pattern=r"^[A-Za-z0-9_.\[\]-]+$")Evaluation of one portable constraint field.
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
- pydantic.main.BaseModel
Class variables
var constraint : strvar field : str | Nonevar kind : strvar model_configvar outcome : ConstraintOutcome
class ConstraintOutcome (*args, **kwds)-
Expand source code
class ConstraintOutcome(StrEnum): """Result of preflighting one portable change-term constraint.""" satisfied = "satisfied" violated = "violated" unknown = "unknown"Result of preflighting one portable change-term constraint.
Ancestors
- enum.StrEnum
- builtins.str
- enum.ReprEnum
- enum.Enum
Class variables
var satisfiedvar unknownvar violated
class MediaBuyActionAssessment (**data: Any)-
Expand source code
class MediaBuyActionAssessment(BaseModel): """The joined possible/promised/current answer for one action.""" model_config = ConfigDict(extra="forbid", frozen=True) action: str = Field(max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]{0,127}$") status: ActionAvailabilityStatus possible: ActionKnowledge promised: ActionKnowledge available: ActionKnowledge task: ActionTask | None = None mode: str | None = Field(default=None, max_length=64, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$") change_term_id: str | None = Field(default=None, max_length=255, pattern=r"^[A-Za-z0-9_.:-]+$") async_processing: bool = False constraints: tuple[ConstraintCheck, ...] = () diagnostics: tuple[ActionDiagnostic, ...] = ()The joined possible/promised/current answer for one action.
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
- pydantic.main.BaseModel
Class variables
var action : strvar async_processing : boolvar available : ActionKnowledgevar change_term_id : str | Nonevar constraints : tuple[ConstraintCheck, ...]var diagnostics : tuple[ActionDiagnostic, ...]var mode : str | Nonevar model_configvar possible : ActionKnowledgevar promised : ActionKnowledgevar status : ActionAvailabilityStatusvar task : ActionTask | None
class MediaBuyActionError (code: str, field: str | None = None)-
Expand source code
class MediaBuyActionError(ValueError): """A safe local validation error for invalid seller helper inputs.""" def __init__(self, code: str, field: str | None = None) -> None: self.code = code self.field = field suffix = f" at {field}" if field is not None else "" super().__init__(f"media-buy action validation failed: {code}{suffix}")A safe local validation error for invalid seller helper inputs.
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class MediaBuyActionProjection (**data: Any)-
Expand source code
class MediaBuyActionProjection(BaseModel): """Seller current-state projection plus any fail-closed omissions.""" model_config = ConfigDict(extra="forbid", frozen=True) actions: tuple[ProjectedMediaBuyAction, ...] = () diagnostics: tuple[ActionDiagnostic, ...] = () def to_wire(self) -> list[dict[str, object]]: """Return the JSON-ready ``available_actions`` array.""" return [action.model_dump(mode="json", exclude_none=True) for action in self.actions]Seller current-state projection plus any fail-closed omissions.
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
- pydantic.main.BaseModel
Class variables
var actions : tuple[ProjectedMediaBuyAction, ...]var diagnostics : tuple[ActionDiagnostic, ...]var model_config
Methods
def to_wire(self) ‑> list[dict[str, object]]-
Expand source code
def to_wire(self) -> list[dict[str, object]]: """Return the JSON-ready ``available_actions`` array.""" return [action.model_dump(mode="json", exclude_none=True) for action in self.actions]Return the JSON-ready
available_actionsarray.
class ProjectedMediaBuyAction (**data: Any)-
Expand source code
class ProjectedMediaBuyAction(BaseModel): """Version-adaptable seller projection for one currently available action.""" model_config = ConfigDict(extra="forbid", frozen=True) action: str = Field(max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]{0,127}$") mode: str = Field(max_length=64, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$") task: ActionTask | None = None sla: dict[str, object] | None = None change_term_id: str | None = Field(default=None, max_length=255, pattern=r"^[A-Za-z0-9_.:-]+$") terms_ref: str | None = Field(default=None, max_length=1000)Version-adaptable seller projection for one currently available action.
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
- pydantic.main.BaseModel
Class variables
var action : strvar change_term_id : str | Nonevar mode : strvar model_configvar sla : dict[str, object] | Nonevar task : ActionTask | Nonevar terms_ref : str | None