diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fb114e9..50f7b8219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [4.0.0b1] — 2026-04-20 + +First 4.0 beta. See [MIGRATION_v3_to_v4.md](MIGRATION_v3_to_v4.md) for upgrade +instructions and before/after examples. + +### Breaking changes + +* **Removed types (no replacement stubs):** `BrandManifest`, `FormatCategory`, + `DeliverTo`, `PromotedProducts`, `PromotedOfferings`, `Pricing`, + `PackageStatus`. These were removed upstream from the AdCP spec. Inline any + dict payloads, or use the spec-current replacements (`BrandReference` for + `BrandManifest`; `MediaBuyStatus` for the former `PackageStatus`). +* **`ResolvedBrand.brand_manifest` field removed.** Use `ResolvedBrand.brand`. + The cross-populate validator that mirrored `brand` ↔ `brand_manifest` is gone. +* **`CreateMediaBuyRequest.brand_manifest` → `brand`**, matching the spec. The + request now takes a `BrandReference`, not an inline brand manifest dict. +* **Numbered discriminated-union classes renumbered.** `Assets5…Assets14` + from 3.x now correspond to higher-numbered variants (`Assets57…Assets149` + depending on the response). Import via semantic aliases from `adcp.types` + (e.g., `CreateMediaBuySuccessResponse`) instead of numbered classes. Raw + `Assets*` imports are unsupported — they are code-generation artifacts and + will shift again. + +### Non-breaking + +* `__version__` is now sourced from installed distribution metadata + (`importlib.metadata.version("adcp")`) so it always matches `pyproject.toml`. +* Added top-level re-exports: `TargetingOverlay`, `AdvertiserIndustry`, + `KellerType`, `BrandSource`. Any generated type not on the top-level + surface is available from `adcp.types` — `adcp.types.generated_poc.*` + is internal and not supported for direct import. + ## [3.12.0](https://github.com/adcontextprotocol/adcp-client-python/compare/v3.11.0...v3.12.0) (2026-04-16) diff --git a/MIGRATION_v3_to_v4.md b/MIGRATION_v3_to_v4.md new file mode 100644 index 000000000..888c50175 --- /dev/null +++ b/MIGRATION_v3_to_v4.md @@ -0,0 +1,219 @@ +# Migrating from v3.x to v4.0 + +4.0 realigns the SDK surface with the current AdCP spec. The schema redesign +removed several types and renamed fields; this guide lists each change with +before/after code. + +## Audit your exposure first + +```bash +grep -rnE "BrandManifest|FormatCategory|DeliverTo|PromotedProducts|PromotedOfferings|PackageStatus|from adcp import Pricing|\.brand_manifest|adcp\.types\.generated_poc" src/ +``` + +Each match is either an import that will now raise `ImportError`, an attribute +access that will raise `AttributeError`, or a coupling to a private module. + +Update your dependency pin: + +```toml +# pyproject.toml +[project] +dependencies = [ + "adcp>=4.0.0b1,<5", +] +``` + +## Removed types + +The following types were removed from the AdCP spec and have no replacement +stubs in the SDK. Imports will fail at runtime with `ImportError`. + +| Removed | Replacement | +| --- | --- | +| `BrandManifest` | `BrandReference(domain=...)` when constructing requests; inline dict when reading registry `brand` payloads | +| `FormatCategory` | Removed without replacement (previously an enum, now inferred from format metadata) | +| `DeliverTo` | Removed — use `publisher_properties` on the request | +| `PromotedProducts` / `PromotedOfferings` | Removed — use the `offerings` field shape from the current spec | +| `Pricing` | Use the discriminated `*PricingOption` types (e.g. `CpmFixedRatePricingOption`) | +| `PackageStatus` | Package status is now carried by `MediaBuyStatus`; per-package status was removed | + +### `BrandManifest` → `BrandReference` + +**Before (v3.x):** +```python +from adcp import CreateMediaBuyRequest, BrandManifest + +request = CreateMediaBuyRequest( + brand_manifest=BrandManifest( + name="Coffee Co", + brand_url="https://coffeeco.com", + logo_url="https://coffeeco.com/logo.png", + ), + packages=[...], + publisher_properties=..., +) +``` + +**After (v4.0):** +```python +from adcp import CreateMediaBuyRequest, BrandReference + +request = CreateMediaBuyRequest( + brand=BrandReference(domain="coffeeco.com"), + packages=[...], + publisher_properties=..., +) +``` + +The spec now resolves brand identity from `/.well-known/brand.json` at the +supplied domain, or from the AdCP registry. The SDK no longer models the +brand manifest as a typed class. + +### `FormatCategory` → removed + +The spec no longer models category as a separate enum. Read category +information from `Format` metadata instead. + +```python +# Before +from adcp import FormatCategory +if fmt.category == FormatCategory.video: ... + +# After — category info lives on Format itself +if fmt.type == "video": ... # or fmt.channel, depending on what you were matching +``` + +### `DeliverTo` → `publisher_properties` + +```python +# Before +request = CreateMediaBuyRequest(deliver_to=DeliverTo(...), ...) + +# After +request = CreateMediaBuyRequest( + publisher_properties=PublisherPropertiesAll(selection_type="all"), + ..., +) +``` + +### `PromotedProducts` / `PromotedOfferings` → `offerings` + +```python +# Before +request.promoted_offerings = PromotedOfferings(...) + +# After — pass the spec-current offerings shape as a dict/model +request.offerings = [...] +``` + +### `Pricing` → discriminated `*PricingOption` + +```python +# Before +from adcp import Pricing +pricing = Pricing(model="cpm", rate=5.0, currency="USD") + +# After — each pricing model has its own class +from adcp import CpmFixedRatePricingOption +pricing = CpmFixedRatePricingOption( + pricing_option_id="cpm_usd", + pricing_model="cpm", + is_fixed=True, + currency="USD", + rate=5.0, +) +``` + +### `PackageStatus` → `MediaBuyStatus` + +Per-package status was removed. Status now lives on the media buy. + +```python +# Before +if package.status == PackageStatus.active: ... + +# After +if media_buy.status == MediaBuyStatus.active: ... +``` + +### `ResolvedBrand.brand_manifest` field removed + +`RegistryClient.lookup_brand()` returns a `ResolvedBrand` whose +`brand_manifest` field and cross-populate validator are gone. + +**Before (v3.x):** +```python +result = await registry.lookup_brand("nike.com") +manifest = result.brand_manifest # Either `brand` or `brand_manifest` worked +``` + +**After (v4.0):** +```python +result = await registry.lookup_brand("nike.com") +manifest = result.brand # Only `.brand` +``` + +## Numbered discriminated-union classes shifted + +`datamodel-code-generator` numbers variant classes in the order they appear in +the upstream `oneOf`. When the spec reorders variants, the numbers shift. +Example: `Assets5`…`Assets14` in 3.x now correspond to higher-numbered +variants (`Assets57`…`Assets149`) across different response modules. + +**Don't** import numbered classes directly: + +```python +# Fragile — will break on the next spec revision: +from adcp.types.generated_poc.bundled.creative.build_creative_response import Assets9 +``` + +**Do** import the semantic alias from `adcp.types`: + +```python +from adcp.types import CreateMediaBuySuccessResponse, BuildCreativeSuccessResponse +``` + +Aliases for all discriminated-union success/error variants live in +`adcp/types/aliases.py`. If a variant you need isn't aliased, file an issue — +aliasing is the supported path; direct `Assets*` imports aren't. + +## Public vs. internal imports + +`adcp.types.generated_poc.*` is internal. Generated module paths and class +names can change with every schema regeneration. Import from `adcp.types` +instead. + +**Before:** +```python +from adcp.types.generated_poc.core.context import ContextObject +from adcp.types.generated_poc.core.targeting import TargetingOverlay +``` + +**After:** +```python +from adcp import ContextObject, TargetingOverlay +# or +from adcp.types import ContextObject, TargetingOverlay +``` + +4.0 adds top-level re-exports for `TargetingOverlay`, `AdvertiserIndustry`, +`KellerType`, and `BrandSource`. If you need a type that isn't on the +top-level surface, check `from adcp.types import X` first — most generated +types are re-exported there. + +## `__version__` now reflects the installed distribution + +`adcp.__version__` now reads from `importlib.metadata.version("adcp")` +instead of a hardcoded constant, so it always matches `pyproject.toml`. If +you're running from a source checkout without `pip install -e .`, you'll see +`"0.0.0+unknown"` — install the package (or run `pip install -e .` in CI) to +get the real version. If your test suite asserts on `__version__`, it will +need the same install step. + +## Watch for silent Pydantic field drops + +`CreateMediaBuyRequest` (and most other request models) accept extra fields +without erroring. Passing `brand_manifest=...` by keyword after upgrading +won't raise at construction — the field is silently dropped, and you'll see +the failure as a server-side rejection or missing `brand` at execution time. +The `grep` in the audit section above catches these. diff --git a/README.md b/README.md index bc6036368..59f630ac5 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ Full type hints with Pydantic validation and auto-generated types from the AdCP ```python from adcp import ( GetProductsRequest, - BrandManifest, + BrandReference, Package, CpmFixedRatePricingOption, MediaBuyStatus, @@ -297,11 +297,13 @@ if media_buy.status == MediaBuyStatus.active: ``` **Exported from main package:** -- **Core domain types**: `BrandManifest`, `Creative`, `CreativeManifest`, `MediaBuy`, `Package` -- **Status enums**: `CreativeStatus`, `MediaBuyStatus`, `PackageStatus`, `PricingModel` +- **Core domain types**: `BrandReference`, `Creative`, `CreativeManifest`, `MediaBuy`, `Package`, `PackageRequest`, `TargetingOverlay` +- **AdCP status enums**: `CreativeStatus`, `DeliveryStatus`, `MediaBuyStatus`, `PricingModel` - **All 9 pricing options**: `CpcPricingOption`, `CpmFixedRatePricingOption`, `VcpmAuctionPricingOption`, etc. - **Request/Response types**: All 16 operations with full request/response types +For types not on the top-level surface, import from `adcp.types` (e.g., `from adcp.types import AssetStatus`). If a type you need isn't in `adcp.types`, open an issue — we'll add an alias. The `adcp.types.generated_poc.*` modules are internal; class names and module paths shift on every schema regeneration and are not a supported API. + #### Semantic Type Aliases For discriminated union types (success/error responses), use semantic aliases for clearer code: @@ -332,7 +334,7 @@ See `examples/type_aliases_demo.py` for more examples. **Import guidelines:** - ✅ **DO**: Import from main package: `from adcp import GetProductsRequest` - ✅ **DO**: Use semantic aliases: `from adcp import CreateMediaBuySuccessResponse` -- ⚠️ **AVOID**: Import from internal modules: `from adcp.types._generated import CreateMediaBuyResponse1` +- ⚠️ **AVOID**: Import from `adcp.types.generated_poc.*` — paths and class names (including numbered `Assets*` variants) change on every schema regeneration. The main package exports provide a stable API while internal generated types may change. @@ -784,7 +786,7 @@ A typical media buy workflow involves discovering products, creating the buy, an ```python from adcp import ADCPClient, AgentConfig, GetProductsRequest, CreateMediaBuyRequest -from adcp import BrandManifest, PublisherPropertiesAll +from adcp import BrandReference, PublisherPropertiesAll # 1. Connect to agent config = AgentConfig(id="sales_agent", agent_uri="https://...", protocol="mcp") @@ -802,19 +804,14 @@ async with ADCPClient(config) as client: # 3. Create media buy reservation media_buy_result = await client.create_media_buy( CreateMediaBuyRequest( - brand_manifest=BrandManifest( - name="Coffee Co", - brand_url="https://coffeeco.com", - logo_url="https://coffeeco.com/logo.png", - # ... additional brand details - ), + brand=BrandReference(domain="coffeeco.com"), packages=[{ "package_id": product.packages[0].package_id, - "quantity": 1000000 # impressions + "quantity": 1000000, # impressions }], publisher_properties=PublisherPropertiesAll( - selection_type="all" # Target all authorized properties - ) + selection_type="all", # Target all authorized properties + ), ) ) @@ -901,8 +898,8 @@ async with ADCPClient(config) as client: Combine both workflows for a complete campaign setup: ```python -from adcp import ADCPMultiAgentClient, AgentConfig -from adcp import GetProductsRequest, CreateMediaBuyRequest, BuildCreativeRequest +from adcp import ADCPMultiAgentClient, AgentConfig, BrandReference, PublisherPropertiesAll +from adcp import BuildCreativeRequest, CreateMediaBuyRequest # Connect to both sales and creative agents async with ADCPMultiAgentClient( @@ -927,17 +924,17 @@ async with ADCPMultiAgentClient( creative_result = await creative_agent.build_creative( BuildCreativeRequest( manifest=creative_manifest, - target_format_id=formats.formats[0].format_id.id + target_format_id=formats.formats[0].format_id.id, ) ) # 4. Create media buy with creative media_buy_result = await sales_agent.create_media_buy( CreateMediaBuyRequest( - brand_manifest=brand_manifest, + brand=BrandReference(domain="coffeeco.com"), packages=[{"package_id": products.products[0].packages[0].package_id}], - publisher_properties=publisher_properties, - creative_urls=[creative_result.data.assets[0].url] + publisher_properties=PublisherPropertiesAll(selection_type="all"), + creative_urls=[creative_result.data.assets[0].url], ) ) diff --git a/scripts/consolidate_exports.py b/scripts/consolidate_exports.py index ed4dc434b..73456a99f 100644 --- a/scripts/consolidate_exports.py +++ b/scripts/consolidate_exports.py @@ -254,10 +254,10 @@ def _stem_matches_export(module_stem: str, export_name: str) -> bool: # Kept so existing code importing them continues to work. # Model stubs accept any payload (extra="allow"). # PromotedOfferingsRequirement is preserved as an Enum since it was one upstream. - # No backward-compat stubs. The 3.0.0-rc3 SDK surface matches the - # 3.0.0-rc3 spec. Removed types (BrandManifest, PromotedOfferings, - # DeliverTo, Pricing, FormatCategory, PackageStatus, etc.) are - # documented in the CHANGELOG migration guide. + # No backward-compat stubs. The SDK surface matches the spec directly. + # Removed types (BrandManifest, PromotedOfferings, DeliverTo, Pricing, + # FormatCategory, PackageStatus, etc.) are documented in + # MIGRATION_v3_to_v4.md. # Format __all__ list with proper line breaks (max 100 chars per line) # Exclude private names that are alias targets (internal intermediates only). diff --git a/scripts/regenerate_public_api_snapshot.py b/scripts/regenerate_public_api_snapshot.py new file mode 100755 index 000000000..99839d9af --- /dev/null +++ b/scripts/regenerate_public_api_snapshot.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Regenerate tests/fixtures/public_api_snapshot.json. + +Run after an intentional addition or removal in `adcp.__all__` or +`adcp.types.__all__`. The snapshot backs +`tests/test_public_api.py::test_public_api_surface_matches_snapshot`. + +Usage: + python scripts/regenerate_public_api_snapshot.py +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import adcp +import adcp.types + +SNAPSHOT_PATH = Path(__file__).parent.parent / "tests" / "fixtures" / "public_api_snapshot.json" + + +def main() -> None: + snapshot = { + "adcp": sorted(adcp.__all__), + "adcp.types": sorted(adcp.types.__all__), + } + SNAPSHOT_PATH.write_text(json.dumps(snapshot, indent=2) + "\n") + print(f"Wrote {SNAPSHOT_PATH}") + + +if __name__ == "__main__": + main() diff --git a/src/adcp/__init__.py b/src/adcp/__init__.py index fa177e5ef..8ef516c54 100644 --- a/src/adcp/__init__.py +++ b/src/adcp/__init__.py @@ -7,6 +7,9 @@ Supports both A2A and MCP protocols with full type safety. """ +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _pkg_version + from adcp.adagents import ( AuthorizationContext, domain_matches, @@ -87,12 +90,14 @@ # Audience & Targeting ActivateSignalRequest, ActivateSignalResponse, + AdvertiserIndustry, # Creative types ArtifactWebhookPayload, AssetContentType, AudienceSource, # Core domain types BrandReference, + BrandSource, # Creative Operations BuildCreativeRequest, BuildCreativeResponse, @@ -175,6 +180,7 @@ Gtin, IdentityMatchRequest, IdentityMatchResponse, + KellerType, # Account Operations ListAccountsRequest, ListAccountsResponse, @@ -240,6 +246,7 @@ SyncEventSourcesResponse, SyncPlansRequest, SyncPlansResponse, + TargetingOverlay, TimeBasedPricingOption, TimeUnit, Transform, @@ -430,7 +437,37 @@ sign_webhook, ) -__version__ = "3.12.0" +try: + __version__ = _pkg_version("adcp") +except PackageNotFoundError: + # Running from a source tree without an installed distribution. + __version__ = "0.0.0+unknown" + + +# Types removed in 4.0 — raise an informative ImportError instead of the +# default "cannot import name" traceback, and point at the migration guide. +# See MIGRATION_v3_to_v4.md. +_REMOVED_IN_V4 = { + "BrandManifest": ( + "use `BrandReference(domain=...)` on requests; read " + "`ResolvedBrand.brand` from the registry" + ), + "FormatCategory": "removed without replacement — format metadata carries category info", + "DeliverTo": "use `publisher_properties` on the request instead", + "PromotedProducts": "use the spec-current `offerings` shape", + "PromotedOfferings": "use the spec-current `offerings` shape", + "Pricing": "use the discriminated pricing classes (e.g. `CpmFixedRatePricingOption`)", + "PackageStatus": "package status moved onto `MediaBuyStatus`", +} + + +def __getattr__(name: str) -> object: + if name in _REMOVED_IN_V4: + raise ImportError( + f"`{name}` was removed in adcp 4.0: {_REMOVED_IN_V4[name]}. " + "See MIGRATION_v3_to_v4.md." + ) + raise AttributeError(f"module 'adcp' has no attribute {name!r}") def get_adcp_version() -> str: @@ -629,9 +666,11 @@ def get_adcp_version() -> str: "SignalCatalogType", # Core domain types (from stable API) "AccountScope", + "AdvertiserIndustry", "ArtifactWebhookPayload", "AudienceSource", "BrandReference", + "BrandSource", "BuyingMode", "CatalogGroupBinding", "ContextObject", @@ -649,6 +688,7 @@ def get_adcp_version() -> str: "ErrorCode", "EventType", "ExtensionObject", + "KellerType", "MediaBuy", "MediaBuyDeliveryStatus", "MediaBuyPackage", @@ -664,6 +704,7 @@ def get_adcp_version() -> str: "ReportPlanOutcomeResponse", "Snapshot", "SnapshotUnavailableReason", + "TargetingOverlay", "WcagLevel", # Status enums (for control flow) "CreativeStatus", diff --git a/src/adcp/client.py b/src/adcp/client.py index d0352b737..23a162d74 100644 --- a/src/adcp/client.py +++ b/src/adcp/client.py @@ -1175,7 +1175,7 @@ async def create_media_buy( Args: request: Media buy creation parameters including: - - brand_manifest: Advertiser brand information and creative assets + - brand: Brand reference; resolved from brand.json or the registry at execution - packages: List of package requests specifying desired inventory - publisher_properties: Target properties for ad placement - budget: Optional budget constraints @@ -1189,12 +1189,12 @@ async def create_media_buy( - Additional platform-specific metadata Example: - >>> from adcp import ADCPClient, CreateMediaBuyRequest + >>> from adcp import ADCPClient, CreateMediaBuyRequest, BrandReference >>> client = ADCPClient(agent_config) >>> request = CreateMediaBuyRequest( - ... brand_manifest=brand, + ... brand=BrandReference(domain="acme.com"), ... packages=[package_request], - ... publisher_properties=properties + ... publisher_properties=properties, ... ) >>> result = await client.create_media_buy(request) >>> if result.success: diff --git a/src/adcp/server/mcp_tools.py b/src/adcp/server/mcp_tools.py index 7a4771faa..7793b5467 100644 --- a/src/adcp/server/mcp_tools.py +++ b/src/adcp/server/mcp_tools.py @@ -563,7 +563,7 @@ "description": {"type": "string"}, "base_properties": {"type": "array"}, "filters": {"type": "object"}, - "brand_manifest": {"type": "object"}, + "brand": {"type": "object"}, }, "required": ["name"], }, @@ -605,7 +605,7 @@ "name": {"type": "string"}, "description": {"type": "string"}, "filters": {"type": "object"}, - "brand_manifest": {"type": "object"}, + "brand": {"type": "object"}, }, "required": ["list_id"], }, diff --git a/src/adcp/server/translate.py b/src/adcp/server/translate.py index c4091c3b0..4173f0eb3 100644 --- a/src/adcp/server/translate.py +++ b/src/adcp/server/translate.py @@ -95,8 +95,7 @@ def _build_error_data( data["details"] = details if errors: data["errors"] = [ - e.model_dump(exclude_none=True) if hasattr(e, "model_dump") else e - for e in errors + e.model_dump(exclude_none=True) if hasattr(e, "model_dump") else e for e in errors ] return data @@ -163,9 +162,12 @@ def translate_error( if proto == "mcp": return _to_mcp(code, message, suggestion=suggestion) return _to_a2a( - code, message, - recovery=recovery, suggestion=suggestion, - details=details, errors=errors, + code, + message, + recovery=recovery, + suggestion=suggestion, + details=details, + errors=errors, ) @@ -193,9 +195,12 @@ def _to_a2a( ) -> ServerError: """Format error as a ServerError for A2A servers.""" data = _build_error_data( - code, message, - recovery=recovery, suggestion=suggestion, - details=details, errors=errors, + code, + message, + recovery=recovery, + suggestion=suggestion, + details=details, + errors=errors, ) # Use InvalidParamsError for correctable errors (client can fix), @@ -242,6 +247,10 @@ def _normalize_brand_manifest(params: dict[str, Any]) -> None: Old format: ``brand_manifest: "https://example.com/brand.json"`` New format: ``brand: {domain: "example.com"}`` + + Kept as a wire-level shim so 3.x clients can keep talking to 4.x servers. + The field is removed from the SDK type system; only tool-boundary + translation accepts the legacy name. """ if "brand_manifest" not in params: return @@ -321,8 +330,7 @@ def normalize_request( # Package-level transforms (deep copy the packages list) if "packages" in result and isinstance(result["packages"], list): result["packages"] = [ - dict(pkg) if isinstance(pkg, dict) else pkg - for pkg in result["packages"] + dict(pkg) if isinstance(pkg, dict) else pkg for pkg in result["packages"] ] _normalize_packages(result) diff --git a/src/adcp/simple.py b/src/adcp/simple.py index 1e46cca53..5f5223044 100644 --- a/src/adcp/simple.py +++ b/src/adcp/simple.py @@ -129,7 +129,7 @@ async def get_products( For full control over error handling, use client.get_products() instead. Args: - **kwargs: Arguments for GetProductsRequest (brief, brand_manifest, etc.) + **kwargs: Arguments for GetProductsRequest (brief, brand, etc.) Returns: GetProductsResponse directly (no TaskResult wrapper) @@ -385,7 +385,7 @@ async def create_media_buy( Example: media_buy = await client.simple.create_media_buy( - brand_manifest=brand, + brand=brand, packages=[package_request], publisher_properties=properties ) diff --git a/src/adcp/types/__init__.py b/src/adcp/types/__init__.py index 68086eed0..3941ab5a0 100644 --- a/src/adcp/types/__init__.py +++ b/src/adcp/types/__init__.py @@ -52,6 +52,7 @@ AcquireRightsResponse, ActivateSignalRequest, ActivateSignalResponse, + AdvertiserIndustry, AggregatedTotals, Artifact, ArtifactWebhookPayload, @@ -213,6 +214,7 @@ Input, JavascriptAsset, JavascriptModuleType, + KellerType, LandingPageRequirement, ListAccountsRequest, ListAccountsResponse, @@ -550,6 +552,7 @@ is_update_media_buy_success, is_validate_content_delivery_success, ) +from adcp.types.registry import BrandSource # Semantic aliases for auto-generated field enum names ListCreativesField = Field1 @@ -824,8 +827,10 @@ def __init__(self, *args: object, **kwargs: object) -> None: "Asset", "AssetContentType", "AssetType", # Deprecated + "AdvertiserIndustry", "AudienceSource", "BrandReference", + "BrandSource", "BuyingMode", "ContextObject", "DateRange", @@ -866,6 +871,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: "FormatIdParameter", "Identifier", "Input", + "KellerType", "LandingPageRequirement", "Logo", "ListCreativesField", diff --git a/src/adcp/types/core.py b/src/adcp/types/core.py index 3c9a863cb..18a5f7ce9 100644 --- a/src/adcp/types/core.py +++ b/src/adcp/types/core.py @@ -5,7 +5,7 @@ from enum import Enum from typing import Any, Generic, Literal, TypeVar -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator class Protocol(str, Enum): @@ -199,21 +199,8 @@ class ResolvedBrand(BaseModel): house_name: str | None = None brand_agent_url: str | None = None brand: dict[str, Any] | None = None - brand_manifest: dict[str, Any] | None = None source: str - @model_validator(mode="before") - @classmethod - def _normalize_brand_fields(cls, data: Any) -> Any: - """Cross-populate brand and brand_manifest so both names are always accessible.""" - if isinstance(data, dict): - data = dict(data) - if "brand" in data and "brand_manifest" not in data: - data["brand_manifest"] = data["brand"] - elif "brand_manifest" in data and "brand" not in data: - data["brand"] = data["brand_manifest"] - return data - class Member(BaseModel): """An organization registered in the AAO member directory.""" diff --git a/src/adcp/types/registry.py b/src/adcp/types/registry.py index 20fcd2ef5..44e0df6c6 100644 --- a/src/adcp/types/registry.py +++ b/src/adcp/types/registry.py @@ -473,7 +473,7 @@ class ResolvedBrand(RegistryBaseModel): house_domain: str | None = None house_name: str | None = None brand_agent_url: str | None = None - brand_manifest: dict[str, Any] | None = None + brand: dict[str, Any] | None = None source: BrandSource diff --git a/tests/fixtures/public_api_snapshot.json b/tests/fixtures/public_api_snapshot.json new file mode 100644 index 000000000..d35d20065 --- /dev/null +++ b/tests/fixtures/public_api_snapshot.json @@ -0,0 +1,864 @@ +{ + "adcp": [ + "ADCPAuthenticationError", + "ADCPClient", + "ADCPConnectionError", + "ADCPError", + "ADCPFeatureUnsupportedError", + "ADCPMultiAgentClient", + "ADCPProtocolError", + "ADCPSigningRequiredError", + "ADCPTimeoutError", + "ADCPToolNotFoundError", + "ADCPWebhookError", + "ADCPWebhookSignatureError", + "AccountReference", + "AccountReferenceById", + "AccountReferenceByNaturalKey", + "AccountScope", + "AcquireRightsAcquiredResponse", + "AcquireRightsErrorResponse", + "AcquireRightsPendingResponse", + "AcquireRightsRejectedResponse", + "AcquireRightsRequest", + "AcquireRightsResponse", + "ActivateSignalErrorResponse", + "ActivateSignalRequest", + "ActivateSignalResponse", + "ActivateSignalSuccessResponse", + "AdagentsNotFoundError", + "AdagentsTimeoutError", + "AdagentsValidationError", + "AdvertiserIndustry", + "AgentCapabilities", + "AgentCompliance", + "AgentConfig", + "AgentDeployment", + "AgentDestination", + "AgentHealth", + "AgentStats", + "ArtifactWebhookPayload", + "AssetContentType", + "AudienceSource", + "AuthorizationContext", + "AuthorizedAgent", + "AuthorizedAgentsByInlineProperties", + "AuthorizedAgentsByPropertyId", + "AuthorizedAgentsByPropertyTag", + "AuthorizedAgentsByPublisherProperties", + "AuthorizedAgentsBySignalId", + "AuthorizedAgentsBySignalTag", + "BothPreviewRender", + "BrandActivity", + "BrandReference", + "BrandRegistryItem", + "BrandSource", + "BuildCreativeErrorResponse", + "BuildCreativeRequest", + "BuildCreativeResponse", + "BuildCreativeSuccessResponse", + "BuyingMode", + "CREATIVE_AGENT_CONFIG", + "CalibrateContentErrorResponse", + "CalibrateContentSuccessResponse", + "Catalog", + "CatalogAction", + "CatalogFieldBinding", + "CatalogFieldMapping", + "CatalogGroupBinding", + "CatalogItemStatus", + "CatalogRequirements", + "CatalogType", + "ChangeHandler", + "CheckGovernanceRequest", + "CheckGovernanceResponse", + "ComplyTestControllerRequest", + "ComplyTestControllerResponse", + "ConsentBasis", + "ContentIdType", + "ContextMatchRequest", + "ContextMatchResponse", + "ContextObject", + "CpaPricingOption", + "CpcPricingOption", + "CpcvPricingOption", + "CpmAuctionPricingOption", + "CpmFixedRatePricingOption", + "CpmPricingOption", + "CppPricingOption", + "CpvPricingOption", + "CreateContentStandardsErrorResponse", + "CreateContentStandardsSuccessResponse", + "CreateMediaBuyErrorResponse", + "CreateMediaBuyRequest", + "CreateMediaBuyResponse", + "CreateMediaBuySuccessResponse", + "Creative", + "CreativeApproval", + "CreativeApprovalStatus", + "CreativeFilters", + "CreativeManifest", + "CreativeStatus", + "CreativeVariant", + "CursorStore", + "DateRange", + "DatetimeRange", + "DeliveryStatus", + "Deployment", + "Destination", + "DevicePlatform", + "DeviceType", + "DomainLookupResult", + "Duration", + "Error", + "ErrorCode", + "EventType", + "ExtensionObject", + "FeatureResolver", + "FederatedAgentWithDetails", + "FederatedPublisher", + "FeedEvent", + "FeedFormat", + "FeedPage", + "FileCursorStore", + "FlatRatePricingOption", + "Format", + "FormatId", + "GeneratedTaskStatus", + "GetAccountFinancialsErrorResponse", + "GetAccountFinancialsRequest", + "GetAccountFinancialsResponse", + "GetAccountFinancialsSuccessResponse", + "GetBrandIdentityErrorResponse", + "GetBrandIdentityRequest", + "GetBrandIdentityResponse", + "GetBrandIdentitySuccessResponse", + "GetContentStandardsErrorResponse", + "GetContentStandardsSuccessResponse", + "GetCreativeDeliveryByBuyerRefRequest", + "GetCreativeDeliveryByCreativeRequest", + "GetCreativeDeliveryByMediaBuyRequest", + "GetCreativeDeliveryRequest", + "GetCreativeDeliveryResponse", + "GetCreativeFeaturesErrorResponse", + "GetCreativeFeaturesRequest", + "GetCreativeFeaturesResponse", + "GetCreativeFeaturesSuccessResponse", + "GetMediaBuyArtifactsErrorResponse", + "GetMediaBuyArtifactsSuccessResponse", + "GetMediaBuyDeliveryRequest", + "GetMediaBuyDeliveryResponse", + "GetMediaBuysRequest", + "GetMediaBuysResponse", + "GetPlanAuditLogsRequest", + "GetPlanAuditLogsResponse", + "GetProductsBriefRequest", + "GetProductsRefineRequest", + "GetProductsRequest", + "GetProductsResponse", + "GetProductsWholesaleRequest", + "GetRightsErrorResponse", + "GetRightsRequest", + "GetRightsResponse", + "GetRightsSuccessResponse", + "GetSignalsDiscoveryRequest", + "GetSignalsLookupRequest", + "GetSignalsRequest", + "GetSignalsResponse", + "Gtin", + "HtmlPreviewRender", + "IdentityMatchRequest", + "IdentityMatchResponse", + "InlineDaastAsset", + "InlineVastAsset", + "KellerType", + "KeyValueActivationKey", + "LegacyHmacFallback", + "ListAccountsRequest", + "ListAccountsResponse", + "ListContentStandardsErrorResponse", + "ListContentStandardsSuccessResponse", + "ListCreativeFormatsRequest", + "ListCreativeFormatsResponse", + "ListCreativesRequest", + "ListCreativesResponse", + "LogEventErrorResponse", + "LogEventRequest", + "LogEventResponse", + "LogEventSuccessResponse", + "McpWebhookPayload", + "MediaBuy", + "MediaBuyDeliveryStatus", + "MediaBuyPackage", + "MediaBuyStatus", + "MediaChannel", + "Member", + "MemoryBackend", + "OfferingAssetConstraint", + "OfferingAssetGroup", + "OptimizationGoal", + "Overlay", + "Package", + "PackageRequest", + "PaginationRequest", + "PlatformDeployment", + "PlatformDestination", + "Policy", + "PolicyExemplar", + "PolicyExemplars", + "PolicyHistory", + "PolicyRevision", + "PolicySummary", + "PreviewCreativeBatchResponse", + "PreviewCreativeInteractiveResponse", + "PreviewCreativeRequest", + "PreviewCreativeResponse", + "PreviewCreativeSingleResponse", + "PreviewCreativeStaticResponse", + "PreviewCreativeVariantResponse", + "PriceGuidance", + "PricingModel", + "PricingOption", + "Product", + "ProductFilters", + "Property", + "PropertyActivity", + "PropertyId", + "PropertyIdActivationKey", + "PropertyIdentifier", + "PropertyRegistry", + "PropertyRegistryItem", + "PropertySummary", + "PropertyTag", + "PropertyTagActivationKey", + "Proposal", + "Protocol", + "ProvidePerformanceFeedbackByBuyerRefRequest", + "ProvidePerformanceFeedbackByMediaBuyRequest", + "ProvidePerformanceFeedbackErrorResponse", + "ProvidePerformanceFeedbackRequest", + "ProvidePerformanceFeedbackResponse", + "ProvidePerformanceFeedbackSuccessResponse", + "PublisherProperties", + "PublisherPropertiesAll", + "PublisherPropertiesById", + "PublisherPropertiesByTag", + "PushNotificationConfig", + "Refine", + "RegistryClient", + "RegistryError", + "RegistrySync", + "ReportPlanOutcomeRequest", + "ReportPlanOutcomeResponse", + "ReportUsageRequest", + "ReportUsageResponse", + "ResolvedBrand", + "ResolvedProperty", + "SegmentIdActivationKey", + "SiSendActionResponseRequest", + "SiSendTextMessageRequest", + "SignalCatalogType", + "SignalFilters", + "SignalPricingOption", + "Snapshot", + "SnapshotUnavailableReason", + "SyncAccountsErrorResponse", + "SyncAccountsRequest", + "SyncAccountsResponse", + "SyncAccountsSuccessResponse", + "SyncAudiencesAudience", + "SyncAudiencesErrorResponse", + "SyncAudiencesRequest", + "SyncAudiencesResponse", + "SyncAudiencesSuccessResponse", + "SyncCatalogResult", + "SyncCatalogsErrorResponse", + "SyncCatalogsInputRequired", + "SyncCatalogsRequest", + "SyncCatalogsResponse", + "SyncCatalogsSubmitted", + "SyncCatalogsSuccessResponse", + "SyncCatalogsWorking", + "SyncCreativeResult", + "SyncCreativesErrorResponse", + "SyncCreativesRequest", + "SyncCreativesResponse", + "SyncCreativesSuccessResponse", + "SyncEventSourcesErrorResponse", + "SyncEventSourcesRequest", + "SyncEventSourcesResponse", + "SyncEventSourcesSuccessResponse", + "SyncPlansRequest", + "SyncPlansResponse", + "TEST_AGENT_A2A_CONFIG", + "TEST_AGENT_A2A_NO_AUTH_CONFIG", + "TEST_AGENT_MCP_CONFIG", + "TEST_AGENT_MCP_NO_AUTH_CONFIG", + "TEST_AGENT_TOKEN", + "TargetingOverlay", + "TaskResult", + "TaskStatus", + "TimeBasedPricingOption", + "TimeUnit", + "Transform", + "UpdateContentStandardsErrorResponse", + "UpdateContentStandardsSuccessResponse", + "UpdateFrequency", + "UpdateMediaBuyErrorResponse", + "UpdateMediaBuyPackagesRequest", + "UpdateMediaBuyPropertiesRequest", + "UpdateMediaBuyRequest", + "UpdateMediaBuyResponse", + "UpdateMediaBuySuccessResponse", + "UrlDaastAsset", + "UrlPreviewRender", + "UrlVastAsset", + "ValidateContentDeliveryErrorResponse", + "ValidateContentDeliverySuccessResponse", + "ValidationError", + "ValidationResult", + "VcpmAuctionPricingOption", + "VcpmFixedRatePricingOption", + "VcpmPricingOption", + "WcagLevel", + "WebhookDedupStore", + "WebhookMetadata", + "WebhookReceiver", + "WebhookReceiverConfig", + "WebhookVerifyOptions", + "aliases", + "create_a2a_webhook_payload", + "create_mcp_webhook_payload", + "create_test_agent", + "creative_agent", + "domain_matches", + "extract_webhook_result_data", + "fetch_adagents", + "fetch_agent_authorizations", + "generate_webhook_idempotency_key", + "generated", + "get_adcp_signed_headers_for_webhook", + "get_adcp_version", + "get_all_properties", + "get_all_tags", + "get_asset_count", + "get_format_assets", + "get_individual_assets", + "get_optional_assets", + "get_properties_by_agent", + "get_repeatable_groups", + "get_required_assets", + "has_assets", + "identifiers_match", + "normalize_assets_required", + "sign_legacy_webhook", + "sign_webhook", + "test_agent", + "test_agent_a2a", + "test_agent_a2a_no_auth", + "test_agent_client", + "test_agent_no_auth", + "uses_deprecated_assets_field", + "validate_adagents", + "validate_agent_authorization", + "validate_capabilities", + "validate_product", + "validate_publisher_properties_item", + "verify_agent_authorization", + "verify_agent_for_property" + ], + "adcp.types": [ + "A2UiComponent", + "A2UiSurface", + "Account", + "AccountReference", + "AccountReferenceById", + "AccountReferenceByNaturalKey", + "AccountScope", + "AcquireRightsAcquiredResponse", + "AcquireRightsErrorResponse", + "AcquireRightsPendingResponse", + "AcquireRightsRejectedResponse", + "AcquireRightsRequest", + "AcquireRightsResponse", + "Action", + "ActivateSignalErrorResponse", + "ActivateSignalRequest", + "ActivateSignalResponse", + "ActivateSignalSuccessResponse", + "AdvertiserIndustry", + "AgentConfig", + "AgentDeployment", + "AgentDestination", + "AggregatedTotals", + "Artifact", + "ArtifactWebhookPayload", + "Asset", + "AssetContentType", + "AssetType", + "AssignedPackage", + "Assignments", + "AudienceSource", + "AudioAsset", + "Authentication", + "AuthenticationScheme", + "AuthorizedAgent", + "AuthorizedAgents", + "AuthorizedAgentsByInlineProperties", + "AuthorizedAgentsByPropertyId", + "AuthorizedAgentsByPropertyTag", + "AuthorizedAgentsByPublisherProperties", + "AuthorizedAgentsBySignalId", + "AuthorizedAgentsBySignalTag", + "AvailableMetric", + "AvailablePackage", + "AvailableReportingFrequency", + "BothPreviewRender", + "BrandReference", + "BrandSource", + "BuildCreativeErrorResponse", + "BuildCreativeRequest", + "BuildCreativeResponse", + "BuildCreativeSuccessResponse", + "BuyingMode", + "ByCatalogItemItem", + "ByPackageItem", + "CalibrateContentErrorResponse", + "CalibrateContentRequest", + "CalibrateContentResponse", + "CalibrateContentSuccessResponse", + "Capability", + "Catalog", + "CatalogAction", + "CatalogFieldBinding", + "CatalogFieldBinding1", + "CatalogFieldMapping", + "CatalogGroupBinding", + "CatalogItemStatus", + "CatalogRequirements", + "CatalogType", + "CheckGovernanceRequest", + "CheckGovernanceResponse", + "CoBranding", + "CoBrandingRequirement", + "CollectionList", + "CollectionListChangedWebhook", + "CollectionListFilters", + "Colors", + "ComplyErrorResponse", + "ComplyListScenariosResponse", + "ComplySimulationResponse", + "ComplyStateTransitionResponse", + "ComplyTestControllerRequest", + "ComplyTestControllerResponse", + "ConsentBasis", + "Contact", + "ContentIdType", + "ContentStandards", + "ContextMatchRequest", + "ContextMatchResponse", + "ContextObject", + "Country", + "CpaPricingOption", + "CpcPricingOption", + "CpcvPricingOption", + "CpmAuctionPricingOption", + "CpmFixedRatePricingOption", + "CpmPricingOption", + "CppPricingOption", + "CpvPricingOption", + "CreateCollectionListRequest", + "CreateCollectionListResponse", + "CreateContentStandardsErrorResponse", + "CreateContentStandardsRequest", + "CreateContentStandardsResponse", + "CreateContentStandardsSuccessResponse", + "CreateMediaBuyErrorResponse", + "CreateMediaBuyRequest", + "CreateMediaBuyResponse", + "CreateMediaBuySuccessResponse", + "CreatePropertyListRequest", + "CreatePropertyListResponse", + "Creative", + "CreativeAction", + "CreativeAgent", + "CreativeAgentCapability", + "CreativeApproval", + "CreativeApprovalStatus", + "CreativeAsset", + "CreativeAssignment", + "CreativeFilters", + "CreativeManifest", + "CreativePolicy", + "CreativeStatus", + "CreativeVariant", + "CssAsset", + "DaastTrackingEvent", + "DaastVersion", + "DailyBreakdownItem", + "DateRange", + "DatetimeRange", + "DayOfWeek", + "DaypartTarget", + "DeleteCollectionListRequest", + "DeleteCollectionListResponse", + "DeletePropertyListRequest", + "DeletePropertyListResponse", + "DeliveryForecast", + "DeliveryMeasurement", + "DeliveryMetrics", + "DeliveryStatus", + "DeliveryType", + "DemographicSystem", + "Deployment", + "Destination", + "DevicePlatform", + "DeviceType", + "DimensionUnit", + "Disclaimer", + "DoohMetrics", + "Duration", + "Error", + "ErrorCode", + "EventType", + "ExtensionObject", + "FeedFormat", + "FeedbackSource", + "FieldModel", + "FlatRatePricingOption", + "Fonts", + "ForecastMethod", + "ForecastPoint", + "ForecastRange", + "ForecastRangeUnit", + "ForecastableMetric", + "Format", + "FormatCard", + "FormatCardDetailed", + "FormatId", + "FormatIdParameter", + "FrequencyCap", + "FrequencyCapScope", + "GeneratedTaskStatus", + "GeoCountry", + "GeoMetro", + "GeoPostalArea", + "GeoRegion", + "GetAccountFinancialsErrorResponse", + "GetAccountFinancialsRequest", + "GetAccountFinancialsResponse", + "GetAccountFinancialsSuccessResponse", + "GetAdcpCapabilitiesRequest", + "GetAdcpCapabilitiesResponse", + "GetBrandIdentityErrorResponse", + "GetBrandIdentityField", + "GetBrandIdentityRequest", + "GetBrandIdentityResponse", + "GetBrandIdentitySuccessResponse", + "GetCollectionListRequest", + "GetCollectionListResponse", + "GetContentStandardsErrorResponse", + "GetContentStandardsRequest", + "GetContentStandardsResponse", + "GetContentStandardsSuccessResponse", + "GetCreativeDeliveryByBuyerRefRequest", + "GetCreativeDeliveryByCreativeRequest", + "GetCreativeDeliveryByMediaBuyRequest", + "GetCreativeDeliveryRequest", + "GetCreativeDeliveryResponse", + "GetCreativeFeaturesErrorResponse", + "GetCreativeFeaturesRequest", + "GetCreativeFeaturesResponse", + "GetCreativeFeaturesSuccessResponse", + "GetMediaBuyArtifactsErrorResponse", + "GetMediaBuyArtifactsRequest", + "GetMediaBuyArtifactsResponse", + "GetMediaBuyArtifactsSuccessResponse", + "GetMediaBuyDeliveryRequest", + "GetMediaBuyDeliveryResponse", + "GetMediaBuysRequest", + "GetMediaBuysResponse", + "GetPlanAuditLogsRequest", + "GetPlanAuditLogsResponse", + "GetProductsBriefRequest", + "GetProductsField", + "GetProductsRefineRequest", + "GetProductsRequest", + "GetProductsResponse", + "GetProductsWholesaleRequest", + "GetPropertyListRequest", + "GetPropertyListResponse", + "GetRightsErrorResponse", + "GetRightsRequest", + "GetRightsResponse", + "GetRightsSuccessResponse", + "GetSignalsDiscoveryRequest", + "GetSignalsLookupRequest", + "GetSignalsRequest", + "GetSignalsResponse", + "Gtin", + "HtmlAsset", + "HtmlPreviewRender", + "HttpMethod", + "Identifier", + "IdentityMatchRequest", + "IdentityMatchResponse", + "ImageAsset", + "InlineDaastAsset", + "InlineVastAsset", + "Input", + "JavascriptAsset", + "JavascriptModuleType", + "KellerType", + "KeyValueActivationKey", + "LandingPage", + "LandingPageRequirement", + "ListAccountsRequest", + "ListAccountsResponse", + "ListCollectionListsRequest", + "ListCollectionListsResponse", + "ListContentStandardsErrorResponse", + "ListContentStandardsRequest", + "ListContentStandardsResponse", + "ListContentStandardsSuccessResponse", + "ListCreativeFormatsRequest", + "ListCreativeFormatsResponse", + "ListCreativesField", + "ListCreativesRequest", + "ListCreativesResponse", + "ListPropertyListsRequest", + "ListPropertyListsResponse", + "LogEventErrorResponse", + "LogEventRequest", + "LogEventResponse", + "LogEventSuccessResponse", + "Logo", + "MarkdownFlavor", + "McpWebhookPayload", + "MeasurementPeriod", + "MediaBuy", + "MediaBuyDelivery", + "MediaBuyDeliveryStatus", + "MediaBuyPackage", + "MediaBuyStatus", + "MediaChannel", + "MediaSubAsset", + "Member", + "Metadata", + "Method", + "MetricType", + "ModuleType", + "NotificationType", + "OfferPrice", + "Offering", + "OfferingAssetConstraint", + "OfferingAssetGroup", + "OptimizationGoal", + "OutcomeMeasurement", + "OutputFormat", + "Overlay", + "Pacing", + "Package", + "PackageRequest", + "PackageUpdate", + "Pagination", + "PaginationRequest", + "PaginationResponse", + "Parameters", + "Performance", + "PerformanceFeedback", + "Placement", + "PlatformDeployment", + "PlatformDestination", + "Policy", + "PolicyExemplar", + "PolicyExemplars", + "PolicyHistory", + "PolicyRevision", + "PolicySummary", + "Preview", + "PreviewCreativeBatchResponse", + "PreviewCreativeFormatRequest", + "PreviewCreativeInteractiveResponse", + "PreviewCreativeManifestRequest", + "PreviewCreativeRequest", + "PreviewCreativeResponse", + "PreviewCreativeSingleResponse", + "PreviewCreativeStaticResponse", + "PreviewCreativeVariantResponse", + "PreviewOutputFormat", + "PreviewRender", + "PriceGuidance", + "PricingModel", + "PricingOption", + "PrimaryCountry", + "Product", + "ProductCard", + "ProductCardDetailed", + "ProductCatalog", + "ProductFilters", + "Property", + "PropertyId", + "PropertyIdActivationKey", + "PropertyIdentifierTypes", + "PropertyList", + "PropertyListChangedWebhook", + "PropertyListFilters", + "PropertyListReference", + "PropertyTag", + "PropertyTagActivationKey", + "PropertyType", + "Proposal", + "Protocol", + "ProtocolEnvelope", + "ProtocolResponse", + "ProvidePerformanceFeedbackByBuyerRefRequest", + "ProvidePerformanceFeedbackByMediaBuyRequest", + "ProvidePerformanceFeedbackErrorResponse", + "ProvidePerformanceFeedbackRequest", + "ProvidePerformanceFeedbackResponse", + "ProvidePerformanceFeedbackSuccessResponse", + "PublisherDomain", + "PublisherIdentifierTypes", + "PublisherProperties", + "PublisherPropertiesAll", + "PublisherPropertiesById", + "PublisherPropertiesByTag", + "PushNotificationConfig", + "QuartileData", + "QuerySummary", + "ReachUnit", + "Refine", + "ReportPlanOutcomeRequest", + "ReportPlanOutcomeResponse", + "ReportUsageRequest", + "ReportUsageResponse", + "ReportingCapabilities", + "ReportingFrequency", + "ReportingPeriod", + "ReportingWebhook", + "Request", + "ResolvedBrand", + "ResolvedProperty", + "Response", + "ResponseType", + "Responsive", + "Results", + "RightsPricingOption", + "RightsTerms", + "Scheme", + "Security", + "SegmentIdActivationKey", + "SiCapabilities", + "SiGetOfferingRequest", + "SiGetOfferingResponse", + "SiIdentity", + "SiInitiateSessionRequest", + "SiInitiateSessionResponse", + "SiSendActionResponseRequest", + "SiSendMessageRequest", + "SiSendMessageResponse", + "SiSendTextMessageRequest", + "SiTerminateSessionRequest", + "SiTerminateSessionResponse", + "SiUiElement", + "Signal", + "SignalCatalogType", + "SignalFilters", + "SignalPricingOption", + "SignalType", + "Snapshot", + "SnapshotUnavailableReason", + "Sort", + "SortApplied", + "SortDirection", + "Status", + "StatusSummary", + "SyncAccountsErrorResponse", + "SyncAccountsRequest", + "SyncAccountsResponse", + "SyncAccountsSuccessResponse", + "SyncAudiencesAudience", + "SyncAudiencesErrorResponse", + "SyncAudiencesRequest", + "SyncAudiencesResponse", + "SyncAudiencesSuccessResponse", + "SyncCatalogResult", + "SyncCatalogsErrorResponse", + "SyncCatalogsInputRequired", + "SyncCatalogsRequest", + "SyncCatalogsResponse", + "SyncCatalogsSubmitted", + "SyncCatalogsSuccessResponse", + "SyncCatalogsWorking", + "SyncCreativeResult", + "SyncCreativesErrorResponse", + "SyncCreativesRequest", + "SyncCreativesResponse", + "SyncCreativesSuccessResponse", + "SyncEventSourcesErrorResponse", + "SyncEventSourcesRequest", + "SyncEventSourcesResponse", + "SyncEventSourcesSuccessResponse", + "SyncGovernanceRequest", + "SyncGovernanceResponse", + "SyncPlansRequest", + "SyncPlansResponse", + "Tags", + "TargetingOverlay", + "TaskResult", + "TaskType", + "TextAsset", + "TextSubAsset", + "TimeBasedPricingOption", + "TimeUnit", + "TmpError", + "TmpOffer", + "Totals", + "TrackingEvent", + "Transform", + "Unit", + "UpdateCollectionListRequest", + "UpdateCollectionListResponse", + "UpdateContentStandardsErrorResponse", + "UpdateContentStandardsRequest", + "UpdateContentStandardsResponse", + "UpdateContentStandardsSuccessResponse", + "UpdateFrequency", + "UpdateMediaBuyErrorResponse", + "UpdateMediaBuyPackagesRequest", + "UpdateMediaBuyPropertiesRequest", + "UpdateMediaBuyRequest", + "UpdateMediaBuyResponse", + "UpdateMediaBuySuccessResponse", + "UpdatePropertyListRequest", + "UpdatePropertyListResponse", + "UpdateRightsRequest", + "UpdateRightsResponse", + "UrlAsset", + "UrlAssetType", + "UrlDaastAsset", + "UrlPreviewRender", + "UrlType", + "UrlVastAsset", + "ValidateContentDeliveryErrorResponse", + "ValidateContentDeliveryRequest", + "ValidateContentDeliveryResponse", + "ValidateContentDeliverySuccessResponse", + "ValidationMode", + "VastTrackingEvent", + "VastVersion", + "VcpmAuctionPricingOption", + "VcpmFixedRatePricingOption", + "VcpmPricingOption", + "VenueBreakdownItem", + "VideoAsset", + "ViewThreshold", + "WcagLevel", + "WebhookAsset", + "WebhookMetadata", + "WebhookResponseType", + "aliases", + "generated" + ] +} diff --git a/tests/test_public_api.py b/tests/test_public_api.py index e5810896d..503e26e35 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -342,3 +342,60 @@ def test_list_creative_formats_request_filter_params_types(): data = request.model_dump(exclude_none=True) assert data["is_responsive"] is True assert data["name_search"] == "mobile" + + +def test_removed_v4_types_raise_informative_import_error(): + """Removed-in-4.0 names should raise a clear ImportError pointing at MIGRATION.""" + import pytest + + import adcp + + for name in ("BrandManifest", "FormatCategory", "DeliverTo", "Pricing", "PackageStatus"): + with pytest.raises(ImportError) as exc: + getattr(adcp, name) + assert "MIGRATION_v3_to_v4.md" in str(exc.value) + assert "4.0" in str(exc.value) + + +def test_public_api_surface_matches_snapshot(): + """Fail when `adcp.__all__` or `adcp.types.__all__` drifts from the snapshot. + + Regenerate after an intentional change: + + python scripts/regenerate_public_api_snapshot.py + + Note: this test tracks names only. A name whose underlying class identity + changes (e.g., aliased to a different generated class) won't be caught + here — review the diff on `adcp/types/aliases.py` separately for that. + """ + import json + from pathlib import Path + + import adcp + import adcp.types + + snapshot_path = Path(__file__).parent / "fixtures" / "public_api_snapshot.json" + snapshot = json.loads(snapshot_path.read_text()) + regen_cmd = "python scripts/regenerate_public_api_snapshot.py" + + current = { + "adcp": sorted(adcp.__all__), + "adcp.types": sorted(adcp.types.__all__), + } + + for module_name in ("adcp", "adcp.types"): + expected = set(snapshot[module_name]) + actual = set(current[module_name]) + removed = sorted(expected - actual) + added = sorted(actual - expected) + assert not removed, ( + f"Public names removed from {module_name}: {removed}. " + "Removals are breaking changes — add a CHANGELOG entry and, for " + "a major version bump, a MIGRATION note, then regenerate the " + f"snapshot with `{regen_cmd}`." + ) + assert not added, ( + f"Public names added to {module_name}: {added}. " + f"Once the addition is intentional, regenerate the snapshot with " + f"`{regen_cmd}`." + ) diff --git a/tests/test_registry.py b/tests/test_registry.py index 14c00e96f..6de0cb052 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -12,15 +12,6 @@ from adcp.types.core import Member, ResolvedBrand, ResolvedProperty BRAND_DATA = { - "canonical_id": "nike.com", - "canonical_domain": "nike.com", - "brand_name": "Nike", - "keller_type": "master", - "source": "brand_json", - "brand_manifest": {"name": "Nike"}, -} - -BRAND_DATA_NEW = { "canonical_id": "nike.com", "canonical_domain": "nike.com", "brand_name": "Nike", @@ -202,19 +193,6 @@ async def test_extra_fields_preserved(self): assert result is not None assert result.extra_field == "extra_value" # type: ignore[attr-defined] - @pytest.mark.asyncio - async def test_resolves_brand_with_new_brand_field(self): - """Registry returns 'brand' field (new API); both brand and brand_manifest accessible.""" - mock_client = MagicMock() - mock_client.get = AsyncMock(return_value=_mock_response(200, BRAND_DATA_NEW)) - - rc = RegistryClient(client=mock_client) - result = await rc.lookup_brand("nike.com") - - assert result is not None - assert result.brand == {"name": "Nike"} - assert result.brand_manifest == {"name": "Nike"} - @pytest.mark.asyncio async def test_raises_on_invalid_response_data(self): mock_client = MagicMock() @@ -626,32 +604,13 @@ def test_resolved_brand_optional_fields(self): } brand = ResolvedBrand.model_validate(minimal) assert brand.keller_type is None - assert brand.brand_manifest is None assert brand.brand is None assert brand.house_domain is None - def test_resolved_brand_new_field_populates_brand_manifest(self): - """New API returns 'brand' field; brand_manifest must still be accessible.""" - brand = ResolvedBrand.model_validate(BRAND_DATA_NEW) - assert brand.brand == {"name": "Nike"} - assert brand.brand_manifest == {"name": "Nike"} - - def test_resolved_brand_old_field_populates_brand(self): - """Old API returns 'brand_manifest'; brand field must be populated too.""" + def test_resolved_brand_reads_brand_field(self): brand = ResolvedBrand.model_validate(BRAND_DATA) - assert brand.brand_manifest == {"name": "Nike"} assert brand.brand == {"name": "Nike"} - def test_resolved_brand_both_fields_present(self): - """When both fields are present, each keeps its own value.""" - data = { - **BRAND_DATA, - "brand": {"name": "Nike (brand.json)"}, - } - brand = ResolvedBrand.model_validate(data) - assert brand.brand_manifest == {"name": "Nike"} - assert brand.brand == {"name": "Nike (brand.json)"} - def test_resolved_property_validates(self): prop = ResolvedProperty.model_validate(PROPERTY_DATA) assert prop.publisher_domain == "nytimes.com" @@ -849,9 +808,7 @@ async def test_lists_policies(self): @pytest.mark.asyncio async def test_empty_policy_list(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(200, {"policies": []}) - ) + mock_client.get = AsyncMock(return_value=_mock_response(200, {"policies": []})) rc = RegistryClient(client=mock_client) policies = await rc.list_policies() @@ -869,9 +826,7 @@ async def test_missing_policies_key_returns_empty_list(self): @pytest.mark.asyncio async def test_sends_correct_params(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(200, {"policies": []}) - ) + mock_client.get = AsyncMock(return_value=_mock_response(200, {"policies": []})) rc = RegistryClient( base_url="https://test.example.com", @@ -908,9 +863,7 @@ async def test_sends_correct_params(self): @pytest.mark.asyncio async def test_omits_none_params(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(200, {"policies": []}) - ) + mock_client.get = AsyncMock(return_value=_mock_response(200, {"policies": []})) rc = RegistryClient(client=mock_client) await rc.list_policies(category="standard") @@ -961,9 +914,7 @@ async def test_resolves_known_policy(self): @pytest.mark.asyncio async def test_returns_none_for_404(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(404, {"error": "Policy not found"}) - ) + mock_client.get = AsyncMock(return_value=_mock_response(404, {"error": "Policy not found"})) rc = RegistryClient(client=mock_client) result = await rc.resolve_policy("unknown_policy") @@ -1031,9 +982,7 @@ async def test_raises_on_timeout(self): @pytest.mark.asyncio async def test_raises_on_invalid_response_data(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(200, {"unexpected": "data"}) - ) + mock_client.get = AsyncMock(return_value=_mock_response(200, {"unexpected": "data"})) rc = RegistryClient(client=mock_client) with pytest.raises(RegistryError, match="invalid response"): @@ -1097,9 +1046,7 @@ async def mock_post(url, json, headers, timeout): async def test_policy_absent_from_response_defaults_to_none(self): mock_client = MagicMock() mock_client.post = AsyncMock( - return_value=_mock_response( - 200, {"results": {"gdpr_consent": POLICY_DATA}} - ) + return_value=_mock_response(200, {"results": {"gdpr_consent": POLICY_DATA}}) ) rc = RegistryClient(client=mock_client) @@ -1135,9 +1082,7 @@ class TestPolicyHistory: @pytest.mark.asyncio async def test_retrieves_history(self): mock_client = MagicMock() - mock_client.get = AsyncMock( - return_value=_mock_response(200, POLICY_HISTORY_DATA) - ) + mock_client.get = AsyncMock(return_value=_mock_response(200, POLICY_HISTORY_DATA)) rc = RegistryClient(client=mock_client) result = await rc.policy_history("gdpr_consent") @@ -1293,8 +1238,12 @@ async def test_raises_on_401(self): rc = RegistryClient(client=mock_client) with pytest.raises(RegistryError) as exc_info: await rc.save_policy( - policy_id="x", version="1.0.0", name="X", - category="standard", enforcement="should", policy="text", + policy_id="x", + version="1.0.0", + name="X", + category="standard", + enforcement="should", + policy="text", auth_token="bad_token", ) assert exc_info.value.status_code == 401 @@ -1307,8 +1256,12 @@ async def test_raises_on_409(self): rc = RegistryClient(client=mock_client) with pytest.raises(RegistryError) as exc_info: await rc.save_policy( - policy_id="gdpr_consent", version="1.0.0", name="X", - category="regulation", enforcement="must", policy="text", + policy_id="gdpr_consent", + version="1.0.0", + name="X", + category="regulation", + enforcement="must", + policy="text", auth_token="sk_key", ) assert exc_info.value.status_code == 409 @@ -1321,8 +1274,12 @@ async def test_raises_on_timeout(self): rc = RegistryClient(client=mock_client) with pytest.raises(RegistryError, match="timed out"): await rc.save_policy( - policy_id="x", version="1.0.0", name="X", - category="standard", enforcement="should", policy="text", + policy_id="x", + version="1.0.0", + name="X", + category="standard", + enforcement="should", + policy="text", auth_token="sk_key", ) @@ -1407,10 +1364,12 @@ def test_policy_exemplar_validates(self): def test_policy_exemplars_pass_alias(self): """The 'pass' field uses alias since 'pass' is a Python keyword.""" - exemplars = PolicyExemplars.model_validate({ - "pass": [{"scenario": "ok", "explanation": "fine"}], - "fail": [{"scenario": "bad", "explanation": "not fine"}], - }) + exemplars = PolicyExemplars.model_validate( + { + "pass": [{"scenario": "ok", "explanation": "fine"}], + "fail": [{"scenario": "bad", "explanation": "not fine"}], + } + ) assert len(exemplars.pass_) == 1 assert len(exemplars.fail) == 1