From 9de3cd96d612c01b7365fe8def2f501e94ec6416 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 11 Sep 2026 02:32:19 +0000 Subject: [PATCH] fix(codegen): inherit root allOf ProtocolEnvelope on every response arm Every AdCP response schema composes core/protocol-envelope.json at its root via allOf, so status, task_id, message, context_id, replayed, timestamp, push_notification_config, governance_context, context, payload and adcp_error belong to every arm of the response's oneOf. The response-arm emitter attached ProtocolEnvelope only to the arm whose own branch pinned status: submitted, so 19 of 24 *SuccessResponse aliases and their matching error arms carried those fields as pydantic extras: a seller setting response.replayed = True wrote an extra, a buyer reading it got an AttributeError, and the arms of one oneOf had no common ancestor below AdcpVersionEnvelope. Derive the base from the ROOT schema instead of the branch. Emitter.render resolves the root allOf chain through _resolve_schema_ref, so canonical https:// refs, root-relative /schemas/ refs and ../core/ relative refs all land on the same target, and every emitted arm of a root-composing response gets the base. The is_submitted path becomes a subset and still supplies the base for a response whose root does not compose the envelope. canonical_creative's dynamic clones rebuilt their fields on CanonicalBoundaryModel alone, which dropped the ancestry for the create/update media buy and get_products surfaces. _canonical_clone now re-declares the envelopes the source composes as bases, ordered before CanonicalBoundaryModel so pydantic's left-to-right model_config merge keeps the boundary's extra="allow" rather than reinstating AdCPBaseModel's extra="ignore". The stub mirrors the ancestry through a stub-only _CanonicalResponseEnvelope that relaxes status to Any, so each arm can declare its precise Literal without a per-field suppression. That relaxation is a Liskov bridge, not a public type: every concrete canonical response re-declares status with the exact annotation its runtime model carries - wide TaskStatus for ordinary arms, Literal["completed"] for the pinned sync arms, Literal[TaskStatus .submitted] for the async task-envelope arms - and each carries "= ..." to match the runtime default, so construction never falsely demands status. CreateMediaBuyResponse1's explicit constructor is precise and default- consistent too. typing_extensions.assert_type pins the completed, wide and submitted representatives, and a runtime test parses the stub and fails if any concrete response drifts back onto the bridge's Any. Serialization audit: 50 of 57 generated arms now emit status: "completed" and/or replayed: false by default; no field stops being emitted. Both are conformant - status is REQUIRED on every task response envelope, and replayed is specified as false-or-omitted. Closes #1136 --- scripts/post_generate_fixes.py | 41 ++- src/adcp/types/canonical_creative.py | 30 +- src/adcp/types/canonical_creative.pyi | 84 ++++- .../get_account_financials_response.py | 5 +- .../account/sync_accounts_response.py | 5 +- .../brand/acquire_rights_response.py | 9 +- .../brand/get_brand_identity_response.py | 5 +- .../brand/get_rights_response.py | 5 +- .../brand/update_rights_response.py | 5 +- .../calibrate_content_response.py | 5 +- .../get_content_standards_response.py | 5 +- .../get_media_buy_artifacts_response.py | 5 +- .../validate_content_delivery_response.py | 5 +- .../get_creative_features_response.py | 5 +- .../creative/preview_creative_response.py | 6 +- .../creative/sync_creatives_response.py | 4 +- .../media_buy/build_creative_response.py | 10 +- .../media_buy/create_media_buy_response.py | 4 +- .../media_buy/log_event_response.py | 5 +- .../provide_performance_feedback_response.py | 5 +- .../media_buy/sync_audiences_response.py | 4 +- .../media_buy/sync_catalogs_response.py | 4 +- .../media_buy/sync_event_sources_response.py | 5 +- .../media_buy/update_media_buy_response.py | 4 +- .../signals/activate_signal_response.py | 5 +- tests/test_code_generation.py | 168 +++++++++ tests/test_decisioning_specialisms.py | 22 +- tests/test_protocol_envelope_inheritance.py | 325 ++++++++++++++++++ tests/type_checks/response_envelope_fields.py | 119 +++++++ 29 files changed, 829 insertions(+), 75 deletions(-) create mode 100644 tests/test_protocol_envelope_inheritance.py create mode 100644 tests/type_checks/response_envelope_fields.py diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index da4a78876..7f32c089e 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -3984,6 +3984,7 @@ def __init__(self, relative: str, base: str, schema_rel: Path): self.nested_names: set[str] = set() self.local_ref_types: dict[str, str] = {} self.root_schema: dict[str, Any] = {} + self.root_composes_protocol_envelope = False self.needs_protocol_envelope = False self.needs_media_buy_helpers = False self.needs_sequence = False @@ -4211,16 +4212,51 @@ def emit_nested(self, preferred: str, schema: dict[str, Any]) -> str: self.nested.append("\n".join(lines)) return class_name + def composes_protocol_envelope(self, schema: dict[str, Any]) -> bool: + """Return whether ``schema`` composes ``core/protocol-envelope.json``. + + The check walks the document's own ``allOf`` chain, including + nested ``allOf`` groupings, and resolves every ``$ref`` through + :func:`_resolve_schema_ref` so canonical ``https://`` refs, + root-relative ``/schemas/`` refs and ``../core/`` relative refs all + land on the same target. + """ + + parts = schema.get("allOf") + if not isinstance(parts, list): + return False + for part in parts: + if not isinstance(part, dict): + continue + ref = part.get("$ref") + if isinstance(ref, str): + try: + ref_rel = _resolve_schema_ref(self.schema_rel, ref) + except ValueError: + ref_rel = None + if ref_rel is not None and ref_rel.as_posix() == "core/protocol-envelope.json": + return True + if self.composes_protocol_envelope(part): + return True + return False + def emit_response_class(self, class_name: str, arm: dict[str, Any]) -> str: props = arm.get("properties") or {} required = set(arm.get("required") or []) is_submitted = ( props.get("status", {}).get("const") == "submitted" and "task_id" in props ) + # A root-level ``allOf`` applies to the whole document, so every + # ``oneOf`` arm of a response whose root composes + # ``core/protocol-envelope.json`` carries the protocol envelope: + # success arms, error arms and submitted arms alike. + inherits_protocol_envelope = is_submitted or self.root_composes_protocol_envelope bases = ( - "AdcpVersionEnvelope, ProtocolEnvelope" if is_submitted else "AdcpVersionEnvelope" + "AdcpVersionEnvelope, ProtocolEnvelope" + if inherits_protocol_envelope + else "AdcpVersionEnvelope" ) - if is_submitted: + if inherits_protocol_envelope: self.needs_protocol_envelope = True lines = [f"class {class_name}({bases}):"] if is_submitted: @@ -4320,6 +4356,7 @@ def emit_response_class(self, class_name: str, arm: dict[str, Any]) -> str: def render(self, schema: dict[str, Any]) -> str: self.root_schema = schema + self.root_composes_protocol_envelope = self.composes_protocol_envelope(schema) arms = schema.get("oneOf") or schema.get("anyOf") or [] if not arms: arms = [schema] diff --git a/src/adcp/types/canonical_creative.py b/src/adcp/types/canonical_creative.py index 09e850165..d1c1e8a3b 100644 --- a/src/adcp/types/canonical_creative.py +++ b/src/adcp/types/canonical_creative.py @@ -46,6 +46,8 @@ from adcp.types.generated_poc.core.product import Product as _LegacyProduct from adcp.types.generated_poc.core.product_filters import ProductFilters as _LegacyProductFilters from adcp.types.generated_poc.core.product_format_declaration import SellerPreference +from adcp.types.generated_poc.core.protocol_envelope import ProtocolEnvelope +from adcp.types.generated_poc.core.version_envelope import AdcpVersionEnvelope from adcp.types.generated_poc.creative.get_creative_delivery_response import ( Creative as _LegacyDeliveryCreative, ) @@ -411,6 +413,32 @@ def _serialize_canonical_model( ) +#: Protocol envelopes a generated wire model may compose at its schema root. +#: A canonical clone copies the envelope *fields*, but a clone built on +#: ``CanonicalBoundaryModel`` alone would drop the envelope *ancestry* — so +#: ``issubclass(GetProductsResponse, ProtocolEnvelope)`` would be ``False`` even +#: though the response carries ``status``/``replayed``/``task_id``. Re-declare +#: the envelopes as additional bases so the canonical surface keeps the same +#: ancestry as the generated surface it replaces. +_ENVELOPE_BASES: tuple[type[AdCPBaseModel], ...] = (AdcpVersionEnvelope, ProtocolEnvelope) + + +def _canonical_clone_bases(source: type[AdCPBaseModel]) -> tuple[type[AdCPBaseModel], ...]: + """Return the clone bases for ``source``: its envelopes, then the boundary. + + ``CanonicalBoundaryModel`` comes last on purpose. Pydantic merges + ``model_config`` across bases left to right, so the right-most base wins; + the envelopes inherit :class:`AdCPBaseModel`'s ``extra`` policy and would + otherwise override the boundary's ``extra="allow"`` and start dropping + caller-supplied extension keys. Method resolution is unaffected — the + envelopes override nothing, so ``CanonicalBoundaryModel`` still supplies + ``model_dump``/``model_json_schema`` ahead of :class:`AdCPBaseModel`. + """ + + envelopes = tuple(envelope for envelope in _ENVELOPE_BASES if issubclass(source, envelope)) + return (*envelopes, CanonicalBoundaryModel) + + def _canonical_clone( name: str, source: type[AdCPBaseModel], @@ -420,7 +448,7 @@ def _canonical_clone( ) -> type[CanonicalBoundaryModel]: model = create_model( # type: ignore[call-overload] name, - __base__=CanonicalBoundaryModel, + __base__=_canonical_clone_bases(source), __module__=__name__, __validators__={ "_serialize_canonical": model_serializer(mode="wrap")(_serialize_canonical_model) diff --git a/src/adcp/types/canonical_creative.pyi b/src/adcp/types/canonical_creative.pyi index 4c049fe8c..3da56d7ff 100644 --- a/src/adcp/types/canonical_creative.pyi +++ b/src/adcp/types/canonical_creative.pyi @@ -4,6 +4,9 @@ from typing import Any, ClassVar, Literal, TypeAlias, TypeVar from adcp.types.base import AdCPBaseModel from adcp.types.generated_poc.core.canonical_format_kind import CanonicalFormatKind +from adcp.types.generated_poc.core.protocol_envelope import ProtocolEnvelope +from adcp.types.generated_poc.core.version_envelope import AdcpVersionEnvelope +from adcp.types.generated_poc.enums.task_status import TaskStatus from adcp.types.legacy import LegacyFormatId _T = TypeVar("_T", bound=AdCPBaseModel) @@ -11,6 +14,30 @@ _T = TypeVar("_T", bound=AdCPBaseModel) class CanonicalBoundaryModel(AdCPBaseModel): __adcp_canonical_creative_model__: ClassVar[bool] +class _CanonicalResponseEnvelope(AdcpVersionEnvelope, ProtocolEnvelope, CanonicalBoundaryModel): + """Stub-only Liskov bridge for canonical responses; not a runtime class. + + The canonical response clones inherit :class:`AdcpVersionEnvelope`, + :class:`ProtocolEnvelope` and :class:`CanonicalBoundaryModel` directly at + runtime, so ``isinstance``/``issubclass`` agree with this stub. Collapsing + them into one private ancestor exists purely for the type checker: several + schema arms pin an envelope field to a ``Literal``, and narrowing an + inherited mutable attribute is a Liskov violation. Relaxing the pinned + fields to ``Any`` once here lets each arm declare its precise literal with + no per-field suppression, and keeps plain-string construction + (``status="completed"``) working for adopters. + + This is a bridge, not a public type. Every concrete response below + re-declares ``status`` with the exact annotation its runtime model carries, + so no adopter ever reads ``Any`` off one of them — enforced by + ``test_canonical_response_stub_status_matches_runtime``. The ``= ...`` + matters as much as the type: ``status`` is defaulted on every runtime + response, so a bare ``status: Any`` would make the synthesized ``__init__`` + demand it and reject a plain ``GetMediaBuysResponse(media_buys=[])``. + """ + + status: Any = ... + class Format(CanonicalBoundaryModel): format_option_id: str | None publisher_domain: str | None @@ -70,7 +97,7 @@ class DeliveryCreative(CanonicalBoundaryModel): class CreativeFilters(CanonicalBoundaryModel): ... class ProductFilters(CanonicalBoundaryModel): ... -class PackageRequest(CanonicalBoundaryModel): +class PackageRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): product_id: str format_option_refs: list[Any] | None creatives: list[CreativeAsset] | None @@ -93,7 +120,7 @@ class Package(CanonicalBoundaryModel): **data: Any, ) -> None: ... -class GetProductsRequest(CanonicalBoundaryModel): +class GetProductsRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): account: Any filters: ProductFilters | None fields: Any @@ -101,7 +128,8 @@ class GetProductsRequest(CanonicalBoundaryModel): time_budget: Any pagination: Any -class GetProductsResponse(CanonicalBoundaryModel): +class GetProductsResponse(_CanonicalResponseEnvelope): + status: TaskStatus = ... products: list[Product] | None proposals: Any refinement_applied: Any @@ -114,19 +142,23 @@ class GetProductsResponse(CanonicalBoundaryModel): **data: Any, ) -> None: ... -class CreateMediaBuyRequest(CanonicalBoundaryModel): +class CreateMediaBuyRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): account: Any packages: list[PackageRequest] | None -class UpdateMediaBuyRequest(CanonicalBoundaryModel): +class UpdateMediaBuyRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): account: Any media_buy_id: str packages: list[PackageUpdate] | None new_packages: list[PackageRequest] | None -class CreateMediaBuyResponse1(CanonicalBoundaryModel): +class CreateMediaBuyResponse1(_CanonicalResponseEnvelope): media_buy_id: str packages: list[Package] + # AdCP 3.2 drops the synchronous task-envelope status from this arm, so the + # runtime model pins it to the single outcome and defaults it. Mirror both + # halves: the literal and the default. + status: Literal["completed"] = ... # Required *and* nullable: the schema lists confirmed_at in the success # branch's ``required`` while typing it ``["string", "null"]``. A buy # awaiting seller commitment carries the key with a null value. @@ -135,45 +167,56 @@ class CreateMediaBuyResponse1(CanonicalBoundaryModel): self, *, media_buy_id: str, - status: Any, confirmed_at: datetime | None, revision: int, packages: list[Package], + status: Literal["completed"] = ..., media_buy_status: Any = ..., **data: Any, ) -> None: ... -class CreateMediaBuyResponse2(CanonicalBoundaryModel): ... -class CreateMediaBuyResponse3(CanonicalBoundaryModel): ... +class CreateMediaBuyResponse2(_CanonicalResponseEnvelope): + status: TaskStatus = ... + +class CreateMediaBuyResponse3(_CanonicalResponseEnvelope): + status: Literal[TaskStatus.submitted] = ... CreateMediaBuyResponse: TypeAlias = ( CreateMediaBuyResponse1 | CreateMediaBuyResponse2 | CreateMediaBuyResponse3 ) -class UpdateMediaBuyResponse1(CanonicalBoundaryModel): +class UpdateMediaBuyResponse1(_CanonicalResponseEnvelope): media_buy_id: str - status: Literal["completed"] + # The 3.x schema arm pins ``status`` to the single synchronous outcome. + # ``_CanonicalResponseEnvelope`` is what makes this precise literal legal + # without a per-field suppression, and it keeps ``status="completed"`` + # constructible — see tests/type_checks/extend_response_with_sequence.py. + status: Literal["completed"] = ... revision: int media_buy_status: Any = ... affected_packages: Sequence[Package] | None = ... -class UpdateMediaBuyResponse2(CanonicalBoundaryModel): ... -class UpdateMediaBuyResponse3(CanonicalBoundaryModel): ... +class UpdateMediaBuyResponse2(_CanonicalResponseEnvelope): + status: TaskStatus = ... + +class UpdateMediaBuyResponse3(_CanonicalResponseEnvelope): + status: Literal[TaskStatus.submitted] = ... UpdateMediaBuyResponse: TypeAlias = ( UpdateMediaBuyResponse1 | UpdateMediaBuyResponse2 | UpdateMediaBuyResponse3 ) -class SyncCreativesRequest(CanonicalBoundaryModel): +class SyncCreativesRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): account: Any creatives: list[CreativeAsset] -class ListCreativesRequest(CanonicalBoundaryModel): +class ListCreativesRequest(AdcpVersionEnvelope, CanonicalBoundaryModel): account: Any filters: CreativeFilters | None fields: Any -class ListCreativesResponse(CanonicalBoundaryModel): +class ListCreativesResponse(_CanonicalResponseEnvelope): + status: TaskStatus = ... creatives: list[Creative] class MediaBuyPackage(CanonicalBoundaryModel): ... @@ -181,12 +224,15 @@ class MediaBuyPackage(CanonicalBoundaryModel): ... class MediaBuy(CanonicalBoundaryModel): packages: Sequence[MediaBuyPackage] -class GetMediaBuysResponse(CanonicalBoundaryModel): +class GetMediaBuysResponse(_CanonicalResponseEnvelope): + status: TaskStatus = ... media_buys: Sequence[MediaBuy] -class GetMediaBuyDeliveryResponse(CanonicalBoundaryModel): ... +class GetMediaBuyDeliveryResponse(_CanonicalResponseEnvelope): + status: TaskStatus = ... -class GetCreativeDeliveryResponse(CanonicalBoundaryModel): +class GetCreativeDeliveryResponse(_CanonicalResponseEnvelope): + status: TaskStatus = ... creatives: Sequence[DeliveryCreative] PRIMARY_CANONICAL_MODELS: tuple[type[CanonicalBoundaryModel], ...] diff --git a/src/adcp/types/generated_poc/account/get_account_financials_response.py b/src/adcp/types/generated_poc/account/get_account_financials_response.py index 2d5f405d1..f000dd0e3 100644 --- a/src/adcp/types/generated_poc/account/get_account_financials_response.py +++ b/src/adcp/types/generated_poc/account/get_account_financials_response.py @@ -16,6 +16,7 @@ from ..core import date_range as date_range_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import payment_terms as payment_terms_1 @@ -54,7 +55,7 @@ class Invoice(AdcpVersionEnvelope): paid_date: date | None = None -class GetAccountFinancialsResponse1(AdcpVersionEnvelope): +class GetAccountFinancialsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') account: account_ref_1.AccountReference currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] @@ -70,7 +71,7 @@ class GetAccountFinancialsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class GetAccountFinancialsResponse2(AdcpVersionEnvelope): +class GetAccountFinancialsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/account/sync_accounts_response.py b/src/adcp/types/generated_poc/account/sync_accounts_response.py index f8f28c406..d75c57dc2 100644 --- a/src/adcp/types/generated_poc/account/sync_accounts_response.py +++ b/src/adcp/types/generated_poc/account/sync_accounts_response.py @@ -20,6 +20,7 @@ from ..core import notification_config as notification_config_1 from ..core import operator_unit as operator_unit_1 from ..core import reporting_delivery_config_state as reporting_delivery_config_state_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import account_scope as account_scope_1 from ..enums import billing_party as billing_party_1 from ..enums import payment_terms as payment_terms_1 @@ -68,7 +69,7 @@ class Account(AdcpVersionEnvelope): authorization: account_authorization_1.AccountAuthorization | None = None -class SyncAccountsResponse1(AdcpVersionEnvelope): +class SyncAccountsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') dry_run: bool | None = None accounts: list[Account] @@ -76,7 +77,7 @@ class SyncAccountsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncAccountsResponse2(AdcpVersionEnvelope): +class SyncAccountsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/brand/acquire_rights_response.py b/src/adcp/types/generated_poc/brand/acquire_rights_response.py index a98ea84ab..825371fd7 100644 --- a/src/adcp/types/generated_poc/brand/acquire_rights_response.py +++ b/src/adcp/types/generated_poc/brand/acquire_rights_response.py @@ -15,6 +15,7 @@ from ..core import ext as ext_1 from ..core import generation_credential as generation_credential_1 from ..core import push_notification_config as push_notification_config_1 +from ..core.protocol_envelope import ProtocolEnvelope class Disclosure(AdcpVersionEnvelope): @@ -23,7 +24,7 @@ class Disclosure(AdcpVersionEnvelope): text: str | None = None -class AcquireRightsResponse1(AdcpVersionEnvelope): +class AcquireRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') rights_id: str rights_status: Literal['acquired'] = 'acquired' @@ -39,7 +40,7 @@ class AcquireRightsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class AcquireRightsResponse2(AdcpVersionEnvelope): +class AcquireRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') rights_id: str rights_status: Literal['pending_approval'] = 'pending_approval' @@ -50,7 +51,7 @@ class AcquireRightsResponse2(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class AcquireRightsResponse3(AdcpVersionEnvelope): +class AcquireRightsResponse3(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') rights_id: str rights_status: Literal['rejected'] = 'rejected' @@ -61,7 +62,7 @@ class AcquireRightsResponse3(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class AcquireRightsResponse4(AdcpVersionEnvelope): +class AcquireRightsResponse4(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/brand/get_brand_identity_response.py b/src/adcp/types/generated_poc/brand/get_brand_identity_response.py index ff165d2d7..69ccf26d0 100644 --- a/src/adcp/types/generated_poc/brand/get_brand_identity_response.py +++ b/src/adcp/types/generated_poc/brand/get_brand_identity_response.py @@ -12,6 +12,7 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import asset_content_type as asset_content_type_1 from ..enums import right_use as right_use_1 @@ -105,7 +106,7 @@ class Rights(AdcpVersionEnvelope): content_restrictions: list[str] | None = None -class GetBrandIdentityResponse1(AdcpVersionEnvelope): +class GetBrandIdentityResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') brand_id: str house: House @@ -127,7 +128,7 @@ class GetBrandIdentityResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class GetBrandIdentityResponse2(AdcpVersionEnvelope): +class GetBrandIdentityResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/brand/get_rights_response.py b/src/adcp/types/generated_poc/brand/get_rights_response.py index 2b01adace..f949927e9 100644 --- a/src/adcp/types/generated_poc/brand/get_rights_response.py +++ b/src/adcp/types/generated_poc/brand/get_rights_response.py @@ -13,6 +13,7 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import right_type as right_type_1 from ..enums import right_use as right_use_1 @@ -55,7 +56,7 @@ class Excluded(AdcpVersionEnvelope): suggestions: list[str] | None = None -class GetRightsResponse1(AdcpVersionEnvelope): +class GetRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') rights: list[Right] excluded: list[Excluded] | None = None @@ -63,7 +64,7 @@ class GetRightsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class GetRightsResponse2(AdcpVersionEnvelope): +class GetRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/brand/update_rights_response.py b/src/adcp/types/generated_poc/brand/update_rights_response.py index 69ed76006..7678cdef9 100644 --- a/src/adcp/types/generated_poc/brand/update_rights_response.py +++ b/src/adcp/types/generated_poc/brand/update_rights_response.py @@ -14,9 +14,10 @@ from ..core import error as error_1 from ..core import ext as ext_1 from ..core import generation_credential as generation_credential_1 +from ..core.protocol_envelope import ProtocolEnvelope -class UpdateRightsResponse1(AdcpVersionEnvelope): +class UpdateRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') rights_id: str terms: rights_terms_1.RightsTerms @@ -30,7 +31,7 @@ class UpdateRightsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class UpdateRightsResponse2(AdcpVersionEnvelope): +class UpdateRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py b/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py index 669bfc5f0..c0a3c7623 100644 --- a/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py +++ b/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py @@ -12,6 +12,7 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import binary_verdict as binary_verdict_1 from ..enums import feature_check_status as feature_check_status_1 @@ -25,7 +26,7 @@ class Feature(AdcpVersionEnvelope): confidence: Annotated[float, Field(ge=0, le=1)] | None = None -class CalibrateContentResponse1(AdcpVersionEnvelope): +class CalibrateContentResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') verdict: binary_verdict_1.BinaryVerdict confidence: Annotated[float, Field(ge=0, le=1)] | None = None @@ -35,7 +36,7 @@ class CalibrateContentResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class CalibrateContentResponse2(AdcpVersionEnvelope): +class CalibrateContentResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: list[error_1.Error] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py b/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py index 06b885e35..51020f83b 100644 --- a/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py +++ b/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py @@ -12,15 +12,16 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope -class GetContentStandardsResponse1(AdcpVersionEnvelope): +class GetContentStandardsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') context: context_1.ContextObject | None = None ext: ext_1.ExtensionObject | None = None -class GetContentStandardsResponse2(AdcpVersionEnvelope): +class GetContentStandardsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: list[error_1.Error] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py index 4639ef1f6..54d0504e5 100644 --- a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py +++ b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py @@ -14,6 +14,7 @@ from ..core import error as error_1 from ..core import ext as ext_1 from ..core import pagination_response as pagination_response_1 +from ..core.protocol_envelope import ProtocolEnvelope class BrandContext(AdcpVersionEnvelope): @@ -42,7 +43,7 @@ class CollectionInfo(AdcpVersionEnvelope): effective_rate: float | None = None -class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope): +class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') media_buy_id: str artifacts: list[Artifact] @@ -52,7 +53,7 @@ class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class GetMediaBuyArtifactsResponse2(AdcpVersionEnvelope): +class GetMediaBuyArtifactsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: list[error_1.Error] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py b/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py index 616211201..0c75fb0d5 100644 --- a/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py +++ b/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py @@ -12,6 +12,7 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import binary_verdict as binary_verdict_1 from ..enums import feature_check_status as feature_check_status_1 @@ -39,7 +40,7 @@ class Result(AdcpVersionEnvelope): features: list[Feature] | None = None -class ValidateContentDeliveryResponse1(AdcpVersionEnvelope): +class ValidateContentDeliveryResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') summary: Summary results: list[Result] @@ -47,7 +48,7 @@ class ValidateContentDeliveryResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class ValidateContentDeliveryResponse2(AdcpVersionEnvelope): +class ValidateContentDeliveryResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: list[error_1.Error] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/creative/get_creative_features_response.py b/src/adcp/types/generated_poc/creative/get_creative_features_response.py index 1bfe0665c..f4f1c0f68 100644 --- a/src/adcp/types/generated_poc/creative/get_creative_features_response.py +++ b/src/adcp/types/generated_poc/creative/get_creative_features_response.py @@ -15,9 +15,10 @@ from ..core import creative_consumption as creative_consumption_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope -class GetCreativeFeaturesResponse1(AdcpVersionEnvelope): +class GetCreativeFeaturesResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') results: list[creative_feature_result_1.CreativeFeatureResult] detail_url: AnyUrl | None = None @@ -30,7 +31,7 @@ class GetCreativeFeaturesResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class GetCreativeFeaturesResponse2(AdcpVersionEnvelope): +class GetCreativeFeaturesResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: list[error_1.Error] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/creative/preview_creative_response.py b/src/adcp/types/generated_poc/creative/preview_creative_response.py index 1b5172672..c565a5f68 100644 --- a/src/adcp/types/generated_poc/creative/preview_creative_response.py +++ b/src/adcp/types/generated_poc/creative/preview_creative_response.py @@ -69,7 +69,7 @@ class Preview3(AdcpVersionEnvelope): renders: Annotated[list[preview_render_1.PreviewRender], Field(min_length=1)] -class PreviewCreativeResponse1(AdcpVersionEnvelope): +class PreviewCreativeResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') response_type: Literal['single'] = 'single' previews: Annotated[list[Preview], Field(min_length=1)] @@ -80,7 +80,7 @@ class PreviewCreativeResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class PreviewCreativeResponse2(AdcpVersionEnvelope): +class PreviewCreativeResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') response_type: Literal['batch'] = 'batch' results: Annotated[list[Result], Field(min_length=1)] @@ -88,7 +88,7 @@ class PreviewCreativeResponse2(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class PreviewCreativeResponse3(AdcpVersionEnvelope): +class PreviewCreativeResponse3(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') response_type: Literal['variant'] = 'variant' variant_id: str diff --git a/src/adcp/types/generated_poc/creative/sync_creatives_response.py b/src/adcp/types/generated_poc/creative/sync_creatives_response.py index c8ac62e48..cebca0004 100644 --- a/src/adcp/types/generated_poc/creative/sync_creatives_response.py +++ b/src/adcp/types/generated_poc/creative/sync_creatives_response.py @@ -41,7 +41,7 @@ class Creative(AdcpVersionEnvelope): assignment_errors: dict[Annotated[str, StringConstraints(pattern='^[a-zA-Z0-9_-]+$')], str] | None = None -class SyncCreativesResponse1(AdcpVersionEnvelope): +class SyncCreativesResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') dry_run: bool | None = None creatives: list[Creative] @@ -50,7 +50,7 @@ class SyncCreativesResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncCreativesResponse2(AdcpVersionEnvelope): +class SyncCreativesResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/build_creative_response.py b/src/adcp/types/generated_poc/media_buy/build_creative_response.py index f15b4f518..05c595c51 100644 --- a/src/adcp/types/generated_poc/media_buy/build_creative_response.py +++ b/src/adcp/types/generated_poc/media_buy/build_creative_response.py @@ -132,7 +132,7 @@ class Estimate(AdcpVersionEnvelope): per_leaf: list[PerLeaf] | None = None -class BuildCreativeResponse1(AdcpVersionEnvelope): +class BuildCreativeResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') creative_manifest: creative_manifest_1.CreativeManifest build_variant_id: str | None = None @@ -149,14 +149,14 @@ class BuildCreativeResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class BuildCreativeResponse2(AdcpVersionEnvelope): +class BuildCreativeResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None ext: ext_1.ExtensionObject | None = None -class BuildCreativeResponse3(AdcpVersionEnvelope): +class BuildCreativeResponse3(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') creative_manifests: Annotated[list[creative_manifest_1.CreativeManifest], Field(min_length=1)] sandbox: bool | None = None @@ -171,7 +171,7 @@ class BuildCreativeResponse3(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class BuildCreativeResponse4(AdcpVersionEnvelope): +class BuildCreativeResponse4(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') creatives: Annotated[list[Creative], Field(min_length=1)] items_total: Annotated[int, Field(ge=0)] | None = None @@ -190,7 +190,7 @@ class BuildCreativeResponse4(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class BuildCreativeResponse5(AdcpVersionEnvelope): +class BuildCreativeResponse5(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') mode: Literal['estimate'] = 'estimate' estimate: Estimate diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py index 5ad3e2564..7e94a7f3a 100644 --- a/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py @@ -26,7 +26,7 @@ from adcp.types.media_buy_status_helpers import MEDIA_BUY_LEGACY_STATUS_VALUES, unwrap_enum_value -class CreateMediaBuyResponse1(AdcpVersionEnvelope): +class CreateMediaBuyResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') status: Literal['completed'] = 'completed' proposal_id: Annotated[str, StringConstraints(min_length=1)] | None = None @@ -77,7 +77,7 @@ def _normalize_legacy_status(cls, data: Any) -> Any: return data -class CreateMediaBuyResponse2(AdcpVersionEnvelope): +class CreateMediaBuyResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/log_event_response.py b/src/adcp/types/generated_poc/media_buy/log_event_response.py index e2119072d..6733e580f 100644 --- a/src/adcp/types/generated_poc/media_buy/log_event_response.py +++ b/src/adcp/types/generated_poc/media_buy/log_event_response.py @@ -12,6 +12,7 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope class PartialFailure(AdcpVersionEnvelope): @@ -21,7 +22,7 @@ class PartialFailure(AdcpVersionEnvelope): message: str -class LogEventResponse1(AdcpVersionEnvelope): +class LogEventResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') events_received: Annotated[int, Field(ge=0)] events_processed: Annotated[int, Field(ge=0)] @@ -33,7 +34,7 @@ class LogEventResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class LogEventResponse2(AdcpVersionEnvelope): +class LogEventResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py b/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py index fb80edf90..865af640b 100644 --- a/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +++ b/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py @@ -12,9 +12,10 @@ from ..core import context as context_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope -class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope): +class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') success: Literal[True] feedback_id: Annotated[str, StringConstraints(min_length=1)] | None = None @@ -27,7 +28,7 @@ class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class ProvidePerformanceFeedbackResponse2(AdcpVersionEnvelope): +class ProvidePerformanceFeedbackResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py b/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py index a27287e78..1e1f47c8c 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py +++ b/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py @@ -55,7 +55,7 @@ class Audience(AdcpVersionEnvelope): errors: list[error_1.Error] | None = None -class SyncAudiencesResponse1(AdcpVersionEnvelope): +class SyncAudiencesResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') audiences: list[Audience] sandbox: bool | None = None @@ -63,7 +63,7 @@ class SyncAudiencesResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncAudiencesResponse2(AdcpVersionEnvelope): +class SyncAudiencesResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py b/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py index 39fb3ef7c..e9295ef8c 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py +++ b/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py @@ -45,7 +45,7 @@ class Catalog(AdcpVersionEnvelope): warnings: list[str] | None = None -class SyncCatalogsResponse1(AdcpVersionEnvelope): +class SyncCatalogsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') status: Literal['completed'] | None = None dry_run: bool | None = None @@ -57,7 +57,7 @@ class SyncCatalogsResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncCatalogsResponse2(AdcpVersionEnvelope): +class SyncCatalogsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[Any], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py b/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py index 9331f2691..2a2294556 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py +++ b/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py @@ -14,6 +14,7 @@ from ..core import event_source_health as event_source_health_1 from ..core import event_surface as event_surface_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope from ..enums import action_source as action_source_1 from ..enums import event_type as event_type_1 @@ -42,7 +43,7 @@ class EventSource(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncEventSourcesResponse1(AdcpVersionEnvelope): +class SyncEventSourcesResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') event_sources: list[EventSource] sandbox: bool | None = None @@ -50,7 +51,7 @@ class SyncEventSourcesResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class SyncEventSourcesResponse2(AdcpVersionEnvelope): +class SyncEventSourcesResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py index d4e42d09f..4880fea45 100644 --- a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py @@ -26,7 +26,7 @@ from adcp.types.media_buy_status_helpers import MEDIA_BUY_LEGACY_STATUS_VALUES, unwrap_enum_value -class UpdateMediaBuyResponse1(AdcpVersionEnvelope): +class UpdateMediaBuyResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') status: Literal['completed'] = 'completed' media_buy_id: str @@ -73,7 +73,7 @@ def _normalize_legacy_status(cls, data: Any) -> Any: return data -class UpdateMediaBuyResponse2(AdcpVersionEnvelope): +class UpdateMediaBuyResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/signals/activate_signal_response.py b/src/adcp/types/generated_poc/signals/activate_signal_response.py index 8c6ee7abb..4fc188c45 100644 --- a/src/adcp/types/generated_poc/signals/activate_signal_response.py +++ b/src/adcp/types/generated_poc/signals/activate_signal_response.py @@ -13,9 +13,10 @@ from ..core import deployment as deployment_1 from ..core import error as error_1 from ..core import ext as ext_1 +from ..core.protocol_envelope import ProtocolEnvelope -class ActivateSignalResponse1(AdcpVersionEnvelope): +class ActivateSignalResponse1(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') deployments: list[deployment_1.Deployment] sandbox: bool | None = None @@ -23,7 +24,7 @@ class ActivateSignalResponse1(AdcpVersionEnvelope): ext: ext_1.ExtensionObject | None = None -class ActivateSignalResponse2(AdcpVersionEnvelope): +class ActivateSignalResponse2(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict(extra='allow') errors: Annotated[list[error_1.Error], Field(min_length=1)] context: context_1.ContextObject | None = None diff --git a/tests/test_code_generation.py b/tests/test_code_generation.py index 4b31084f4..50acc8cad 100644 --- a/tests/test_code_generation.py +++ b/tests/test_code_generation.py @@ -1634,3 +1634,171 @@ def test_generated_create_media_buy_success_matches_schema_nullability(): ) assert ast.unparse(confirmed_at.annotation) == "AwareDatetime | None" assert confirmed_at.value is None + + +def _write_response_arm_fixture( + tmp_path, + monkeypatch, + *, + root_all_of: list[dict], + arms: list[dict], +): + """Drive the response-arm emitter over a synthetic account response schema. + + Only ``account/sync-accounts-response.json`` is materialized, so every other + entry in ``response_specs`` is skipped for missing schema/target. + """ + import json + + from scripts import post_generate_fixes + + generated_dir = tmp_path / "generated_poc" + target = generated_dir / "account" / "sync_accounts_response.py" + target.parent.mkdir(parents=True) + target.write_text( + "# generated by datamodel-codegen:\n" + "# filename: account/sync_accounts_response.json\n\n" + "from __future__ import annotations\n\n" + "from ..core.version_envelope import AdcpVersionEnvelope\n\n\n" + "class SyncAccountsResponse(AdcpVersionEnvelope):\n" + " pass\n" + ) + + schema_dir = tmp_path / "schemas" + schema_path = schema_dir / "account" / "sync-accounts-response.json" + schema_path.parent.mkdir(parents=True) + schema_path.write_text( + json.dumps( + { + "title": "Sync Accounts Response", + "type": "object", + "allOf": root_all_of, + "oneOf": arms, + } + ) + ) + + monkeypatch.setattr(post_generate_fixes, "OUTPUT_DIR", generated_dir) + monkeypatch.setattr(post_generate_fixes, "SCHEMA_DIR", schema_dir) + post_generate_fixes.restore_response_variant_aliases() + source = target.read_text() + + # Regenerating from the same schema must be a no-op. + post_generate_fixes.restore_response_variant_aliases() + assert target.read_text() == source + compile(source, str(target), "exec") + return source + + +def _arm_bases(source: str, class_name: str) -> list[str]: + import ast + + node = next( + item + for item in ast.parse(source).body + if isinstance(item, ast.ClassDef) and item.name == class_name + ) + return [ast.unparse(base) for base in node.bases] + + +_SUCCESS_ARM = { + "type": "object", + "properties": {"accounts": {"type": "array", "items": {"type": "string"}}}, + "required": ["accounts"], +} +_ERROR_ARM = { + "type": "object", + "properties": {"errors": {"type": "array", "items": {"type": "string"}}}, + "required": ["errors"], +} +_SUBMITTED_ARM = { + "type": "object", + "properties": { + "status": {"const": "submitted"}, + "task_id": {"type": "string"}, + }, + "required": ["status", "task_id"], +} + + +@pytest.mark.parametrize( + "protocol_envelope_ref", + [ + "../core/protocol-envelope.json", + "/schemas/core/protocol-envelope.json", + "https://adcontextprotocol.org/schemas/3.2.0-rc.1/core/protocol-envelope.json", + ], + ids=["relative", "root-relative", "canonical-url"], +) +def test_root_all_of_protocol_envelope_reaches_every_one_of_arm( + tmp_path, monkeypatch, protocol_envelope_ref +): + """A root ``allOf`` applies to the whole document, so every arm inherits it. + + Regression test for #1136: only the ``status: submitted`` arm used to pick + up ``ProtocolEnvelope``, leaving ``status``/``replayed``/``task_id`` untyped + on the success and error arms of the same response. + """ + source = _write_response_arm_fixture( + tmp_path, + monkeypatch, + root_all_of=[ + {"$ref": "../core/version-envelope.json"}, + {"$ref": protocol_envelope_ref}, + ], + arms=[_SUCCESS_ARM, _ERROR_ARM], + ) + + assert "from ..core.protocol_envelope import ProtocolEnvelope" in source + for class_name in ("SyncAccountsResponse1", "SyncAccountsResponse2"): + assert _arm_bases(source, class_name) == ["AdcpVersionEnvelope", "ProtocolEnvelope"] + + +def test_nested_root_all_of_protocol_envelope_is_detected(tmp_path, monkeypatch): + """The root ``allOf`` walk descends into nested ``allOf`` groupings.""" + source = _write_response_arm_fixture( + tmp_path, + monkeypatch, + root_all_of=[ + {"allOf": [{"$ref": "../core/protocol-envelope.json"}]}, + ], + arms=[_SUCCESS_ARM, _ERROR_ARM], + ) + + for class_name in ("SyncAccountsResponse1", "SyncAccountsResponse2"): + assert _arm_bases(source, class_name) == ["AdcpVersionEnvelope", "ProtocolEnvelope"] + + +def test_arms_keep_version_envelope_only_without_root_protocol_envelope(tmp_path, monkeypatch): + """The base is attached from the root schema, not sprayed unconditionally. + + A response whose root does not compose the protocol envelope keeps the + plain version envelope on its ordinary arms; the ``submitted`` arm still + gets ``ProtocolEnvelope`` from its own task-envelope shape. + """ + source = _write_response_arm_fixture( + tmp_path, + monkeypatch, + root_all_of=[{"$ref": "../core/version-envelope.json"}], + arms=[_SUCCESS_ARM, _ERROR_ARM, _SUBMITTED_ARM], + ) + + assert _arm_bases(source, "SyncAccountsResponse1") == ["AdcpVersionEnvelope"] + assert _arm_bases(source, "SyncAccountsResponse2") == ["AdcpVersionEnvelope"] + assert _arm_bases(source, "SyncAccountsResponse3") == [ + "AdcpVersionEnvelope", + "ProtocolEnvelope", + ] + + +def test_unrelated_root_all_of_ref_does_not_attach_protocol_envelope(tmp_path, monkeypatch): + """Only ``core/protocol-envelope.json`` counts — not any ``core`` ref.""" + source = _write_response_arm_fixture( + tmp_path, + monkeypatch, + root_all_of=[{"$ref": "../core/push-notification-config.json"}], + arms=[_SUCCESS_ARM], + ) + + assert _arm_bases(source, "SyncAccountsResponse1") == ["AdcpVersionEnvelope"] + assert "from ..core.protocol_envelope import ProtocolEnvelope" not in source diff --git a/tests/test_decisioning_specialisms.py b/tests/test_decisioning_specialisms.py index 50aef8b2b..4fadcd313 100644 --- a/tests/test_decisioning_specialisms.py +++ b/tests/test_decisioning_specialisms.py @@ -507,18 +507,34 @@ def test_build_creative_response_includes_submitted_arm() -> None: """The spec now includes the task-submitted arm in build_creative responses.""" import typing - from adcp.types import LegacyBuildCreativeResponse + from adcp.types import GeneratedTaskStatus, LegacyBuildCreativeResponse arms = typing.get_args(LegacyBuildCreativeResponse) assert len(arms) > 0, "LegacyBuildCreativeResponse should be a Union of arms" + + # The root ``allOf`` composes core/protocol-envelope.json, so ``task_id`` + # and ``status`` are present on *every* arm. Mere presence therefore does + # not identify the submitted arm; the pinned ``Literal`` annotation and its + # matching default do. + assert all({"task_id", "status"}.issubset(arm.model_fields) for arm in arms) + submitted_arms = [ arm for arm in arms - if hasattr(arm, "model_fields") - and {"task_id", "status"}.issubset(set(arm.model_fields.keys())) + if arm.model_fields["status"].annotation + == typing.Literal[GeneratedTaskStatus.submitted] # type: ignore[valid-type] + and arm.model_fields["status"].default is GeneratedTaskStatus.submitted ] assert [arm.__name__ for arm in submitted_arms] == ["BuildCreativeResponse6"] + # Only the submitted arm's own schema branch makes ``task_id`` required; + # the inherited envelope field stays optional on the others. + submitted = submitted_arms[0] + assert submitted.model_fields["task_id"].is_required() + assert not any( + arm.model_fields["task_id"].is_required() for arm in arms if arm is not submitted + ) + # ---- CreativeAdServerPlatform ---- diff --git a/tests/test_protocol_envelope_inheritance.py b/tests/test_protocol_envelope_inheritance.py new file mode 100644 index 000000000..4b39b67ab --- /dev/null +++ b/tests/test_protocol_envelope_inheritance.py @@ -0,0 +1,325 @@ +"""Every response arm carries the root ``allOf`` protocol envelope (#1136). + +Each AdCP response schema composes ``core/protocol-envelope.json`` at its root +via ``allOf``, so ``status``, ``task_id``, ``message``, ``context_id``, +``replayed``, ``timestamp``, ``push_notification_config``, +``governance_context``, ``context``, ``payload`` and ``adcp_error`` are part of +every response's contract — on the success arm, the error arm and the submitted +arm alike. The code generator used to attach :class:`ProtocolEnvelope` only to +the ``status: submitted`` arm, leaving those fields untyped on the rest: a +seller that set one wrote a pydantic extra and a buyer that read one got an +``AttributeError``. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Literal, get_args, get_origin + +import pytest +from pydantic import BaseModel + +from adcp._version import _read_packaged_version +from adcp.types import ProtocolEnvelope, canonical_creative +from adcp.types import aliases as aliases_module +from adcp.types.generated_poc.enums.task_status import TaskStatus +from adcp.validation.version import resolve_bundle_key + +_ENVELOPE_FIELDS = frozenset(ProtocolEnvelope.model_fields) + + +def _schema_dir() -> Path: + return Path("schemas") / "cache" / resolve_bundle_key(_read_packaged_version()) + + +def _public_response_classes() -> list[tuple[str, type[BaseModel]]]: + """Public ``*SuccessResponse`` / ``*ErrorResponse`` aliases that are classes. + + Non-discriminated ``oneOf`` responses also expose bare union aliases, which + are ``types.UnionType`` rather than classes; those are out of scope here. + """ + found: list[tuple[str, type[BaseModel]]] = [] + for name in sorted(dir(aliases_module)): + if name.startswith("_") or not name.endswith(("SuccessResponse", "ErrorResponse")): + continue + obj = getattr(aliases_module, name) + if isinstance(obj, type) and issubclass(obj, BaseModel): + found.append((name, obj)) + return found + + +def test_public_response_aliases_inherit_protocol_envelope() -> None: + """The alias surface adopters import must carry the envelope.""" + classes = _public_response_classes() + assert len(classes) > 20, "expected the full public response-alias surface" + + missing = [name for name, obj in classes if not issubclass(obj, ProtocolEnvelope)] + assert missing == [], "response aliases missing the ProtocolEnvelope base: " + ", ".join( + missing + ) + + +def test_public_response_aliases_declare_every_envelope_field() -> None: + """Envelope fields are declared fields, never ``extra`` bags.""" + for name, obj in _public_response_classes(): + assert _ENVELOPE_FIELDS <= set(obj.model_fields), name + + +@pytest.mark.parametrize( + "relative", + [ + "account/get_account_financials_response.py", + "account/sync_accounts_response.py", + "brand/acquire_rights_response.py", + "brand/get_brand_identity_response.py", + "brand/get_rights_response.py", + "brand/update_rights_response.py", + "content_standards/calibrate_content_response.py", + "content_standards/get_content_standards_response.py", + "content_standards/get_media_buy_artifacts_response.py", + "content_standards/validate_content_delivery_response.py", + "creative/get_creative_features_response.py", + "creative/preview_creative_response.py", + "creative/sync_creatives_response.py", + "media_buy/build_creative_response.py", + "media_buy/create_media_buy_response.py", + "media_buy/log_event_response.py", + "media_buy/provide_performance_feedback_response.py", + "media_buy/sync_audiences_response.py", + "media_buy/sync_catalogs_response.py", + "media_buy/sync_event_sources_response.py", + "media_buy/update_media_buy_response.py", + "signals/activate_signal_response.py", + ], +) +def test_generated_arms_match_their_root_schema_composition(relative: str) -> None: + """Every emitted arm mirrors whether its ROOT schema composes the envelope.""" + import importlib + import re + + module_name = "adcp.types.generated_poc." + relative.removesuffix(".py").replace("/", ".") + module = importlib.import_module(module_name) + + schema_path = _schema_dir() / Path(relative).with_suffix(".json").as_posix().replace("_", "-") + schema = json.loads(schema_path.read_text()) + root_refs = [part.get("$ref", "") for part in schema.get("allOf", []) if isinstance(part, dict)] + root_composes_envelope = any(ref.endswith("core/protocol-envelope.json") for ref in root_refs) + assert root_composes_envelope, f"{relative}: fixture assumes a root protocol envelope" + + # ``__all__`` is emitted as ``[union_alias, *numbered_arms, *nested]``; the + # arms are exactly the ```` names, never the nested helper models. + base_name = module.__all__[0] + arm_pattern = re.compile(rf"{re.escape(base_name)}\d+$") + arms = [ + getattr(module, name) for name in module.__all__ if arm_pattern.fullmatch(name) is not None + ] + assert len(arms) == len(schema["oneOf"]), f"{relative}: arm count drifted from the schema" + for arm in arms: + assert issubclass(arm, ProtocolEnvelope), f"{relative}: {arm.__name__}" + + +def test_error_arm_can_carry_envelope_state() -> None: + """The error arm of a ``oneOf`` is an envelope too, not a bare payload.""" + from adcp.types.aliases import CreateMediaBuyErrorResponse + + error = CreateMediaBuyErrorResponse.model_validate( + { + "errors": [{"code": "INVALID_BUDGET", "message": "too low"}], + "status": "rejected", + "task_id": "task_1", + "replayed": True, + } + ) + assert error.status == "rejected" + assert error.task_id == "task_1" + assert error.replayed is True + assert error.model_extra == {} + + +def test_replayed_and_status_round_trip_as_declared_fields() -> None: + """Idempotency rule 4: a replayed response sets ``replayed`` on the envelope.""" + from adcp.types.aliases import SyncCreativesSuccessResponse + + response = SyncCreativesSuccessResponse.model_validate({"creatives": []}) + assert response.replayed is False + assert response.model_extra == {} + + response.replayed = True + dumped = response.model_dump(mode="json") + assert dumped["replayed"] is True + # ``status`` is required on every task response envelope and now serializes + # from a declared field rather than being dropped. + assert dumped["status"] == "completed" + + reparsed = SyncCreativesSuccessResponse.model_validate(dumped) + assert reparsed.replayed is True + assert reparsed.model_extra == {} + + +def test_envelope_state_used_to_land_in_extra() -> None: + """Pin the exact regression: envelope keys are not ``extra`` any more.""" + from adcp.types.aliases import LogEventSuccessResponse + + response = LogEventSuccessResponse.model_validate( + { + "events_received": 0, + "events_processed": 0, + "status": "completed", + "replayed": True, + "context_id": "ctx_1", + } + ) + assert response.model_extra == {} + assert response.context_id == "ctx_1" + + +# ---- canonical_creative dynamic clones ---- + + +_CANONICAL_RESPONSE_CLONES = ( + "GetProductsResponse", + "CreateMediaBuyResponse1", + "CreateMediaBuyResponse2", + "CreateMediaBuyResponse3", + "UpdateMediaBuyResponse1", + "UpdateMediaBuyResponse2", + "UpdateMediaBuyResponse3", + "ListCreativesResponse", + "GetMediaBuysResponse", + "GetMediaBuyDeliveryResponse", + "GetCreativeDeliveryResponse", +) + + +@pytest.mark.parametrize("name", _CANONICAL_RESPONSE_CLONES) +def test_canonical_clone_preserves_envelope_ancestry(name: str) -> None: + """``_canonical_clone`` rebuilds fields; it must keep the ancestry too.""" + model = getattr(canonical_creative, name) + assert issubclass(model, ProtocolEnvelope), name + assert _ENVELOPE_FIELDS <= set(model.model_fields), name + + +@pytest.mark.parametrize("name", [*_CANONICAL_RESPONSE_CLONES, "GetProductsRequest", "Product"]) +def test_canonical_clone_keeps_the_boundary_extra_policy(name: str) -> None: + """The envelope bases must not override ``CanonicalBoundaryModel``'s config. + + Pydantic merges ``model_config`` left to right across bases, so an envelope + listed after the boundary model would reinstate :class:`AdCPBaseModel`'s + ``extra="ignore"`` and silently drop caller-supplied extension keys. + """ + model = getattr(canonical_creative, name) + assert model.model_config["extra"] == "allow", name + + +def test_canonical_clone_still_round_trips_extension_keys() -> None: + """The config regression above is observable through an unknown key.""" + request = canonical_creative.GetProductsRequest.model_validate( + {"promoted_offering": "test", "buying_mode": "brief"} + ) + assert request.promoted_offering == "test" + + +def test_canonical_response_keeps_its_schema_pinned_status() -> None: + """Adding the envelope base must not relax an arm's pinned ``status``.""" + response = canonical_creative.UpdateMediaBuyResponse1.model_validate( + {"media_buy_id": "mb_1", "revision": 2} + ) + assert response.status == "completed" + assert ( + canonical_creative.UpdateMediaBuyResponse1.model_fields["status"].annotation + == Literal["completed"] + ) + + +def test_canonical_responses_construct_without_status() -> None: + """``status`` is defaulted at runtime, so construction must not demand it. + + The static half of this — that the stub's synthesized ``__init__`` agrees — + is pinned in ``tests/type_checks/response_envelope_fields.py``. + """ + listed = canonical_creative.ListCreativesResponse( + creatives=[], + query_summary={"total_matching": 0, "returned": 0}, + pagination={"has_more": False}, + ) + assert listed.status == TaskStatus.completed + + buys = canonical_creative.GetMediaBuysResponse(media_buys=[]) + assert buys.status == TaskStatus.completed + + accepted = canonical_creative.UpdateMediaBuyResponse3(task_id="task_2") + assert accepted.status == TaskStatus.submitted + + +def _stub_status_annotation(runtime_annotation: object) -> str: + """Spell a runtime ``status`` annotation the way the stub must declare it.""" + if runtime_annotation is TaskStatus: + return "TaskStatus" + if get_origin(runtime_annotation) is Literal: + (member,) = get_args(runtime_annotation) + if isinstance(member, TaskStatus): + return f"Literal[TaskStatus.{member.name}]" + return f"Literal['{member}']" + raise AssertionError(f"unhandled runtime status annotation: {runtime_annotation!r}") + + +def test_canonical_response_stub_status_matches_runtime() -> None: + """Every concrete canonical response re-declares its exact ``status``. + + ``_CanonicalResponseEnvelope`` relaxes ``status`` to ``Any`` so the arms' + ``Literal`` pins do not trip the Liskov check. That relaxation is a bridge, + not a public type: a concrete response that forgets to re-declare ``status`` + silently hands adopters ``Any``, which type-checks against anything and + hides the very narrowing this module is about. Pin stub and runtime + together so the bridge cannot leak. + """ + import ast + + stub_path = Path(canonical_creative.__file__).with_suffix(".pyi") + stub = ast.parse(stub_path.read_text()) + + declared: dict[str, str] = {} + for node in stub.body: + if not isinstance(node, ast.ClassDef): + continue + if not any( + isinstance(base, ast.Name) and base.id == "_CanonicalResponseEnvelope" + for base in node.bases + ): + continue + status = next( + ( + item + for item in node.body + if isinstance(item, ast.AnnAssign) + and isinstance(item.target, ast.Name) + and item.target.id == "status" + ), + None, + ) + assert status is not None, ( + f"{node.name} inherits the relaxed ``status: Any`` bridge without " + f"re-declaring its own — adopters would read ``Any``" + ) + declared[node.name] = ast.unparse(status.annotation) + + # Guard the other direction too: a newly added canonical response must show + # up in the stub rather than quietly skipping this check. + runtime_responses = { + name + for name in dir(canonical_creative) + if not name.startswith("_") + and isinstance(getattr(canonical_creative, name), type) + and issubclass(getattr(canonical_creative, name), ProtocolEnvelope) + # ``ProtocolEnvelope`` itself is imported into this namespace as a base; + # only the canonical clones built on it are in scope here. + and issubclass(getattr(canonical_creative, name), canonical_creative.CanonicalBoundaryModel) + } + assert declared.keys() == runtime_responses + + for name, stub_annotation in sorted(declared.items()): + runtime = getattr(canonical_creative, name).model_fields["status"] + assert stub_annotation == _stub_status_annotation(runtime.annotation), name + # The stub marks the field defaulted (``= ...``); the runtime agrees. + assert not runtime.is_required(), name diff --git a/tests/type_checks/response_envelope_fields.py b/tests/type_checks/response_envelope_fields.py new file mode 100644 index 000000000..c05b25a03 --- /dev/null +++ b/tests/type_checks/response_envelope_fields.py @@ -0,0 +1,119 @@ +"""Adopter pattern: read and write protocol-envelope state on a response arm. + +Every AdCP response schema composes ``core/protocol-envelope.json`` at its +root, so ``status``, ``task_id``, ``replayed`` and friends belong to every +response arm — the success arm, the error arm and the submitted arm alike. +Before #1136 the generator attached the envelope base only to the submitted +arm, so a seller setting ``response.replayed = True`` on any of the other 19 +success shapes wrote a pydantic *extra*, and a buyer reading it got an +``AttributeError``. Statically the attribute did not exist at all. + +This file pins the typed surface: the envelope fields must be visible to a type +checker on ordinary success and error arms, on the canonical-boundary clones, +and through ``ProtocolEnvelope`` as a common ancestor of the arms of one +``oneOf`` — which previously had no shared base below ``AdcpVersionEnvelope``. +""" + +from __future__ import annotations + +from typing import Literal + +from typing_extensions import assert_type + +from adcp.types import ( + CreateMediaBuyErrorResponse, + CreateMediaBuyResponse1, + GeneratedTaskStatus, + GetMediaBuysResponse, + GetProductsResponse, + ListCreativesResponse, + ProtocolEnvelope, + SyncCreativesResponse1, + UpdateMediaBuyResponse3, +) + + +def echo_envelope(response: ProtocolEnvelope) -> str | None: + """A boundary can take any response arm through the shared ancestor.""" + return response.task_id + + +# --- A plain success arm carries envelope state --- + +synced = SyncCreativesResponse1(creatives=[]) +synced.replayed = True +synced.task_id = "task_1" +synced.context_id = "ctx_1" + +replayed: bool | None = synced.replayed +task_id: str | None = synced.task_id +assert replayed is True +assert task_id == "task_1" +assert echo_envelope(synced) == "task_1" + +# --- The canonical-boundary clones keep their generated source's ancestry --- + +created = CreateMediaBuyResponse1( + media_buy_id="mb_1", + status="completed", + confirmed_at=None, + revision=1, + packages=[], +) +assert echo_envelope(created) is None + +# The error arm of the same ``oneOf`` is an envelope too, so both arms share a +# base a seller can annotate against. +rejected = CreateMediaBuyErrorResponse.model_validate( + {"errors": [{"code": "INVALID_BUDGET", "message": "too low"}]} +) +rejected.replayed = True +assert echo_envelope(rejected) is None + +# --- ``status`` keeps its exact runtime type on every concrete response --- +# +# The stub ancestor relaxes ``status`` to ``Any`` purely to make the arms' +# narrowing legal (see ``_CanonicalResponseEnvelope``). ``assert_type`` is what +# proves that relaxation does not leak: an annotation like ``pinned: str = +# created.status`` would pass silently from ``Any`` and prove nothing. + +# Pinned to the single synchronous outcome by the 3.2 schema arm. +assert_type(created.status, Literal["completed"]) + +# Ordinary arm — the wide envelope enum, no narrowing in the schema. +products = GetProductsResponse() +assert_type(products.status, GeneratedTaskStatus) + +# Async task-envelope arm — pinned to the submitted member. +submitted = UpdateMediaBuyResponse3(task_id="task_1") +assert_type(submitted.status, Literal[GeneratedTaskStatus.submitted]) + + +# The envelope's own view stays wide, which is what makes the arms' narrowing +# a genuine refinement rather than a redefinition. +def read_envelope_status(response: ProtocolEnvelope) -> GeneratedTaskStatus: + return response.status + + +assert read_envelope_status(created) == "completed" + +# --- ``status`` is defaulted, so construction must not demand it --- +# +# ``status`` has a default on every runtime response. If the stub declared it +# without ``= ...`` the synthesized ``__init__`` would require it, and these +# three plain constructions would fail to type-check even though the runtime +# model defaults the field. (``ListCreativesResponse`` also has required +# ``query_summary``/``pagination`` fields the stub does not enumerate, so only +# the ``status`` half of its signature is asserted here; the runtime +# construction is covered in tests/test_protocol_envelope_inheritance.py.) + +listed = ListCreativesResponse(creatives=[]) +assert_type(listed.status, GeneratedTaskStatus) + +buys = GetMediaBuysResponse(media_buys=[]) +assert_type(buys.status, GeneratedTaskStatus) +assert buys.status == "completed" + +accepted = UpdateMediaBuyResponse3(task_id="task_2") +assert_type(accepted.status, Literal[GeneratedTaskStatus.submitted]) +assert accepted.status == "submitted"