From 65dccc055a6cab401908b300a531c6d3a05292c5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 26 Aug 2026 10:00:56 +0100 Subject: [PATCH 1/3] fix(sdk): complete object union and structured error handling Fixes #1086 Fixes #1087 Fixes #1088 --- scripts/post_generate_fixes.py | 72 +++++++++++-- src/adcp/server/a2a_server.py | 41 ++++--- src/adcp/server/governance_enforcement.py | 6 +- src/adcp/server/responses.py | 3 +- src/adcp/server/serve.py | 17 +-- src/adcp/server/translate.py | 27 +++-- src/adcp/types/aliases.py | 2 +- .../account/get_account_financials_request.py | 2 +- .../get_account_financials_response.py | 2 +- .../account/list_accounts_request.py | 2 +- .../account/report_usage_request.py | 2 +- .../account/sync_accounts_request.py | 4 +- .../account/sync_governance_request.py | 2 +- .../brand/acquire_rights_request.py | 2 +- .../brand/update_rights_request.py | 2 +- .../collection/collection_list.py | 2 +- .../create_collection_list_request.py | 2 +- .../delete_collection_list_request.py | 2 +- .../collection/get_collection_list_request.py | 2 +- .../list_collection_lists_request.py | 2 +- .../update_collection_list_request.py | 2 +- .../get_media_buy_artifacts_request.py | 2 +- .../generated_poc/core/creative_filters.py | 2 +- .../types/generated_poc/core/postal_area.py | 4 +- .../generated_poc/core/tasks_get_request.py | 2 +- .../generated_poc/core/tasks_list_request.py | 2 +- .../creative/get_creative_delivery_request.py | 2 +- .../creative/get_creative_features_request.py | 2 +- .../creative/list_creative_formats_request.py | 2 +- .../creative/list_creatives_request.py | 2 +- .../creative/list_transformers_request.py | 2 +- .../creative/sync_creatives_request.py | 2 +- .../creative/validate_input_request.py | 2 +- .../media_buy/build_creative_request.py | 2 +- .../media_buy/create_media_buy_request.py | 2 +- .../get_media_buy_delivery_request.py | 2 +- .../media_buy/get_media_buys_request.py | 2 +- .../media_buy/get_products_request.py | 2 +- .../legacy_purchase_continuation_input.py | 2 +- .../media_buy/sync_audiences_request.py | 2 +- .../media_buy/sync_catalogs_request.py | 2 +- .../media_buy/sync_event_sources_request.py | 2 +- .../media_buy/update_media_buy_request.py | 2 +- .../property/create_property_list_request.py | 2 +- .../property/delete_property_list_request.py | 2 +- .../property/get_property_list_request.py | 2 +- .../property/list_property_lists_request.py | 2 +- .../generated_poc/property/property_list.py | 2 +- .../property/update_property_list_request.py | 2 +- .../validate_property_delivery_request.py | 2 +- .../protocol/get_task_status_request.py | 2 +- .../protocol/list_tasks_request.py | 2 +- .../signals/activate_signal_request.py | 2 +- .../signals/get_signals_request.py | 2 +- tests/test_a2a_structured_error.py | 22 ++++ .../test_account_reference_request_fields.py | 101 ++++++++++++++++++ tests/test_code_generation.py | 43 ++++++++ tests/test_mcp_structured_error.py | 31 ++++++ tests/test_postal_area_compat.py | 22 ++++ 59 files changed, 391 insertions(+), 94 deletions(-) create mode 100644 tests/test_account_reference_request_fields.py diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index 844c7c98b..b4942e51b 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -1536,6 +1536,68 @@ def _intersection_field(field: str, keep_base: str, all_bases: list[str]) -> str print(" No allOf-merge field override conflicts found") +def expose_account_reference_union_fields() -> None: + """Replace generated AccountReference wrappers with their concrete arms. + + ``AccountReference`` is public as a composable object-union alias, but + datamodel-codegen still annotates every schema reference with its outer + ``RootModel`` class. Rewrite those generated annotations at the source so + request, nested-input, response, and canonical-clone paths all expose the + same concrete arm types without import-time Pydantic patching. + """ + pattern = re.compile(r"\b(account_ref(?:_\d+)?)\.AccountReference\b(?![12])") + total_files = 0 + total_fields = 0 + + for py_file in sorted(OUTPUT_DIR.rglob("*.py")): + source = py_file.read_text() + fixed, replacements = pattern.subn( + r"\1.AccountReference1 | \1.AccountReference2", + source, + ) + if not replacements: + continue + py_file.write_text(fixed) + total_files += 1 + total_fields += replacements + + if total_fields: + print( + f" Exposed AccountReference union arms in {total_fields} field(s) " + f"across {total_files} file(s)" + ) + else: + print(" AccountReference field annotations already expose concrete arms") + + +def fix_postal_union_arm_order() -> None: + """Prefer the legacy postal arm when a payload omits ``country``. + + The generated native arm contains country-specific models whose ``country`` + fields have defaults. When that arm appears first, a legacy payload such as + ``{"system": "us_zip", ...}`` is accepted as native and serializes with an + injected ``country`` that is incompatible with the retained fused system. + The legacy arm forbids extra fields, so putting it first is safe: native + payloads with ``country`` fall through to the native arm. + """ + target = OUTPUT_DIR / "core" / "postal_area.py" + if not target.exists(): + print(" postal area model not found (skipping arm-order fix)") + return + + source = target.read_text() + old = "PostalArea1 | PostalArea2" + new = "PostalArea2 | PostalArea1" + replacements = source.count(old) + if replacements: + target.write_text(source.replace(old, new)) + print(f" core/postal_area.py: reordered {replacements} postal union annotation(s)") + elif new in source: + print(" postal area union already prefers the legacy arm") + else: + raise RuntimeError("generated postal_area.py has an unexpected outer union shape") + + def fix_postal_country_system_pairing() -> None: """Restore postal country/system pairing dropped by model generation. @@ -4738,9 +4800,7 @@ def preserve_request_signing_operation_strings() -> None: OUTPUT_DIR / "bundled" / "protocol" / "get_adcp_capabilities_response.py", ) operation_item = "Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]" - item_model = re.compile( - r"list\[(?:RequiredForItem|WarnForItem|SupportedForItem)\d*\]" - ) + item_model = re.compile(r"list\[(?:RequiredForItem|WarnForItem|SupportedForItem)\d*\]") for target in targets: if not target.exists(): @@ -4751,9 +4811,7 @@ def preserve_request_signing_operation_strings() -> None: if class_start < 0 or class_end < 0: continue request_signing = source[class_start:class_end] - fixed_class, replacements = item_model.subn( - f"list[{operation_item}]", request_signing - ) + fixed_class, replacements = item_model.subn(f"list[{operation_item}]", request_signing) if replacements: fixed = source[:class_start] + fixed_class + source[class_end:] target.write_text(fixed) @@ -4780,6 +4838,8 @@ def main(): rewrite_response_list_to_sequence, fix_reuse_model_discriminator_bug, fix_allof_merge_field_override_conflicts, + expose_account_reference_union_fields, + fix_postal_union_arm_order, fix_postal_country_system_pairing, fix_adagents_duplicate_aliases, restore_format_category_deprecation_shim, diff --git a/src/adcp/server/a2a_server.py b/src/adcp/server/a2a_server.py index 5e3b12573..f4c6ce5f2 100644 --- a/src/adcp/server/a2a_server.py +++ b/src/adcp/server/a2a_server.py @@ -23,6 +23,7 @@ import os import warnings from contextvars import ContextVar +from functools import lru_cache from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 @@ -44,21 +45,6 @@ from adcp.server.base import ADCPHandler, ToolContext from adcp.server.helpers import ResponseEnhancer, _apply_response_enhancer -# Decisioning-layer ``AdcpError`` (from ``adcp.decisioning.types``) is the -# wire-shaped structured error platform methods raise. It is NOT a subclass -# of :class:`adcp.exceptions.ADCPError`; the executor must catch both so -# storyboards graded against decisioning adopters see the same structured -# envelope as MCP. Lazy import — ``adcp.decisioning`` pulls in the -# decisioning graph, which the A2A server module shouldn't load at import -# time. When the import fails (decisioning extra not installed), only the -# client-side ``ADCPError`` path is active. -try: - from adcp.decisioning.types import AdcpError as _DecisioningAdcpError -except Exception: # pragma: no cover - decisioning is an optional dep surface - _DECISIONING_ADCP_ERROR_TYPES: tuple[type[BaseException], ...] = () -else: - _DECISIONING_ADCP_ERROR_TYPES = (_DecisioningAdcpError,) - if TYPE_CHECKING: from collections.abc import Sequence @@ -143,6 +129,29 @@ async def agent_card_url(request: Request) -> str: from adcp.server.test_controller import TestControllerStore, _handle_test_controller logger = logging.getLogger(__name__) + + +@lru_cache(maxsize=1) +def _load_decisioning_adcp_error_types() -> tuple[type[BaseException], ...]: + """Load the decisioning error type after application imports settle.""" + from adcp.decisioning.types import AdcpError as DecisioningAdcpError + + return (DecisioningAdcpError,) + + +def _get_decisioning_adcp_error_types() -> tuple[type[BaseException], ...]: + """Return structured decisioning errors without caching import failures.""" + try: + return _load_decisioning_adcp_error_types() + except ImportError: + logger.warning( + "Unable to import the decisioning AdcpError type; " + "decisioning errors cannot be projected on A2A yet", + exc_info=True, + ) + return () + + _A2A_REQUEST_CONTEXT: ContextVar[Any | None] = ContextVar("adcp_a2a_request_context", default=None) _A2A_PARSED_REQUEST_SCOPE_KEY = "adcp.a2a_parsed_request" @@ -431,7 +440,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non # ``adcp_error`` envelope per transport-errors.mdx §A2A Binding. structured_error_types: tuple[type[BaseException], ...] = ( ADCPError, - *_DECISIONING_ADCP_ERROR_TYPES, + *_get_decisioning_adcp_error_types(), ) try: result = await self._dispatch_with_middleware(skill_name, params, tool_context) diff --git a/src/adcp/server/governance_enforcement.py b/src/adcp/server/governance_enforcement.py index f7dfe43e3..c143d84a5 100644 --- a/src/adcp/server/governance_enforcement.py +++ b/src/adcp/server/governance_enforcement.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from typing import Any, TypeAlias -from adcp.decisioning.errors import PermissionDeniedError from adcp.governance import ( GovernanceAuthorizationFailure, GovernanceAuthorizationSuccess, @@ -146,6 +145,11 @@ async def verify_then_call( if not result.ok: if on_rejected is not None: await _maybe_await(on_rejected(result, context)) + # Keep server package initialization independent of the + # decisioning graph; that graph imports webhook helpers + # which may already be mid-import in standalone consumers. + from adcp.decisioning.errors import PermissionDeniedError + raise PermissionDeniedError( message="Governance authorization rejected.", field="governance_context", diff --git a/src/adcp/server/responses.py b/src/adcp/server/responses.py index d5c8ba564..83cb135c4 100644 --- a/src/adcp/server/responses.py +++ b/src/adcp/server/responses.py @@ -27,7 +27,6 @@ async def get_products(): from typing import Any from adcp._version import ADCP_MAJOR_VERSION, get_supported_adcp_versions -from adcp.decisioning.account_projection import strip_credentials_from_wire_result from adcp.server.helpers import valid_actions_for_status from adcp.types.canonical_creative import Format, strip_legacy_creative_identity @@ -184,6 +183,8 @@ def _strip_write_only_fields(value: Any) -> Any: helper normalizes nested Pydantic models before recursing and does not mutate the caller's value. """ + from adcp.decisioning.account_projection import strip_credentials_from_wire_result + return strip_credentials_from_wire_result("sync_accounts", value) diff --git a/src/adcp/server/serve.py b/src/adcp/server/serve.py index 4b48e797e..618d260d9 100644 --- a/src/adcp/server/serve.py +++ b/src/adcp/server/serve.py @@ -2735,17 +2735,10 @@ def _register_tool( from pydantic import ConfigDict from adcp.exceptions import ADCPError - from adcp.server.translate import build_mcp_error_result - - # Lazy import — decisioning is optional for non-platform handlers, - # but when present its ``AdcpError`` carries structured ``details`` - # (caused_by, validation_errors) that need to reach the wire. - try: - from adcp.decisioning.types import AdcpError as DecisioningAdcpError # noqa: N813 - except Exception: - decisioning_error_types: tuple[type[BaseException], ...] = () - else: - decisioning_error_types = (DecisioningAdcpError,) + from adcp.server.translate import ( + _get_decisioning_adcp_error_types, + build_mcp_error_result, + ) async def fn(**kwargs: Any) -> dict[str, Any]: # Caller identity: FastMCP does not expose an authenticated principal @@ -2824,7 +2817,7 @@ async def _call_handler() -> Any: # ``adcp.exceptions.ADCPError`` (different class hierarchy # — ``adcp.decisioning.types.AdcpError``). Catch it explicitly # and project the same structured envelope. - if isinstance(exc, decisioning_error_types): + if isinstance(exc, _get_decisioning_adcp_error_types()): return build_mcp_error_result( # type: ignore[return-value] exc, params=kwargs, diff --git a/src/adcp/server/translate.py b/src/adcp/server/translate.py index e2affc0f2..4e471bd94 100644 --- a/src/adcp/server/translate.py +++ b/src/adcp/server/translate.py @@ -26,6 +26,7 @@ from __future__ import annotations import json +from functools import lru_cache from typing import TYPE_CHECKING, Any, Literal, cast from urllib.parse import urlparse @@ -49,6 +50,23 @@ from adcp.types import Error from adcp.types.core import Protocol + +@lru_cache(maxsize=1) +def _load_decisioning_adcp_error_types() -> tuple[type[BaseException], ...]: + """Load the decisioning error type after application imports settle.""" + from adcp.decisioning.types import AdcpError as DecisioningAdcpError + + return (DecisioningAdcpError,) + + +def _get_decisioning_adcp_error_types() -> tuple[type[BaseException], ...]: + """Return decisioning error types without caching transient failures.""" + try: + return _load_decisioning_adcp_error_types() + except ImportError: + return () + + if TYPE_CHECKING: from adcp.server.base import ToolContext @@ -122,14 +140,7 @@ def _extract_structured_fields( Used by both ``translate_error`` and ``build_mcp_error_result`` so the field-extraction logic stays in one place. """ - # Lazy import — ``adcp.decisioning.types`` pulls in the decisioning - # graph, which translate.py shouldn't load at module-import time. - try: - from adcp.decisioning.types import AdcpError as DecisioningAdcpError # noqa: N813 - except Exception: - decisioning_error_types: tuple[type[BaseException], ...] = () - else: - decisioning_error_types = (DecisioningAdcpError,) + decisioning_error_types = _get_decisioning_adcp_error_types() field: str | None = None if isinstance(exc, Error): diff --git a/src/adcp/types/aliases.py b/src/adcp/types/aliases.py index 413e5411b..750565cee 100644 --- a/src/adcp/types/aliases.py +++ b/src/adcp/types/aliases.py @@ -595,7 +595,7 @@ def _generated_alias(name: str, fallback_name: str) -> Any: # of the generator's outer RootModel wrappers. They compose cleanly in adopter # annotations without imposing another wrapper around their constituent arms. PostalArea = _Annotated[ - PostalArea1 | PostalArea2, + PostalArea2 | PostalArea1, BeforeValidator(_g.PostalArea._validate_country_system_pairing), ] """Postal-area union; validate raw values with ``TypeAdapter(PostalArea)``.""" diff --git a/src/adcp/types/generated_poc/account/get_account_financials_request.py b/src/adcp/types/generated_poc/account/get_account_financials_request.py index 0f3cf2135..fcc1185de 100644 --- a/src/adcp/types/generated_poc/account/get_account_financials_request.py +++ b/src/adcp/types/generated_poc/account/get_account_financials_request.py @@ -20,7 +20,7 @@ class GetAccountFinancialsRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account to query financials for. Must be an operator-billed account.'), ] period: Annotated[ 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..af8e0cebd 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 @@ -56,7 +56,7 @@ class Invoice(AdcpVersionEnvelope): class GetAccountFinancialsResponse1(AdcpVersionEnvelope): model_config = ConfigDict(extra='allow') - account: account_ref_1.AccountReference + account: account_ref_1.AccountReference1 | account_ref_1.AccountReference2 currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] period: date_range_1.DateRange timezone: str diff --git a/src/adcp/types/generated_poc/account/list_accounts_request.py b/src/adcp/types/generated_poc/account/list_accounts_request.py index da0e4ae4a..bbdfe0d5b 100644 --- a/src/adcp/types/generated_poc/account/list_accounts_request.py +++ b/src/adcp/types/generated_poc/account/list_accounts_request.py @@ -30,7 +30,7 @@ class ListAccountsRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Optional exact account filter. Use `account_id` to retrieve one known seller/storefront account, or the complete natural key (`brand` + `operator` + optional `operator_unit`, fixed `currency`, buyer-selected account `timezone`, and `sandbox`) for buyer-declared accounts. When present, the seller returns only matching accounts visible to the authenticated caller.' ), diff --git a/src/adcp/types/generated_poc/account/report_usage_request.py b/src/adcp/types/generated_poc/account/report_usage_request.py index e548d0ed4..aab96bddd 100644 --- a/src/adcp/types/generated_poc/account/report_usage_request.py +++ b/src/adcp/types/generated_poc/account/report_usage_request.py @@ -21,7 +21,7 @@ class UsageItem(AdCPBaseModel): extra='allow', ) account: Annotated[ - account_ref.AccountReference, Field(description='Account for this usage record.') + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account for this usage record.') ] media_buy_id: Annotated[ str | None, diff --git a/src/adcp/types/generated_poc/account/sync_accounts_request.py b/src/adcp/types/generated_poc/account/sync_accounts_request.py index 4e248a766..46d671cae 100644 --- a/src/adcp/types/generated_poc/account/sync_accounts_request.py +++ b/src/adcp/types/generated_poc/account/sync_accounts_request.py @@ -26,7 +26,7 @@ class Accounts(AdCPBaseModel): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`.' ), @@ -127,7 +127,7 @@ class Accounts1(AdCPBaseModel): extra='allow', ) account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field( description='Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`.' ), diff --git a/src/adcp/types/generated_poc/account/sync_governance_request.py b/src/adcp/types/generated_poc/account/sync_governance_request.py index 37315e83b..5466fe207 100644 --- a/src/adcp/types/generated_poc/account/sync_governance_request.py +++ b/src/adcp/types/generated_poc/account/sync_governance_request.py @@ -42,7 +42,7 @@ class Account(AdCPBaseModel): extra='forbid', ) account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field( description='Account to sync governance agents for. Use account_id for account-id namespaces or brand + operator for buyer-declared accounts.' ), diff --git a/src/adcp/types/generated_poc/brand/acquire_rights_request.py b/src/adcp/types/generated_poc/brand/acquire_rights_request.py index 7f6fdb08e..ea6a901f2 100644 --- a/src/adcp/types/generated_poc/brand/acquire_rights_request.py +++ b/src/adcp/types/generated_poc/brand/acquire_rights_request.py @@ -94,7 +94,7 @@ class AcquireRightsRequest(AdcpVersionEnvelope): ] buyer: Annotated[brand_ref.BrandReference, Field(description="The buyer's brand identity")] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account context for this acquisition. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance. When both an inline governance_context token and a bound governance agent are present, the token is verified against that resolved relationship. An agent advertising adcp.governance_enforcement for acquire_rights MUST resolve an account and returns ACCOUNT_REQUIRED when neither the request nor an existing resource supplies one; it MUST NOT infer that a missing token means ungoverned. Legacy non-claiming agents may continue to treat omission of both fields as ungoverned during 3.x. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts.' ), diff --git a/src/adcp/types/generated_poc/brand/update_rights_request.py b/src/adcp/types/generated_poc/brand/update_rights_request.py index 63e8cef32..3bd865690 100644 --- a/src/adcp/types/generated_poc/brand/update_rights_request.py +++ b/src/adcp/types/generated_poc/brand/update_rights_request.py @@ -33,7 +33,7 @@ class UpdateRightsRequest(AdcpVersionEnvelope): str, Field(description='Rights grant identifier from acquire_rights response') ] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account context for this update. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance — update_rights is a modification-phase governance trigger (per `/docs/governance/campaign/specification#spend-commit-invocation`) and the brand agent consults the bound agent when computing the incremental commit delta. When both an inline governance_context token (on the protocol envelope) and a bound governance agent are present, the inline token wins. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. The estimated_impressions / commit-delta projection rule for governance-aware updates is tracked separately and not yet normative on this task.' ), diff --git a/src/adcp/types/generated_poc/collection/collection_list.py b/src/adcp/types/generated_poc/collection/collection_list.py index 8a36511d7..b86c17ee2 100644 --- a/src/adcp/types/generated_poc/collection/collection_list.py +++ b/src/adcp/types/generated_poc/collection/collection_list.py @@ -23,7 +23,7 @@ class CollectionList(AdCPBaseModel): None ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns this list. Returned as account_id form (seller-assigned identifier).' ), diff --git a/src/adcp/types/generated_poc/collection/create_collection_list_request.py b/src/adcp/types/generated_poc/collection/create_collection_list_request.py index 869a396f3..0f45c3083 100644 --- a/src/adcp/types/generated_poc/collection/create_collection_list_request.py +++ b/src/adcp/types/generated_poc/collection/create_collection_list_request.py @@ -20,7 +20,7 @@ class CreateCollectionListRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that will own the list. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. When omitted, this task applies its task-local single-account shortcut: if exactly one account is accessible to the authenticated caller, the seller may assign the list to that account; otherwise it MUST return an account-required or ambiguous-account error. Omission MUST NOT mean an undocumented credential-local default account.' ), diff --git a/src/adcp/types/generated_poc/collection/delete_collection_list_request.py b/src/adcp/types/generated_poc/collection/delete_collection_list_request.py index 343654f4c..a641efe18 100644 --- a/src/adcp/types/generated_poc/collection/delete_collection_list_request.py +++ b/src/adcp/types/generated_poc/collection/delete_collection_list_request.py @@ -20,7 +20,7 @@ class DeleteCollectionListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the collection list to delete')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/collection/get_collection_list_request.py b/src/adcp/types/generated_poc/collection/get_collection_list_request.py index 0c53ed74d..84cd47164 100644 --- a/src/adcp/types/generated_poc/collection/get_collection_list_request.py +++ b/src/adcp/types/generated_poc/collection/get_collection_list_request.py @@ -35,7 +35,7 @@ class GetCollectionListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the collection list to retrieve')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts and the list_id is not globally unique within that scope; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/collection/list_collection_lists_request.py b/src/adcp/types/generated_poc/collection/list_collection_lists_request.py index 52182aace..c0b4a19c7 100644 --- a/src/adcp/types/generated_poc/collection/list_collection_lists_request.py +++ b/src/adcp/types/generated_poc/collection/list_collection_lists_request.py @@ -20,7 +20,7 @@ class ListCollectionListsRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Filter to lists owned by this account. When omitted, returns lists across all accounts accessible to the authenticated agent.' ), diff --git a/src/adcp/types/generated_poc/collection/update_collection_list_request.py b/src/adcp/types/generated_poc/collection/update_collection_list_request.py index a5e29efee..6c98428e5 100644 --- a/src/adcp/types/generated_poc/collection/update_collection_list_request.py +++ b/src/adcp/types/generated_poc/collection/update_collection_list_request.py @@ -21,7 +21,7 @@ class UpdateCollectionListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the collection list to update')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_request.py b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_request.py index f75efe9d7..5158fb573 100644 --- a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_request.py +++ b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_request.py @@ -38,7 +38,7 @@ class Pagination(AdCPBaseModel): class GetMediaBuyArtifactsRequest(AdcpVersionEnvelope): account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Filter artifacts to a specific account. When omitted, returns artifacts across all accessible accounts.' ), diff --git a/src/adcp/types/generated_poc/core/creative_filters.py b/src/adcp/types/generated_poc/core/creative_filters.py index 60f2e89eb..0b09c25b6 100644 --- a/src/adcp/types/generated_poc/core/creative_filters.py +++ b/src/adcp/types/generated_poc/core/creative_filters.py @@ -20,7 +20,7 @@ class CreativeFilters(AdCPBaseModel): extra='allow', ) accounts: Annotated[ - list[account_ref.AccountReference] | None, + list[account_ref.AccountReference1 | account_ref.AccountReference2] | None, Field( description='Filter creatives by owning accounts. Useful for agencies managing multiple client accounts.', min_length=1, diff --git a/src/adcp/types/generated_poc/core/postal_area.py b/src/adcp/types/generated_poc/core/postal_area.py index 7b8ce3d3c..71f9c72f3 100644 --- a/src/adcp/types/generated_poc/core/postal_area.py +++ b/src/adcp/types/generated_poc/core/postal_area.py @@ -415,9 +415,9 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) -class PostalArea(RootModel[PostalArea1 | PostalArea2]): +class PostalArea(RootModel[PostalArea2 | PostalArea1]): root: Annotated[ - PostalArea1 | PostalArea2, + PostalArea2 | PostalArea1, Field( description='Postal area values. Prefer the native country + postal system form. Deprecated legacy country-fused postal-system tokens remain accepted for compatibility.', title='Postal Area', diff --git a/src/adcp/types/generated_poc/core/tasks_get_request.py b/src/adcp/types/generated_poc/core/tasks_get_request.py index 2ed2432b0..a6c0ac1a6 100644 --- a/src/adcp/types/generated_poc/core/tasks_get_request.py +++ b/src/adcp/types/generated_poc/core/tasks_get_request.py @@ -20,7 +20,7 @@ class TasksGetRequest(AdcpVersionEnvelope): ) task_id: Annotated[str, Field(description='Unique identifier of the task to retrieve')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account scope for the task lookup. Sellers MUST return REFERENCE_NOT_FOUND for a task_id that exists only under a different account or principal. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account.' ), diff --git a/src/adcp/types/generated_poc/core/tasks_list_request.py b/src/adcp/types/generated_poc/core/tasks_list_request.py index af36e7177..432a768dc 100644 --- a/src/adcp/types/generated_poc/core/tasks_list_request.py +++ b/src/adcp/types/generated_poc/core/tasks_list_request.py @@ -96,7 +96,7 @@ class TasksListRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description="Account scope for task reconciliation. Sellers MUST only return tasks created for the caller's authenticated account + principal pair. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account." ), diff --git a/src/adcp/types/generated_poc/creative/get_creative_delivery_request.py b/src/adcp/types/generated_poc/creative/get_creative_delivery_request.py index ab394ec09..305428d57 100644 --- a/src/adcp/types/generated_poc/creative/get_creative_delivery_request.py +++ b/src/adcp/types/generated_poc/creative/get_creative_delivery_request.py @@ -20,7 +20,7 @@ class GetCreativeDeliveryRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account for routing and scoping. Limits results to creatives within this account.' ), diff --git a/src/adcp/types/generated_poc/creative/get_creative_features_request.py b/src/adcp/types/generated_poc/creative/get_creative_features_request.py index ca893f0bd..7127e0161 100644 --- a/src/adcp/types/generated_poc/creative/get_creative_features_request.py +++ b/src/adcp/types/generated_poc/creative/get_creative_features_request.py @@ -33,7 +33,7 @@ class GetCreativeFeaturesRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account for billing this evaluation. Required when the governance agent charges per evaluation.' ), diff --git a/src/adcp/types/generated_poc/creative/list_creative_formats_request.py b/src/adcp/types/generated_poc/creative/list_creative_formats_request.py index 473dee1da..fe58e631c 100644 --- a/src/adcp/types/generated_poc/creative/list_creative_formats_request.py +++ b/src/adcp/types/generated_poc/creative/list_creative_formats_request.py @@ -129,7 +129,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpVersionEnvelope): ), ] = False account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( deprecated=True, description='**DEPRECATED for `list_creative_formats` in 3.1. Removed at 4.0.** Use the account-scoped `list_transformers` request instead. *Legacy 3.x behavior:* identifies the rate card used when `include_pricing` is true.', diff --git a/src/adcp/types/generated_poc/creative/list_creatives_request.py b/src/adcp/types/generated_poc/creative/list_creatives_request.py index e95cc52a2..8253b3c2b 100644 --- a/src/adcp/types/generated_poc/creative/list_creatives_request.py +++ b/src/adcp/types/generated_poc/creative/list_creatives_request.py @@ -126,7 +126,7 @@ class ListCreativesRequest(AdcpVersionEnvelope): ), ] = 50 account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description="Account reference for pricing and access. When provided with include_pricing, the agent returns pricing_options from this account's rate card on each creative." ), diff --git a/src/adcp/types/generated_poc/creative/list_transformers_request.py b/src/adcp/types/generated_poc/creative/list_transformers_request.py index 24bad43eb..bf055e827 100644 --- a/src/adcp/types/generated_poc/creative/list_transformers_request.py +++ b/src/adcp/types/generated_poc/creative/list_transformers_request.py @@ -100,7 +100,7 @@ class ListTransformersRequestCreativeAgent(AdcpVersionEnvelope): Field(description='Include `pricing_options` on each transformer. Requires `account`.'), ] = False account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account reference. Transformers are account-scoped — the returned set, the enumerable option values, and (with include_pricing) the rate card are all resolved for this credential.' ), diff --git a/src/adcp/types/generated_poc/creative/sync_creatives_request.py b/src/adcp/types/generated_poc/creative/sync_creatives_request.py index edbae663e..f55160077 100644 --- a/src/adcp/types/generated_poc/creative/sync_creatives_request.py +++ b/src/adcp/types/generated_poc/creative/sync_creatives_request.py @@ -349,7 +349,7 @@ class SyncCreativesRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference, Field(description='Account that owns these creatives.') + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account that owns these creatives.') ] creatives: Annotated[ list[Creative] | None, diff --git a/src/adcp/types/generated_poc/creative/validate_input_request.py b/src/adcp/types/generated_poc/creative/validate_input_request.py index 5afce1fb1..50f622610 100644 --- a/src/adcp/types/generated_poc/creative/validate_input_request.py +++ b/src/adcp/types/generated_poc/creative/validate_input_request.py @@ -66,7 +66,7 @@ class ValidateInputRequest(AdCPBaseModel): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Optional account scope for seller-specific product validation. Required by sellers that route product declarations by buyer account.' ), diff --git a/src/adcp/types/generated_poc/media_buy/build_creative_request.py b/src/adcp/types/generated_poc/media_buy/build_creative_request.py index 03d67353a..df5dd8ceb 100644 --- a/src/adcp/types/generated_poc/media_buy/build_creative_request.py +++ b/src/adcp/types/generated_poc/media_buy/build_creative_request.py @@ -373,7 +373,7 @@ class BuildCreativeRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account reference for pricing and billing. When present, the creative agent applies account-specific pricing from the rate card, records the build against the account for billing, and can enforce account-level quotas or entitlements. Required by creative agents that charge for their services.' ), diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py index 54cd50a68..c9ce5aa6c 100644 --- a/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py @@ -177,7 +177,7 @@ class CreateMediaBuyRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field( description='Account to bill for this media buy. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts.' ), diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py index cc662b78d..f1320198b 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py @@ -359,7 +359,7 @@ class GetMediaBuyDeliveryRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Filter delivery data to a specific account. When omitted, returns data across all accessible accounts.' ), diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buys_request.py b/src/adcp/types/generated_poc/media_buy/get_media_buys_request.py index fb6d22152..4c7669219 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buys_request.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buys_request.py @@ -31,7 +31,7 @@ class GetMediaBuysRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account to retrieve media buys for. When omitted, returns data across all accessible accounts.' ), diff --git a/src/adcp/types/generated_poc/media_buy/get_products_request.py b/src/adcp/types/generated_poc/media_buy/get_products_request.py index c3263aae3..84d121e70 100644 --- a/src/adcp/types/generated_poc/media_buy/get_products_request.py +++ b/src/adcp/types/generated_poc/media_buy/get_products_request.py @@ -208,7 +208,7 @@ class GetProductsRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description="Account for product lookup. Returns products with pricing specific to this account's rate card." ), diff --git a/src/adcp/types/generated_poc/media_buy/legacy_purchase_continuation_input.py b/src/adcp/types/generated_poc/media_buy/legacy_purchase_continuation_input.py index 9dfd54538..8fc54c4c0 100644 --- a/src/adcp/types/generated_poc/media_buy/legacy_purchase_continuation_input.py +++ b/src/adcp/types/generated_poc/media_buy/legacy_purchase_continuation_input.py @@ -42,7 +42,7 @@ class CompatibilityPurchaseCoordinatorInput(AdCPBaseModel): ), ] account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field( description='Account identity that must match the account bound into the continuation token.' ), diff --git a/src/adcp/types/generated_poc/media_buy/sync_audiences_request.py b/src/adcp/types/generated_poc/media_buy/sync_audiences_request.py index b0bed33c9..c149264ec 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_audiences_request.py +++ b/src/adcp/types/generated_poc/media_buy/sync_audiences_request.py @@ -98,7 +98,7 @@ class SyncAudiencesRequest(AdcpVersionEnvelope): ), ] account: Annotated[ - account_ref.AccountReference, Field(description='Account to manage audiences for.') + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account to manage audiences for.') ] audiences: Annotated[ list[Audience] | None, diff --git a/src/adcp/types/generated_poc/media_buy/sync_catalogs_request.py b/src/adcp/types/generated_poc/media_buy/sync_catalogs_request.py index d95a0d5b5..2772d8d9d 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_catalogs_request.py +++ b/src/adcp/types/generated_poc/media_buy/sync_catalogs_request.py @@ -35,7 +35,7 @@ class SyncCatalogsRequest(AdcpVersionEnvelope): ), ] account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Seller account containing these buyer-managed catalogs.'), ] catalogs: Annotated[ diff --git a/src/adcp/types/generated_poc/media_buy/sync_event_sources_request.py b/src/adcp/types/generated_poc/media_buy/sync_event_sources_request.py index 6b16d5732..b488d0b6e 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_event_sources_request.py +++ b/src/adcp/types/generated_poc/media_buy/sync_event_sources_request.py @@ -76,7 +76,7 @@ class SyncEventSourcesRequest(AdcpVersionEnvelope): ), ] account: Annotated[ - account_ref.AccountReference, Field(description='Account to configure event sources for.') + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account to configure event sources for.') ] event_sources: Annotated[ list[EventSource] | None, diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py index 57579cb57..d6c04689d 100644 --- a/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py @@ -45,7 +45,7 @@ class UpdateMediaBuyRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference, + account_ref.AccountReference1 | account_ref.AccountReference2, Field( description='Account that owns this media buy. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. Required for governance checks and account resolution.' ), diff --git a/src/adcp/types/generated_poc/property/create_property_list_request.py b/src/adcp/types/generated_poc/property/create_property_list_request.py index dc18097a0..3bc1deb56 100644 --- a/src/adcp/types/generated_poc/property/create_property_list_request.py +++ b/src/adcp/types/generated_poc/property/create_property_list_request.py @@ -20,7 +20,7 @@ class CreatePropertyListRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that will own the list. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. When omitted, this task applies its task-local single-account shortcut: if exactly one account is accessible to the authenticated caller, the seller may assign the list to that account; otherwise it MUST return an account-required or ambiguous-account error. Omission MUST NOT mean an undocumented credential-local default account.' ), diff --git a/src/adcp/types/generated_poc/property/delete_property_list_request.py b/src/adcp/types/generated_poc/property/delete_property_list_request.py index 7dff0619e..cfa19940b 100644 --- a/src/adcp/types/generated_poc/property/delete_property_list_request.py +++ b/src/adcp/types/generated_poc/property/delete_property_list_request.py @@ -20,7 +20,7 @@ class DeletePropertyListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the property list to delete')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/property/get_property_list_request.py b/src/adcp/types/generated_poc/property/get_property_list_request.py index 79d602b97..5ae6eeac5 100644 --- a/src/adcp/types/generated_poc/property/get_property_list_request.py +++ b/src/adcp/types/generated_poc/property/get_property_list_request.py @@ -35,7 +35,7 @@ class GetPropertyListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the property list to retrieve')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts and the list_id is not globally unique within that scope; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/property/list_property_lists_request.py b/src/adcp/types/generated_poc/property/list_property_lists_request.py index 8ed896d61..c2bc88ed5 100644 --- a/src/adcp/types/generated_poc/property/list_property_lists_request.py +++ b/src/adcp/types/generated_poc/property/list_property_lists_request.py @@ -20,7 +20,7 @@ class ListPropertyListsRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Filter to lists owned by this account. When omitted, returns lists across all accounts accessible to the authenticated agent.' ), diff --git a/src/adcp/types/generated_poc/property/property_list.py b/src/adcp/types/generated_poc/property/property_list.py index 6effde2ab..e9d0b28d1 100644 --- a/src/adcp/types/generated_poc/property/property_list.py +++ b/src/adcp/types/generated_poc/property/property_list.py @@ -23,7 +23,7 @@ class PropertyList(AdCPBaseModel): None ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns this list. Returned as account_id form (seller-assigned identifier).' ), diff --git a/src/adcp/types/generated_poc/property/update_property_list_request.py b/src/adcp/types/generated_poc/property/update_property_list_request.py index 7001be10e..7bb85ed04 100644 --- a/src/adcp/types/generated_poc/property/update_property_list_request.py +++ b/src/adcp/types/generated_poc/property/update_property_list_request.py @@ -21,7 +21,7 @@ class UpdatePropertyListRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the property list to update')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/property/validate_property_delivery_request.py b/src/adcp/types/generated_poc/property/validate_property_delivery_request.py index b0145a87d..c28772b26 100644 --- a/src/adcp/types/generated_poc/property/validate_property_delivery_request.py +++ b/src/adcp/types/generated_poc/property/validate_property_delivery_request.py @@ -21,7 +21,7 @@ class ValidatePropertyDeliveryRequest(AdcpVersionEnvelope): ) list_id: Annotated[str, Field(description='ID of the property list to validate against')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), diff --git a/src/adcp/types/generated_poc/protocol/get_task_status_request.py b/src/adcp/types/generated_poc/protocol/get_task_status_request.py index 8734c3a12..d2afe676f 100644 --- a/src/adcp/types/generated_poc/protocol/get_task_status_request.py +++ b/src/adcp/types/generated_poc/protocol/get_task_status_request.py @@ -20,7 +20,7 @@ class GetTaskStatusRequest(AdcpVersionEnvelope): ) task_id: Annotated[str, Field(description='Unique identifier of the task to retrieve')] account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account scope for the task lookup. Sellers MUST return REFERENCE_NOT_FOUND for a task_id that exists only under a different account or principal. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account.' ), diff --git a/src/adcp/types/generated_poc/protocol/list_tasks_request.py b/src/adcp/types/generated_poc/protocol/list_tasks_request.py index 2b09861ff..1bb75f5f7 100644 --- a/src/adcp/types/generated_poc/protocol/list_tasks_request.py +++ b/src/adcp/types/generated_poc/protocol/list_tasks_request.py @@ -96,7 +96,7 @@ class ListTasksRequest(AdcpVersionEnvelope): extra='allow', ) account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description="Account scope for task reconciliation. Sellers MUST only return tasks created for the caller's authenticated account + principal pair. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account." ), diff --git a/src/adcp/types/generated_poc/signals/activate_signal_request.py b/src/adcp/types/generated_poc/signals/activate_signal_request.py index 672850daa..075db8543 100644 --- a/src/adcp/types/generated_poc/signals/activate_signal_request.py +++ b/src/adcp/types/generated_poc/signals/activate_signal_request.py @@ -60,7 +60,7 @@ class ActivateSignalRequest(AdcpVersionEnvelope): ), ] = None account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description='Account for this activation. Associates with a commercial relationship established via sync_accounts.' ), diff --git a/src/adcp/types/generated_poc/signals/get_signals_request.py b/src/adcp/types/generated_poc/signals/get_signals_request.py index 2d2975525..6733361ad 100644 --- a/src/adcp/types/generated_poc/signals/get_signals_request.py +++ b/src/adcp/types/generated_poc/signals/get_signals_request.py @@ -76,7 +76,7 @@ class GetSignalsRequest(AdcpVersionEnvelope): ), ] = DiscoveryMode.brief account: Annotated[ - account_ref.AccountReference | None, + account_ref.AccountReference1 | account_ref.AccountReference2 | None, Field( description="Account for this request. When provided, the signals agent returns per-account pricing options if configured. In 'wholesale' mode, this is the rate-card scope: when omitted in wholesale mode, agents return their default rate-card pricing or omit pricing_options entirely." ), diff --git a/tests/test_a2a_structured_error.py b/tests/test_a2a_structured_error.py index bf5ef32f1..13bf2592b 100644 --- a/tests/test_a2a_structured_error.py +++ b/tests/test_a2a_structured_error.py @@ -20,6 +20,7 @@ from __future__ import annotations +import builtins from typing import Any import pytest @@ -97,6 +98,27 @@ def _adcp_error_data_part(task: pb.Task) -> dict[str, Any]: raise AssertionError("no adcp_error DataPart found on task artifacts") +def test_decisioning_error_import_retries_after_transient_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A circular-import failure must not disable structured errors forever.""" + from adcp.server import a2a_server + + a2a_server._load_decisioning_adcp_error_types.cache_clear() + real_import = builtins.__import__ + + def transient_import_error(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "adcp.decisioning.types": + raise ImportError("simulated circular import") + return real_import(name, *args, **kwargs) + + with monkeypatch.context() as import_patch: + import_patch.setattr(builtins, "__import__", transient_import_error) + assert a2a_server._get_decisioning_adcp_error_types() == () + + assert a2a_server._get_decisioning_adcp_error_types() == (DecisioningAdcpError,) + + # --------------------------------------------------------------------------- # Handlers that raise specific structured errors # --------------------------------------------------------------------------- diff --git a/tests/test_account_reference_request_fields.py b/tests/test_account_reference_request_fields.py new file mode 100644 index 000000000..d74dde012 --- /dev/null +++ b/tests/test_account_reference_request_fields.py @@ -0,0 +1,101 @@ +"""Regression tests for concrete AccountReference arms on request models.""" + +from __future__ import annotations + +import subprocess +import sys +from typing import get_args + +from adcp.types import ( + AccountReferenceById, + CompatibilityPurchaseCoordinatorInput, + GetProductsRequest, + ListCreativesRequest, + SyncAccountsRequest, +) +from adcp.types.generated_poc.core.account_ref import AccountReference as GeneratedAccountReference +from adcp.types.versioned import make_versioned_base + + +def _contains_generated_wrapper(annotation: object) -> bool: + return annotation is GeneratedAccountReference or any( + _contains_generated_wrapper(arg) for arg in get_args(annotation) + ) + + +def test_alias_first_import_order_keeps_concrete_request_arms() -> None: + code = """ +from adcp.types.aliases import AccountReferenceById +from adcp.types import GetProductsRequest +request = GetProductsRequest( + buying_mode="wholesale", + brief="test", + account={"account_id": "acc_123"}, +) +assert isinstance(request.account, AccountReferenceById) +""" + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + + +def test_get_products_account_returns_concrete_union_arm() -> None: + request = GetProductsRequest( + buying_mode="wholesale", + brief="test", + account={"account_id": "acc_123"}, + ) + + assert isinstance(request.account, AccountReferenceById) + assert not _contains_generated_wrapper(GetProductsRequest.model_fields["account"].annotation) + + +def test_nested_sync_accounts_account_returns_concrete_union_arm() -> None: + request = SyncAccountsRequest( + idempotency_key="sync-accounts-0001", + accounts=[ + { + "account": {"account_id": "acc_123"}, + } + ], + ) + + assert isinstance(request.accounts[0].account, AccountReferenceById) + + +def test_nested_creative_filter_accounts_return_concrete_union_arms() -> None: + request = ListCreativesRequest(filters={"accounts": [{"account_id": "acc_123"}]}) + + assert isinstance(request.filters.accounts[0], AccountReferenceById) + + +def test_compatibility_input_account_returns_concrete_union_arm() -> None: + request = CompatibilityPurchaseCoordinatorInput( + idempotency_key="12345678-1234-5678-1234-567812345678", + continuation_token="continuation-0001", + account={"account_id": "acc_123"}, + selected_product_ids=["product_1"], + accepted_losses=["feed_version_not_atomic", "pricing_version_not_atomic"], + legacy_create_request={"idempotency_key": "legacy"}, + ) + + assert isinstance(request.account, AccountReferenceById) + + +def test_versioned_request_account_returns_concrete_union_arm() -> None: + request_type = make_versioned_base("3.1", "GetProductsRequest") + request = request_type( + buying_mode="wholesale", + brief="test", + account={"account_id": "acc_123"}, + ) + + assert isinstance(request.account, AccountReferenceById) + assert not _contains_generated_wrapper(request_type.model_fields["account"].annotation) + + +def test_versioned_nested_account_returns_concrete_union_arm() -> None: + request_type = make_versioned_base("3.1", "ListCreativesRequest") + request = request_type(filters={"accounts": [{"account_id": "acc_123"}]}) + + assert isinstance(request.filters.accounts[0], AccountReferenceById) diff --git a/tests/test_code_generation.py b/tests/test_code_generation.py index 81642e162..fe00205a9 100644 --- a/tests/test_code_generation.py +++ b/tests/test_code_generation.py @@ -352,6 +352,49 @@ def test_post_generate_injects_postal_pairing_validator_idempotently(tmp_path, m assert "'US': ('zip', 'zip_plus_four')" in fixed +def test_post_generate_prefers_legacy_postal_union_arm_idempotently(tmp_path, monkeypatch): + from scripts import post_generate_fixes + + generated_dir = tmp_path / "generated_poc" + target = generated_dir / "core" / "postal_area.py" + target.parent.mkdir(parents=True) + target.write_text( + "from typing import Annotated\n" + "from pydantic import RootModel\n\n" + "class PostalArea(RootModel[PostalArea1 | PostalArea2]):\n" + " root: Annotated[PostalArea1 | PostalArea2, object()]\n" + ) + monkeypatch.setattr(post_generate_fixes, "OUTPUT_DIR", generated_dir) + + post_generate_fixes.fix_postal_union_arm_order() + post_generate_fixes.fix_postal_union_arm_order() + + fixed = target.read_text() + assert "PostalArea1 | PostalArea2" not in fixed + assert fixed.count("PostalArea2 | PostalArea1") == 2 + + +def test_post_generate_exposes_account_reference_union_fields_idempotently(tmp_path, monkeypatch): + from scripts import post_generate_fixes + + generated_dir = tmp_path / "generated_poc" + target = generated_dir / "sample_request.py" + target.parent.mkdir(parents=True) + target.write_text( + "account: account_ref.AccountReference | None\n" + "accounts: list[account_ref_1.AccountReference]\n" + ) + monkeypatch.setattr(post_generate_fixes, "OUTPUT_DIR", generated_dir) + + post_generate_fixes.expose_account_reference_union_fields() + post_generate_fixes.expose_account_reference_union_fields() + + assert target.read_text() == ( + "account: account_ref.AccountReference1 | account_ref.AccountReference2 | None\n" + "accounts: list[account_ref_1.AccountReference1 | account_ref_1.AccountReference2]\n" + ) + + def test_product_change_map_uses_valid_constrained_string_key_type(): """Constrained mapping keys must be valid for Pydantic and static type checkers.""" from adcp.types.generated_poc.core.product_change_map import ProductChangeMap diff --git a/tests/test_mcp_structured_error.py b/tests/test_mcp_structured_error.py index e21a628f5..453e242f5 100644 --- a/tests/test_mcp_structured_error.py +++ b/tests/test_mcp_structured_error.py @@ -9,6 +9,7 @@ from __future__ import annotations +import builtins from typing import Any import pytest @@ -198,6 +199,36 @@ async def caller(_kwargs: dict[str, Any], *, context: Any = None) -> Any: ) +@pytest.mark.asyncio +async def test_transient_decisioning_import_during_registration_is_retried( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from mcp.server import MCPServer + + from adcp.server import translate + from adcp.server.serve import _register_tool + + async def caller(_kwargs: dict[str, Any], *, context: Any = None) -> Any: + raise DecisioningAdcpError("INVALID_REQUEST", message="retry import") + + translate._load_decisioning_adcp_error_types.cache_clear() + real_import = builtins.__import__ + + def transient_import_error(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "adcp.decisioning.types": + raise ImportError("simulated circular import") + return real_import(name, *args, **kwargs) + + mcp = MCPServer("test-transient-decisioning-import") + with monkeypatch.context() as import_patch: + import_patch.setattr(builtins, "__import__", transient_import_error) + _register_tool(mcp, "test_tool", "test", {"type": "object"}, caller) + + result = await mcp.call_tool("test_tool", {}) + assert isinstance(result, CallToolResult) + assert result.structured_content["adcp_error"]["code"] == "INVALID_REQUEST" + + @pytest.mark.asyncio @pytest.mark.parametrize( "code,recovery", diff --git a/tests/test_postal_area_compat.py b/tests/test_postal_area_compat.py index 8606e00d3..0ef312891 100644 --- a/tests/test_postal_area_compat.py +++ b/tests/test_postal_area_compat.py @@ -135,6 +135,16 @@ def test_constructed_value_validates_against_postalarea_union_legacy_arm(): } +def test_raw_legacy_value_round_trips_through_postalarea_adapter(): + """Raw mappings must select the legacy arm before native defaults apply.""" + validated = _POSTAL_AREA_ADAPTER.validate_python({"system": "us_zip", "values": ["10001"]}) + + wire = _POSTAL_AREA_ADAPTER.dump_python(validated, mode="json") + assert wire == {"system": "us_zip", "values": ["10001"]} + reparsed = _POSTAL_AREA_ADAPTER.validate_python(wire) + assert _POSTAL_AREA_ADAPTER.dump_python(reparsed, mode="json") == wire + + def test_legacy_value_accepted_where_geo_postal_areas_used(): """Legacy postal areas are accepted in ``TargetingOverlay.geo_postal_areas``.""" geo_postal_area = _geo_postal_area_cls() @@ -153,6 +163,18 @@ def test_legacy_value_accepted_where_geo_postal_areas_used(): ] +def test_raw_legacy_value_round_trips_in_targeting_overlay(): + overlay = TargetingOverlay( + geo_postal_areas=[{"system": "us_zip", "values": ["10001"]}], + geo_postal_areas_exclude=[{"system": "gb_outward", "values": ["SW1A"]}], + ) + + wire = overlay.model_dump(mode="json", exclude_none=True) + assert wire["geo_postal_areas"] == [{"system": "us_zip", "values": ["10001"]}] + assert wire["geo_postal_areas_exclude"] == [{"system": "gb_outward", "values": ["SW1A"]}] + assert TargetingOverlay.model_validate(wire).model_dump(mode="json", exclude_none=True) == wire + + @pytest.mark.parametrize( ("country", "system"), [ From 57e2cc69fb40cb7d204cdb1d183b052d2bb03c94 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 26 Aug 2026 10:08:07 +0100 Subject: [PATCH 2/3] fix(types): derive generated account union arms --- scripts/post_generate_fixes.py | 48 ++++++++++++++++++++++++++++++++-- tests/test_code_generation.py | 15 ++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index b4942e51b..444b99967 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -1545,14 +1545,58 @@ def expose_account_reference_union_fields() -> None: request, nested-input, response, and canonical-clone paths all expose the same concrete arm types without import-time Pydantic patching. """ - pattern = re.compile(r"\b(account_ref(?:_\d+)?)\.AccountReference\b(?![12])") + account_ref_source = OUTPUT_DIR / "core" / "account_ref.py" + if not account_ref_source.exists(): + print(" account reference model not found (skipping union-field fix)") + return + + tree = ast.parse(account_ref_source.read_text()) + wrapper = next( + ( + node + for node in tree.body + if isinstance(node, ast.ClassDef) and node.name == "AccountReference" + ), + None, + ) + if wrapper is None: + raise RuntimeError("generated account_ref.py has no AccountReference wrapper") + + root_base = next( + ( + base + for base in wrapper.bases + if isinstance(base, ast.Subscript) + and isinstance(base.value, ast.Name) + and base.value.id == "RootModel" + ), + None, + ) + if root_base is None: + raise RuntimeError("generated AccountReference has no RootModel union base") + + def union_arm_names(node: ast.expr) -> list[str]: + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + return [*union_arm_names(node.left), *union_arm_names(node.right)] + if isinstance(node, ast.Name): + return [node.id] + raise RuntimeError( + "generated AccountReference has an unsupported union expression: " + f"{ast.unparse(node)}" + ) + + arm_names = union_arm_names(root_base.slice) + if len(arm_names) < 2 or len(set(arm_names)) != len(arm_names): + raise RuntimeError(f"generated AccountReference has invalid union arms: {arm_names!r}") + + pattern = re.compile(r"\b(account_ref(?:_\d+)?)\.AccountReference\b(?!\d)") total_files = 0 total_fields = 0 for py_file in sorted(OUTPUT_DIR.rglob("*.py")): source = py_file.read_text() fixed, replacements = pattern.subn( - r"\1.AccountReference1 | \1.AccountReference2", + lambda match: " | ".join(f"{match.group(1)}.{arm_name}" for arm_name in arm_names), source, ) if not replacements: diff --git a/tests/test_code_generation.py b/tests/test_code_generation.py index fe00205a9..042b88cac 100644 --- a/tests/test_code_generation.py +++ b/tests/test_code_generation.py @@ -378,8 +378,15 @@ def test_post_generate_exposes_account_reference_union_fields_idempotently(tmp_p from scripts import post_generate_fixes generated_dir = tmp_path / "generated_poc" + account_ref = generated_dir / "core" / "account_ref.py" + account_ref.parent.mkdir(parents=True) + account_ref.write_text( + "from pydantic import RootModel\n\n" + "class AccountReference(RootModel[" + "AccountReference1 | AccountReference2 | AccountReference3]):\n" + " pass\n" + ) target = generated_dir / "sample_request.py" - target.parent.mkdir(parents=True) target.write_text( "account: account_ref.AccountReference | None\n" "accounts: list[account_ref_1.AccountReference]\n" @@ -390,8 +397,10 @@ def test_post_generate_exposes_account_reference_union_fields_idempotently(tmp_p post_generate_fixes.expose_account_reference_union_fields() assert target.read_text() == ( - "account: account_ref.AccountReference1 | account_ref.AccountReference2 | None\n" - "accounts: list[account_ref_1.AccountReference1 | account_ref_1.AccountReference2]\n" + "account: account_ref.AccountReference1 | account_ref.AccountReference2 | " + "account_ref.AccountReference3 | None\n" + "accounts: list[account_ref_1.AccountReference1 | account_ref_1.AccountReference2 | " + "account_ref_1.AccountReference3]\n" ) From 4b95734f9ab537a73d4480eb49539bac4c544ea7 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 26 Aug 2026 10:23:09 +0100 Subject: [PATCH 3/3] test(types): assert concrete account references --- tests/test_catalog_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_catalog_types.py b/tests/test_catalog_types.py index 1391160ab..d6f42f490 100644 --- a/tests/test_catalog_types.py +++ b/tests/test_catalog_types.py @@ -180,7 +180,7 @@ def test_sync_catalogs_request_basic(): ], } ) - assert req.account.root.account_id == "acct_123" + assert req.account.account_id == "acct_123" assert req.catalogs is not None assert len(req.catalogs) == 1 @@ -192,7 +192,7 @@ def test_sync_catalogs_request_discovery_only(): req = SyncCatalogsRequest.model_validate( {"idempotency_key": "test-idempotency-key", "account": {"account_id": "acct_123"}} ) - assert req.account.root.account_id == "acct_123" + assert req.account.account_id == "acct_123" assert req.catalogs is None