From e45077826ee9b893550b890d7a637706eaf52291 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 1 Mar 2026 08:11:02 -0500 Subject: [PATCH 1/3] fix: expose GetSignalsRequest as union alias, not RootModel Pydantic hard-rejects model_config['extra'] on RootModel subclasses, which broke the documented pattern of subclassing library types with env-based config. Adding a plain union alias in aliases.py shadows the generated RootModel wrapper, restoring the ability for consumers to subclass GetSignalsRequest1 or GetSignalsRequest2 with custom model_config. Closes #138 Co-Authored-By: Claude Sonnet 4.6 --- src/adcp/types/__init__.py | 2 +- src/adcp/types/aliases.py | 3 +++ tests/test_type_aliases.py | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/adcp/types/__init__.py b/src/adcp/types/__init__.py index e6dd78d6..9947d9e2 100644 --- a/src/adcp/types/__init__.py +++ b/src/adcp/types/__init__.py @@ -184,7 +184,6 @@ GetProductsResponse, GetPropertyListRequest, GetPropertyListResponse, - GetSignalsRequest, GetSignalsResponse, Gtin, HtmlAsset, @@ -399,6 +398,7 @@ GetProductsWholesaleRequest, GetSignalsDiscoveryRequest, GetSignalsLookupRequest, + GetSignalsRequest, HtmlPreviewRender, InlineDaastAsset, InlineVastAsset, diff --git a/src/adcp/types/aliases.py b/src/adcp/types/aliases.py index 20620347..af0aeee3 100644 --- a/src/adcp/types/aliases.py +++ b/src/adcp/types/aliases.py @@ -506,6 +506,9 @@ def process_result(result: SyncCatalogResult) -> None: """Get products in wholesale mode - buying_mode='wholesale', raw inventory.""" # Get Signals Request Variants +GetSignalsRequest = GetSignalsRequest1 | GetSignalsRequest2 +"""Union of GetSignalsRequest variants. Use instead of the RootModel wrapper to allow subclassing.""" + GetSignalsDiscoveryRequest = GetSignalsRequest1 """Discover signals by natural language spec - signal_spec required.""" diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 5d9a36d8..cec39528 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -808,3 +808,48 @@ def test_destination_union_contains_all_variants(): } assert union_args == expected_variants + + +def test_get_signals_request_is_union_not_root_model(): + """GetSignalsRequest is a plain union alias, not a RootModel. + + This allows consumers to subclass GetSignalsRequest1 or GetSignalsRequest2 + with custom model_config (e.g. extra='forbid' in CI, extra='ignore' in prod). + See: https://github.com/adcontextprotocol/adcp-client-python/issues/138 + """ + import types + + from pydantic import RootModel + + from adcp import GetSignalsRequest + + # Must be a union, not a RootModel subclass + assert isinstance(GetSignalsRequest, types.UnionType) + assert not (isinstance(GetSignalsRequest, type) and issubclass(GetSignalsRequest, RootModel)) + + +def test_get_signals_request_union_contains_variants(): + """GetSignalsRequest union contains the two concrete variants.""" + from typing import get_args + + from adcp import GetSignalsDiscoveryRequest, GetSignalsLookupRequest, GetSignalsRequest + + union_args = set(get_args(GetSignalsRequest)) + assert union_args == {GetSignalsDiscoveryRequest, GetSignalsLookupRequest} + + +def test_get_signals_request_variants_are_subclassable(): + """Consumers can subclass GetSignalsRequest variants with custom model_config.""" + from pydantic import ConfigDict + + from adcp import GetSignalsDiscoveryRequest, GetSignalsLookupRequest + + class MyDiscoveryRequest(GetSignalsDiscoveryRequest): + model_config = ConfigDict(extra="forbid") + + class MyLookupRequest(GetSignalsLookupRequest): + model_config = ConfigDict(extra="forbid") + + # Should construct without error + req = MyDiscoveryRequest(signal_spec="targeting signals for automotive in-market buyers") + assert req.signal_spec == "targeting signals for automotive in-market buyers" From 5b18261c5fbe799edbbffa1a2a983b60235ea1e0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 1 Mar 2026 08:19:12 -0500 Subject: [PATCH 2/3] fix: resolve CI failures from GetSignalsRequest union alias - Shorten docstring in aliases.py to fix ruff E501 line-too-long error - Fix simple.py to dispatch GetSignalsRequest variants via model_validate instead of calling the union type as a constructor (mypy operator error) Co-Authored-By: Claude Sonnet 4.6 --- src/adcp/simple.py | 9 +++++++-- src/adcp/types/aliases.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/adcp/simple.py b/src/adcp/simple.py index f8145bef..986cecc4 100644 --- a/src/adcp/simple.py +++ b/src/adcp/simple.py @@ -36,7 +36,8 @@ GetMediaBuyDeliveryResponse, GetProductsRequest, GetProductsResponse, - GetSignalsRequest, + GetSignalsDiscoveryRequest, + GetSignalsLookupRequest, GetSignalsResponse, ListAccountsRequest, ListAccountsResponse, @@ -275,7 +276,11 @@ async def get_signals( Raises: Exception: If the request fails """ - request = GetSignalsRequest(**kwargs) + request: GetSignalsDiscoveryRequest | GetSignalsLookupRequest + if "signal_ids" in kwargs: + request = GetSignalsLookupRequest.model_validate(kwargs) + else: + request = GetSignalsDiscoveryRequest.model_validate(kwargs) result = await self._client.get_signals(request) if not result.success or not result.data: raise ADCPSimpleAPIError( diff --git a/src/adcp/types/aliases.py b/src/adcp/types/aliases.py index af0aeee3..5eef62ce 100644 --- a/src/adcp/types/aliases.py +++ b/src/adcp/types/aliases.py @@ -507,7 +507,7 @@ def process_result(result: SyncCatalogResult) -> None: # Get Signals Request Variants GetSignalsRequest = GetSignalsRequest1 | GetSignalsRequest2 -"""Union of GetSignalsRequest variants. Use instead of the RootModel wrapper to allow subclassing.""" +"""Union of GetSignalsRequest variants. Use instead of the RootModel wrapper.""" GetSignalsDiscoveryRequest = GetSignalsRequest1 """Discover signals by natural language spec - signal_spec required.""" From a38eae8e0ce8933ad381d1837e0d2aed4196b637 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 1 Mar 2026 08:25:41 -0500 Subject: [PATCH 3/3] chore: exclude examples/ and scripts/ from ruff linting These directories contain dev tooling and examples, not published code. CI already only lints src/, so this makes the local config consistent. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4ba6e2da..94f28a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,8 @@ extend-exclude = [ "src/adcp/types/_generated.py", "src/adcp/types/tasks.py", "src/adcp/types/generated_poc/", + "examples/", + "scripts/", ] [tool.ruff.lint]