Skip to content

Commit a1e9f70

Browse files
bokelleyclaude
andcommitted
feat: improve type ergonomics for library consumers
Add flexible input coercion for request types that reduces boilerplate when constructing API requests. All changes are backward compatible. Improvements: - Enum fields accept string values (e.g., type="video") - List[Enum] fields accept string lists (e.g., asset_types=["image", "video"]) - Context/Ext fields accept dicts (e.g., context={"key": "value"}) - FieldModel lists accept strings (e.g., fields=["creative_id", "name"]) - Sort fields accept string enums (e.g., field="name", direction="asc") Affected types: - ListCreativeFormatsRequest (type, asset_types, context, ext) - ListCreativesRequest (fields, context, ext, sort) - GetProductsRequest (context, ext) - PackageRequest (ext) The list variance issue (list[Subclass] not assignable to list[BaseClass]) is documented with recommended workarounds in adcp.types.coercion. Closes #102 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ff82519 commit a1e9f70

File tree

4 files changed

+771
-1
lines changed

4 files changed

+771
-1
lines changed

src/adcp/types/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,33 @@
66
Examples:
77
from adcp.types import Product, CreativeFilters
88
from adcp import Product, CreativeFilters
9+
10+
Type Coercion:
11+
For developer ergonomics, request types accept flexible input:
12+
13+
- Enum fields accept string values:
14+
ListCreativeFormatsRequest(type="video") # Works!
15+
ListCreativeFormatsRequest(type=FormatCategory.video) # Also works
16+
17+
- Context fields accept dicts:
18+
GetProductsRequest(context={"key": "value"}) # Works!
19+
20+
- FieldModel lists accept strings:
21+
ListCreativesRequest(fields=["creative_id", "name"]) # Works!
22+
23+
See adcp.types.coercion for implementation details.
924
"""
1025

1126
from __future__ import annotations
1227

28+
# Apply type coercion to generated types (must be imported before other types)
29+
from adcp.types import (
30+
_ergonomic, # noqa: F401
31+
aliases, # noqa: F401
32+
)
33+
1334
# Also make submodules available for advanced use
1435
from adcp.types import _generated as generated # noqa: F401
15-
from adcp.types import aliases # noqa: F401
1636

1737
# Import all types from generated code
1838
from adcp.types._generated import (

src/adcp/types/_ergonomic.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Apply type coercion to generated types for better ergonomics.
2+
3+
This module patches the generated types to accept more flexible input types
4+
while maintaining type safety. It uses Pydantic's model_rebuild() to add
5+
BeforeValidator annotations to fields.
6+
7+
The coercion is applied at module load time, so imports from adcp.types
8+
will automatically have the coercion applied.
9+
10+
Coercion rules applied:
11+
1. Enum fields accept string values (e.g., "video" for FormatCategory.video)
12+
2. List[Enum] fields accept list of strings (e.g., ["image", "video"])
13+
3. ContextObject fields accept dict values
14+
4. ExtensionObject fields accept dict values
15+
5. FieldModel (enum) lists accept string lists
16+
17+
Note: List variance issues (list[Subclass] not assignable to list[BaseClass])
18+
are a fundamental Python typing limitation. Users extending library types
19+
should use Sequence[T] in their own code or cast() for type checker appeasement.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from typing import Annotated, Any
25+
26+
from pydantic import BeforeValidator
27+
28+
from adcp.types.coercion import (
29+
coerce_fields_to_enum,
30+
coerce_to_enum,
31+
coerce_to_enum_list,
32+
coerce_to_model,
33+
)
34+
35+
# Import types that need coercion
36+
from adcp.types.generated_poc.core.context import ContextObject
37+
from adcp.types.generated_poc.core.ext import ExtensionObject
38+
from adcp.types.generated_poc.enums.asset_content_type import AssetContentType
39+
from adcp.types.generated_poc.enums.creative_sort_field import CreativeSortField
40+
from adcp.types.generated_poc.enums.format_category import FormatCategory
41+
from adcp.types.generated_poc.enums.sort_direction import SortDirection
42+
from adcp.types.generated_poc.media_buy.get_products_request import GetProductsRequest
43+
from adcp.types.generated_poc.media_buy.list_creative_formats_request import (
44+
ListCreativeFormatsRequest,
45+
)
46+
from adcp.types.generated_poc.media_buy.list_creatives_request import (
47+
FieldModel,
48+
ListCreativesRequest,
49+
Sort,
50+
)
51+
from adcp.types.generated_poc.media_buy.package_request import PackageRequest
52+
53+
54+
def _apply_coercion() -> None:
55+
"""Apply coercion validators to generated types.
56+
57+
This function modifies the generated types in-place to accept
58+
more flexible input types.
59+
"""
60+
# Apply coercion to ListCreativeFormatsRequest
61+
# - type: FormatCategory | str | None
62+
# - asset_types: list[AssetContentType | str] | None
63+
# - context: ContextObject | dict | None
64+
# - ext: ExtensionObject | dict | None
65+
ListCreativeFormatsRequest.model_rebuild(
66+
_types_namespace={
67+
"FormatCategory": FormatCategory,
68+
"AssetContentType": AssetContentType,
69+
"ContextObject": ContextObject,
70+
"ExtensionObject": ExtensionObject,
71+
},
72+
force=True,
73+
)
74+
75+
# Update field annotations for ListCreativeFormatsRequest
76+
_patch_field_annotation(
77+
ListCreativeFormatsRequest,
78+
"type",
79+
Annotated[FormatCategory | None, BeforeValidator(coerce_to_enum(FormatCategory))],
80+
)
81+
_patch_field_annotation(
82+
ListCreativeFormatsRequest,
83+
"asset_types",
84+
Annotated[
85+
list[AssetContentType] | None,
86+
BeforeValidator(coerce_to_enum_list(AssetContentType)),
87+
],
88+
)
89+
_patch_field_annotation(
90+
ListCreativeFormatsRequest,
91+
"context",
92+
Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
93+
)
94+
_patch_field_annotation(
95+
ListCreativeFormatsRequest,
96+
"ext",
97+
Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
98+
)
99+
ListCreativeFormatsRequest.model_rebuild(force=True)
100+
101+
# Apply coercion to ListCreativesRequest
102+
# - fields: list[FieldModel | str] | None
103+
# - context: ContextObject | dict | None
104+
# - ext: ExtensionObject | dict | None
105+
_patch_field_annotation(
106+
ListCreativesRequest,
107+
"fields",
108+
Annotated[list[FieldModel] | None, BeforeValidator(coerce_fields_to_enum(FieldModel))],
109+
)
110+
_patch_field_annotation(
111+
ListCreativesRequest,
112+
"context",
113+
Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
114+
)
115+
_patch_field_annotation(
116+
ListCreativesRequest,
117+
"ext",
118+
Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
119+
)
120+
ListCreativesRequest.model_rebuild(force=True)
121+
122+
# Apply coercion to Sort (nested in ListCreativesRequest)
123+
# - field: CreativeSortField | str | None
124+
# - direction: SortDirection | str | None
125+
_patch_field_annotation(
126+
Sort,
127+
"field",
128+
Annotated[
129+
CreativeSortField | None,
130+
BeforeValidator(coerce_to_enum(CreativeSortField)),
131+
],
132+
)
133+
_patch_field_annotation(
134+
Sort,
135+
"direction",
136+
Annotated[SortDirection | None, BeforeValidator(coerce_to_enum(SortDirection))],
137+
)
138+
Sort.model_rebuild(force=True)
139+
140+
# Apply coercion to GetProductsRequest
141+
# - context: ContextObject | dict | None
142+
# - ext: ExtensionObject | dict | None
143+
_patch_field_annotation(
144+
GetProductsRequest,
145+
"context",
146+
Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
147+
)
148+
_patch_field_annotation(
149+
GetProductsRequest,
150+
"ext",
151+
Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
152+
)
153+
GetProductsRequest.model_rebuild(force=True)
154+
155+
# Apply coercion to PackageRequest
156+
# - ext: ExtensionObject | dict | None
157+
_patch_field_annotation(
158+
PackageRequest,
159+
"ext",
160+
Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
161+
)
162+
PackageRequest.model_rebuild(force=True)
163+
164+
165+
def _patch_field_annotation(
166+
model: type,
167+
field_name: str,
168+
new_annotation: Any,
169+
) -> None:
170+
"""Patch a field annotation on a Pydantic model.
171+
172+
This modifies the model's __annotations__ dict to add
173+
BeforeValidator coercion.
174+
"""
175+
if hasattr(model, "__annotations__"):
176+
model.__annotations__[field_name] = new_annotation
177+
178+
179+
# Apply coercion when module is imported
180+
_apply_coercion()

0 commit comments

Comments
 (0)