adcp 2.14.0__py3-none-any.whl → 2.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
adcp/__init__.py CHANGED
@@ -186,7 +186,7 @@ from adcp.webhooks import (
186
186
  get_adcp_signed_headers_for_webhook,
187
187
  )
188
188
 
189
- __version__ = "2.14.0"
189
+ __version__ = "2.16.0"
190
190
 
191
191
 
192
192
  def get_adcp_version() -> str:
adcp/types/__init__.py CHANGED
@@ -6,13 +6,33 @@ Users should import from here or directly from adcp.
6
6
  Examples:
7
7
  from adcp.types import Product, CreativeFilters
8
8
  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.
9
24
  """
10
25
 
11
26
  from __future__ import annotations
12
27
 
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
+
13
34
  # Also make submodules available for advanced use
14
35
  from adcp.types import _generated as generated # noqa: F401
15
- from adcp.types import aliases # noqa: F401
16
36
 
17
37
  # Import all types from generated code
18
38
  from adcp.types._generated import (
@@ -0,0 +1,464 @@
1
+ # AUTO-GENERATED by scripts/generate_ergonomic_coercion.py
2
+ # Do not edit manually - changes will be overwritten on next type generation.
3
+ # To regenerate: python scripts/generate_types.py
4
+ """Apply type coercion to generated types for better ergonomics.
5
+
6
+ This module patches the generated types to accept more flexible input types
7
+ while maintaining type safety. It uses Pydantic's model_rebuild() to add
8
+ BeforeValidator annotations to fields.
9
+
10
+ Why import-time patching?
11
+ We apply coercion at module load time rather than lazily because:
12
+ 1. Pydantic validation runs during __init__, before any lazy access
13
+ 2. model_rebuild() is the standard Pydantic pattern for post-hoc changes
14
+ 3. The cost is minimal (~10-20ms for all types, once at import)
15
+ 4. After import, there is zero runtime overhead
16
+ 5. This approach maintains full type checker compatibility
17
+
18
+ Coercion rules applied:
19
+ 1. Enum fields accept string values (e.g., "video" for FormatCategory.video)
20
+ 2. List[Enum] fields accept list of strings (e.g., ["image", "video"])
21
+ 3. ContextObject fields accept dict values
22
+ 4. ExtensionObject fields accept dict values
23
+ 5. FieldModel (enum) lists accept string lists
24
+
25
+ Note: List variance issues (list[Subclass] not assignable to list[BaseClass])
26
+ are a fundamental Python typing limitation. Users extending library types
27
+ should use Sequence[T] in their own code or cast() for type checker appeasement.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Annotated, Any
33
+
34
+ from pydantic import BeforeValidator
35
+
36
+ from adcp.types.coercion import (
37
+ coerce_subclass_list,
38
+ coerce_to_enum,
39
+ coerce_to_enum_list,
40
+ coerce_to_model,
41
+ )
42
+
43
+ # Import types that need coercion
44
+ from adcp.types.generated_poc.core.context import ContextObject
45
+ from adcp.types.generated_poc.core.creative_asset import CreativeAsset
46
+ from adcp.types.generated_poc.core.creative_assignment import CreativeAssignment
47
+ from adcp.types.generated_poc.core.error import Error
48
+ from adcp.types.generated_poc.core.ext import ExtensionObject
49
+ from adcp.types.generated_poc.core.format import Format
50
+ from adcp.types.generated_poc.core.package import Package
51
+ from adcp.types.generated_poc.core.product import Product
52
+ from adcp.types.generated_poc.enums.asset_content_type import AssetContentType
53
+ from adcp.types.generated_poc.enums.creative_sort_field import CreativeSortField
54
+ from adcp.types.generated_poc.enums.format_category import FormatCategory
55
+ from adcp.types.generated_poc.enums.pacing import Pacing
56
+ from adcp.types.generated_poc.enums.sort_direction import SortDirection
57
+ from adcp.types.generated_poc.media_buy.create_media_buy_request import (
58
+ CreateMediaBuyRequest,
59
+ )
60
+
61
+ # Response types
62
+ from adcp.types.generated_poc.media_buy.create_media_buy_response import (
63
+ CreateMediaBuyResponse1,
64
+ )
65
+ from adcp.types.generated_poc.media_buy.get_media_buy_delivery_response import (
66
+ GetMediaBuyDeliveryResponse,
67
+ MediaBuyDelivery,
68
+ NotificationType,
69
+ )
70
+ from adcp.types.generated_poc.media_buy.get_products_request import GetProductsRequest
71
+ from adcp.types.generated_poc.media_buy.get_products_response import GetProductsResponse
72
+ from adcp.types.generated_poc.media_buy.list_creative_formats_request import (
73
+ ListCreativeFormatsRequest,
74
+ )
75
+ from adcp.types.generated_poc.media_buy.list_creative_formats_response import (
76
+ CreativeAgent,
77
+ ListCreativeFormatsResponse,
78
+ )
79
+ from adcp.types.generated_poc.media_buy.list_creatives_request import (
80
+ FieldModel,
81
+ ListCreativesRequest,
82
+ Sort,
83
+ )
84
+ from adcp.types.generated_poc.media_buy.list_creatives_response import (
85
+ Creative,
86
+ ListCreativesResponse,
87
+ )
88
+ from adcp.types.generated_poc.media_buy.package_request import PackageRequest
89
+ from adcp.types.generated_poc.media_buy.update_media_buy_request import (
90
+ Packages,
91
+ Packages1,
92
+ )
93
+
94
+
95
+ def _apply_coercion() -> None:
96
+ """Apply coercion validators to generated types.
97
+
98
+ This function modifies the generated types in-place to accept
99
+ more flexible input types.
100
+ """
101
+ # Apply coercion to ListCreativeFormatsRequest
102
+ # - asset_types: list[AssetContentType | str] | None
103
+ # - context: ContextObject | dict | None
104
+ # - ext: ExtensionObject | dict | None
105
+ # - type: FormatCategory | str | None
106
+ _patch_field_annotation(
107
+ ListCreativeFormatsRequest,
108
+ "asset_types",
109
+ Annotated[
110
+ list[AssetContentType] | None,
111
+ BeforeValidator(coerce_to_enum_list(AssetContentType)),
112
+ ],
113
+ )
114
+ _patch_field_annotation(
115
+ ListCreativeFormatsRequest,
116
+ "context",
117
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
118
+ )
119
+ _patch_field_annotation(
120
+ ListCreativeFormatsRequest,
121
+ "ext",
122
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
123
+ )
124
+ _patch_field_annotation(
125
+ ListCreativeFormatsRequest,
126
+ "type",
127
+ Annotated[FormatCategory | None, BeforeValidator(coerce_to_enum(FormatCategory))],
128
+ )
129
+ ListCreativeFormatsRequest.model_rebuild(force=True)
130
+
131
+ # Apply coercion to ListCreativesRequest
132
+ # - context: ContextObject | dict | None
133
+ # - ext: ExtensionObject | dict | None
134
+ # - fields: list[FieldModel | str] | None
135
+ _patch_field_annotation(
136
+ ListCreativesRequest,
137
+ "context",
138
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
139
+ )
140
+ _patch_field_annotation(
141
+ ListCreativesRequest,
142
+ "ext",
143
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
144
+ )
145
+ _patch_field_annotation(
146
+ ListCreativesRequest,
147
+ "fields",
148
+ Annotated[
149
+ list[FieldModel] | None,
150
+ BeforeValidator(coerce_to_enum_list(FieldModel)),
151
+ ],
152
+ )
153
+ ListCreativesRequest.model_rebuild(force=True)
154
+
155
+ # Apply coercion to Sort
156
+ # - direction: SortDirection | str | None
157
+ # - field: CreativeSortField | str | None
158
+ _patch_field_annotation(
159
+ Sort,
160
+ "direction",
161
+ Annotated[SortDirection | None, BeforeValidator(coerce_to_enum(SortDirection))],
162
+ )
163
+ _patch_field_annotation(
164
+ Sort,
165
+ "field",
166
+ Annotated[CreativeSortField | None, BeforeValidator(coerce_to_enum(CreativeSortField))],
167
+ )
168
+ Sort.model_rebuild(force=True)
169
+
170
+ # Apply coercion to GetProductsRequest
171
+ # - context: ContextObject | dict | None
172
+ # - ext: ExtensionObject | dict | None
173
+ _patch_field_annotation(
174
+ GetProductsRequest,
175
+ "context",
176
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
177
+ )
178
+ _patch_field_annotation(
179
+ GetProductsRequest,
180
+ "ext",
181
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
182
+ )
183
+ GetProductsRequest.model_rebuild(force=True)
184
+
185
+ # Apply coercion to PackageRequest
186
+ # - creatives: list[CreativeAsset] (accepts subclass instances)
187
+ # - ext: ExtensionObject | dict | None
188
+ # - pacing: Pacing | str | None
189
+ _patch_field_annotation(
190
+ PackageRequest,
191
+ "creatives",
192
+ Annotated[
193
+ list[CreativeAsset] | None,
194
+ BeforeValidator(coerce_subclass_list(CreativeAsset)),
195
+ ],
196
+ )
197
+ _patch_field_annotation(
198
+ PackageRequest,
199
+ "ext",
200
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
201
+ )
202
+ _patch_field_annotation(
203
+ PackageRequest,
204
+ "pacing",
205
+ Annotated[Pacing | None, BeforeValidator(coerce_to_enum(Pacing))],
206
+ )
207
+ PackageRequest.model_rebuild(force=True)
208
+
209
+ # Apply coercion to CreateMediaBuyRequest
210
+ # - context: ContextObject | dict | None
211
+ # - ext: ExtensionObject | dict | None
212
+ # - packages: list[PackageRequest] (accepts subclass instances)
213
+ _patch_field_annotation(
214
+ CreateMediaBuyRequest,
215
+ "context",
216
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
217
+ )
218
+ _patch_field_annotation(
219
+ CreateMediaBuyRequest,
220
+ "ext",
221
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
222
+ )
223
+ _patch_field_annotation(
224
+ CreateMediaBuyRequest,
225
+ "packages",
226
+ Annotated[
227
+ list[PackageRequest],
228
+ BeforeValidator(coerce_subclass_list(PackageRequest)),
229
+ ],
230
+ )
231
+ CreateMediaBuyRequest.model_rebuild(force=True)
232
+
233
+ # Apply coercion to Packages
234
+ # - creative_assignments: list[CreativeAssignment] (accepts subclass instances)
235
+ # - creatives: list[CreativeAsset] (accepts subclass instances)
236
+ # - pacing: Pacing | str | None
237
+ _patch_field_annotation(
238
+ Packages,
239
+ "creative_assignments",
240
+ Annotated[
241
+ list[CreativeAssignment] | None,
242
+ BeforeValidator(coerce_subclass_list(CreativeAssignment)),
243
+ ],
244
+ )
245
+ _patch_field_annotation(
246
+ Packages,
247
+ "creatives",
248
+ Annotated[
249
+ list[CreativeAsset] | None,
250
+ BeforeValidator(coerce_subclass_list(CreativeAsset)),
251
+ ],
252
+ )
253
+ _patch_field_annotation(
254
+ Packages,
255
+ "pacing",
256
+ Annotated[Pacing | None, BeforeValidator(coerce_to_enum(Pacing))],
257
+ )
258
+ Packages.model_rebuild(force=True)
259
+
260
+ # Apply coercion to Packages1
261
+ # - creative_assignments: list[CreativeAssignment] (accepts subclass instances)
262
+ # - creatives: list[CreativeAsset] (accepts subclass instances)
263
+ # - pacing: Pacing | str | None
264
+ _patch_field_annotation(
265
+ Packages1,
266
+ "creative_assignments",
267
+ Annotated[
268
+ list[CreativeAssignment] | None,
269
+ BeforeValidator(coerce_subclass_list(CreativeAssignment)),
270
+ ],
271
+ )
272
+ _patch_field_annotation(
273
+ Packages1,
274
+ "creatives",
275
+ Annotated[
276
+ list[CreativeAsset] | None,
277
+ BeforeValidator(coerce_subclass_list(CreativeAsset)),
278
+ ],
279
+ )
280
+ _patch_field_annotation(
281
+ Packages1,
282
+ "pacing",
283
+ Annotated[Pacing | None, BeforeValidator(coerce_to_enum(Pacing))],
284
+ )
285
+ Packages1.model_rebuild(force=True)
286
+
287
+ # Apply coercion to GetProductsResponse
288
+ # - context: ContextObject | dict | None
289
+ # - errors: list[Error] (accepts subclass instances)
290
+ # - ext: ExtensionObject | dict | None
291
+ # - products: list[Product] (accepts subclass instances)
292
+ _patch_field_annotation(
293
+ GetProductsResponse,
294
+ "context",
295
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
296
+ )
297
+ _patch_field_annotation(
298
+ GetProductsResponse,
299
+ "errors",
300
+ Annotated[
301
+ list[Error] | None,
302
+ BeforeValidator(coerce_subclass_list(Error)),
303
+ ],
304
+ )
305
+ _patch_field_annotation(
306
+ GetProductsResponse,
307
+ "ext",
308
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
309
+ )
310
+ _patch_field_annotation(
311
+ GetProductsResponse,
312
+ "products",
313
+ Annotated[
314
+ list[Product],
315
+ BeforeValidator(coerce_subclass_list(Product)),
316
+ ],
317
+ )
318
+ GetProductsResponse.model_rebuild(force=True)
319
+
320
+ # Apply coercion to ListCreativesResponse
321
+ # - context: ContextObject | dict | None
322
+ # - creatives: list[Creative] (accepts subclass instances)
323
+ # - ext: ExtensionObject | dict | None
324
+ _patch_field_annotation(
325
+ ListCreativesResponse,
326
+ "context",
327
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
328
+ )
329
+ _patch_field_annotation(
330
+ ListCreativesResponse,
331
+ "creatives",
332
+ Annotated[
333
+ list[Creative],
334
+ BeforeValidator(coerce_subclass_list(Creative)),
335
+ ],
336
+ )
337
+ _patch_field_annotation(
338
+ ListCreativesResponse,
339
+ "ext",
340
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
341
+ )
342
+ ListCreativesResponse.model_rebuild(force=True)
343
+
344
+ # Apply coercion to ListCreativeFormatsResponse
345
+ # - context: ContextObject | dict | None
346
+ # - creative_agents: list[CreativeAgent] (accepts subclass instances)
347
+ # - errors: list[Error] (accepts subclass instances)
348
+ # - ext: ExtensionObject | dict | None
349
+ # - formats: list[Format] (accepts subclass instances)
350
+ _patch_field_annotation(
351
+ ListCreativeFormatsResponse,
352
+ "context",
353
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
354
+ )
355
+ _patch_field_annotation(
356
+ ListCreativeFormatsResponse,
357
+ "creative_agents",
358
+ Annotated[
359
+ list[CreativeAgent] | None,
360
+ BeforeValidator(coerce_subclass_list(CreativeAgent)),
361
+ ],
362
+ )
363
+ _patch_field_annotation(
364
+ ListCreativeFormatsResponse,
365
+ "errors",
366
+ Annotated[
367
+ list[Error] | None,
368
+ BeforeValidator(coerce_subclass_list(Error)),
369
+ ],
370
+ )
371
+ _patch_field_annotation(
372
+ ListCreativeFormatsResponse,
373
+ "ext",
374
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
375
+ )
376
+ _patch_field_annotation(
377
+ ListCreativeFormatsResponse,
378
+ "formats",
379
+ Annotated[
380
+ list[Format],
381
+ BeforeValidator(coerce_subclass_list(Format)),
382
+ ],
383
+ )
384
+ ListCreativeFormatsResponse.model_rebuild(force=True)
385
+
386
+ # Apply coercion to CreateMediaBuyResponse1
387
+ # - context: ContextObject | dict | None
388
+ # - ext: ExtensionObject | dict | None
389
+ # - packages: list[Package] (accepts subclass instances)
390
+ _patch_field_annotation(
391
+ CreateMediaBuyResponse1,
392
+ "context",
393
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
394
+ )
395
+ _patch_field_annotation(
396
+ CreateMediaBuyResponse1,
397
+ "ext",
398
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
399
+ )
400
+ _patch_field_annotation(
401
+ CreateMediaBuyResponse1,
402
+ "packages",
403
+ Annotated[
404
+ list[Package],
405
+ BeforeValidator(coerce_subclass_list(Package)),
406
+ ],
407
+ )
408
+ CreateMediaBuyResponse1.model_rebuild(force=True)
409
+
410
+ # Apply coercion to GetMediaBuyDeliveryResponse
411
+ # - context: ContextObject | dict | None
412
+ # - errors: list[Error] (accepts subclass instances)
413
+ # - ext: ExtensionObject | dict | None
414
+ # - media_buy_deliveries: list[MediaBuyDelivery] (accepts subclass instances)
415
+ # - notification_type: NotificationType | str | None
416
+ _patch_field_annotation(
417
+ GetMediaBuyDeliveryResponse,
418
+ "context",
419
+ Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],
420
+ )
421
+ _patch_field_annotation(
422
+ GetMediaBuyDeliveryResponse,
423
+ "errors",
424
+ Annotated[
425
+ list[Error] | None,
426
+ BeforeValidator(coerce_subclass_list(Error)),
427
+ ],
428
+ )
429
+ _patch_field_annotation(
430
+ GetMediaBuyDeliveryResponse,
431
+ "ext",
432
+ Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],
433
+ )
434
+ _patch_field_annotation(
435
+ GetMediaBuyDeliveryResponse,
436
+ "media_buy_deliveries",
437
+ Annotated[
438
+ list[MediaBuyDelivery],
439
+ BeforeValidator(coerce_subclass_list(MediaBuyDelivery)),
440
+ ],
441
+ )
442
+ _patch_field_annotation(
443
+ GetMediaBuyDeliveryResponse,
444
+ "notification_type",
445
+ Annotated[NotificationType | None, BeforeValidator(coerce_to_enum(NotificationType))],
446
+ )
447
+ GetMediaBuyDeliveryResponse.model_rebuild(force=True)
448
+
449
+
450
+ def _patch_field_annotation(
451
+ model: type,
452
+ field_name: str,
453
+ new_annotation: Any,
454
+ ) -> None:
455
+ """Patch a field annotation on a Pydantic model.
456
+
457
+ This modifies the model's __annotations__ dict to add
458
+ BeforeValidator coercion.
459
+ """
460
+ model.__annotations__[field_name] = new_annotation
461
+
462
+
463
+ # Apply coercion when module is imported
464
+ _apply_coercion()
adcp/types/coercion.py ADDED
@@ -0,0 +1,194 @@
1
+ """Type coercion utilities for improved type ergonomics.
2
+
3
+ This module provides validators and utilities that enable flexible input types
4
+ while maintaining type safety. It allows developers to use natural Python
5
+ patterns (strings for enums, dicts for models) without explicit type construction.
6
+
7
+ Examples:
8
+ # With coercion, these are equivalent:
9
+ ListCreativeFormatsRequest(type="video")
10
+ ListCreativeFormatsRequest(type=FormatCategory.video)
11
+
12
+ # Dict coercion for context:
13
+ ListCreativeFormatsRequest(context={"key": "value"})
14
+ ListCreativeFormatsRequest(context=ContextObject(key="value"))
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+ from enum import Enum
21
+ from typing import TYPE_CHECKING, Any, TypeVar
22
+
23
+ if TYPE_CHECKING:
24
+ from pydantic import BaseModel
25
+
26
+ T = TypeVar("T", bound=Enum)
27
+ M = TypeVar("M", bound="BaseModel")
28
+
29
+
30
+ def coerce_to_enum(enum_class: type[T]) -> Callable[[Any], T | None]:
31
+ """Create a validator that coerces strings to enum values.
32
+
33
+ This allows users to pass string values where enums are expected,
34
+ which Pydantic will coerce at runtime anyway, but this makes it
35
+ type-checker friendly.
36
+
37
+ Args:
38
+ enum_class: The enum class to coerce to.
39
+
40
+ Returns:
41
+ A validator function for use with Pydantic's BeforeValidator.
42
+
43
+ Example:
44
+ ```python
45
+ from pydantic import BeforeValidator
46
+ from typing import Annotated
47
+
48
+ type: Annotated[
49
+ FormatCategory | None,
50
+ BeforeValidator(coerce_to_enum(FormatCategory))
51
+ ] = None
52
+ ```
53
+ """
54
+
55
+ def validator(value: Any) -> T | None:
56
+ if value is None:
57
+ return None
58
+ if isinstance(value, enum_class):
59
+ return value
60
+ if isinstance(value, str):
61
+ try:
62
+ return enum_class(value)
63
+ except ValueError:
64
+ # Let Pydantic handle the validation error
65
+ return value # type: ignore
66
+ return value # type: ignore
67
+
68
+ return validator
69
+
70
+
71
+ def coerce_to_enum_list(enum_class: type[T]) -> Callable[[Any], list[T] | None]:
72
+ """Create a validator that coerces a list of strings to enum values.
73
+
74
+ Args:
75
+ enum_class: The enum class to coerce to.
76
+
77
+ Returns:
78
+ A validator function for use with Pydantic's BeforeValidator.
79
+ """
80
+
81
+ def validator(value: Any) -> list[T] | None:
82
+ if value is None:
83
+ return None
84
+ if not isinstance(value, (list, tuple)):
85
+ return value # type: ignore
86
+ result: list[T] = []
87
+ for item in value:
88
+ if isinstance(item, enum_class):
89
+ result.append(item)
90
+ elif isinstance(item, str):
91
+ try:
92
+ result.append(enum_class(item))
93
+ except ValueError:
94
+ # Let Pydantic handle the validation error
95
+ result.append(item) # type: ignore
96
+ else:
97
+ result.append(item)
98
+ return result
99
+
100
+ return validator
101
+
102
+
103
+ def coerce_to_model(model_class: type[M]) -> Callable[[Any], M | None]:
104
+ """Create a validator that coerces dicts to Pydantic model instances.
105
+
106
+ This allows users to pass dict values where model objects are expected,
107
+ making the API more ergonomic.
108
+
109
+ Args:
110
+ model_class: The Pydantic model class to coerce to.
111
+
112
+ Returns:
113
+ A validator function for use with Pydantic's BeforeValidator.
114
+
115
+ Example:
116
+ ```python
117
+ from pydantic import BeforeValidator
118
+ from typing import Annotated
119
+
120
+ context: Annotated[
121
+ ContextObject | None,
122
+ BeforeValidator(coerce_to_model(ContextObject))
123
+ ] = None
124
+ ```
125
+ """
126
+
127
+ def validator(value: Any) -> M | None:
128
+ if value is None:
129
+ return None
130
+ if isinstance(value, model_class):
131
+ return value
132
+ if isinstance(value, dict):
133
+ return model_class(**value)
134
+ return value # type: ignore
135
+
136
+ return validator
137
+
138
+
139
+ def coerce_subclass_list(base_class: type[M]) -> Callable[[Any], list[M] | None]:
140
+ """Create a validator that accepts lists containing subclass instances.
141
+
142
+ This addresses Python's list invariance limitation where `list[Subclass]`
143
+ cannot be assigned to `list[BaseClass]` despite being type-safe at runtime.
144
+
145
+ The validator:
146
+ 1. Accepts Any as input (satisfies mypy for subclass lists)
147
+ 2. Validates each item is an instance of base_class (or subclass)
148
+ 3. Returns list[base_class] (satisfies the field type)
149
+
150
+ Args:
151
+ base_class: The base Pydantic model class for list items.
152
+
153
+ Returns:
154
+ A validator function for use with Pydantic's BeforeValidator.
155
+
156
+ Example:
157
+ ```python
158
+ class ExtendedCreative(CreativeAsset):
159
+ internal_id: str = Field(exclude=True)
160
+
161
+ # Without coercion: requires cast()
162
+ # PackageRequest(creatives=cast(list[CreativeAsset], [extended]))
163
+
164
+ # With coercion: just works
165
+ PackageRequest(creatives=[extended]) # No cast needed!
166
+ ```
167
+ """
168
+
169
+ def validator(value: Any) -> list[M] | None:
170
+ if value is None:
171
+ return None
172
+ if not isinstance(value, (list, tuple)):
173
+ return value # type: ignore
174
+ # Return the list as-is - Pydantic will validate each item
175
+ # is an instance of base_class (including subclasses)
176
+ return list(value)
177
+
178
+ return validator
179
+
180
+
181
+ # =============================================================================
182
+ # List Variance Notes
183
+ # =============================================================================
184
+ #
185
+ # The coerce_subclass_list validator above handles the common case of passing
186
+ # `list[Subclass]` to a field expecting `list[BaseClass]` when constructing
187
+ # request models.
188
+ #
189
+ # For function signatures in your own code, use Sequence[T] which is covariant:
190
+ #
191
+ # from collections.abc import Sequence
192
+ # def process_creatives(creatives: Sequence[CreativeAsset]) -> None:
193
+ # ... # Accepts list[ExtendedCreative] without cast()
194
+ #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 2.14.0
3
+ Version: 2.16.0
4
4
  Summary: Official Python client for the Ad Context Protocol (AdCP)
5
5
  Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
6
  License: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  adcp/ADCP_VERSION,sha256=cy9k2HT5B4jAGZsMGb_Zs7ZDbhQBFOU-RigyUy10xhw,6
2
- adcp/__init__.py,sha256=Hm0x0KmPso2Y7Rp2kbMQPGkO6Js-XqFx0e_dLVaJep0,10319
2
+ adcp/__init__.py,sha256=SVea2r6sPQTljwVo_f6PNFSDJNYnt1DQ-oSOJEohWqw,10319
3
3
  adcp/__main__.py,sha256=8v-j_W9IIWDIcvhaR1yuZAhBCDRfehUyN0tusstzyfQ,15139
4
4
  adcp/adagents.py,sha256=HY7vZ8QqC8Zjzc_jRA0ZMhAfvm1pJN_VJfS8a8Fbc6c,24481
5
5
  adcp/client.py,sha256=gdDnyCyotRKMBHgxyONq3BWr4Seo7U2KZE18gqpw3nU,50839
@@ -15,10 +15,12 @@ adcp/protocols/base.py,sha256=oGUe3Ce_gupLYgZqQsgkerkiRlBKw4KBfqCe75UMszw,6485
15
15
  adcp/protocols/mcp.py,sha256=kCKRDhnahRI5nCE9p7FmhrayfMcKUe0h6uYbuSZ4SDE,22642
16
16
  adcp/testing/__init__.py,sha256=ZWp_floWjVZfy8RBG5v_FUXQ8YbN7xjXvVcX-_zl_HU,1416
17
17
  adcp/testing/test_helpers.py,sha256=-UKuxxyKQald5EvXxguQH34b3J0JdsxKH_nRT6GTjkQ,10029
18
- adcp/types/__init__.py,sha256=aN5IyR3fecA356i2EojHTOHVrO8ymBAu0qqKO9P_thU,13101
18
+ adcp/types/__init__.py,sha256=9f_6R333b2rGA6nXHk4FeVw40nh8bOcCQ7iasAgd8aA,13746
19
+ adcp/types/_ergonomic.py,sha256=EYC4ltYIOGgZ8_f_eEecanr-uLyQgq8d2yAZnsnsoQU,15979
19
20
  adcp/types/_generated.py,sha256=Nd6vMiIJ8m0wRbD79dY5XTFmh0LbHBlvHIkLGQw0u7I,23268
20
21
  adcp/types/aliases.py,sha256=DDc-AwFJfaDce84Mkq57KFIPgfK1knoiA_OOkrmDN7A,26114
21
22
  adcp/types/base.py,sha256=Xr0cwbjG4tRPLbDTX9lucGty6_Y_S0a5C_ve0n7umc8,8227
23
+ adcp/types/coercion.py,sha256=gCxMvxcj7sTx75AgEMkdN-xjlYBBH_d9xnvE76NEe4c,6081
22
24
  adcp/types/core.py,sha256=RXkKCWCXS9BVJTNpe3Opm5O1I_LaQPMUuVwa-ipvS1Q,4839
23
25
  adcp/types/generated_poc/__init__.py,sha256=bgFFvPK1-e04eOnyw0qmtVMzoA2V7GeAMPDVrx-VIwA,103
24
26
  adcp/types/generated_poc/adagents.py,sha256=a9yOFnBVwmopy28eEp1CdclBRjyzrkV7OvL1wtkojBE,16069
@@ -184,9 +186,9 @@ adcp/utils/__init__.py,sha256=uetvSJB19CjQbtwEYZiTnumJG11GsafQmXm5eR3hL7E,153
184
186
  adcp/utils/operation_id.py,sha256=wQX9Bb5epXzRq23xoeYPTqzu5yLuhshg7lKJZihcM2k,294
185
187
  adcp/utils/preview_cache.py,sha256=fy792IGXX9385FGsOhyuN1ZjoagsCstmcC1a8HAwtlI,18771
186
188
  adcp/utils/response_parser.py,sha256=uPk2vIH-RYZmq7y3i8lC4HTMQ3FfKdlgXKTjgJ1955M,6253
187
- adcp-2.14.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
188
- adcp-2.14.0.dist-info/METADATA,sha256=aV-lmZXLWGgZic7uJmIsnWHyLhyMuRsNCyfFAY02RcA,31359
189
- adcp-2.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
190
- adcp-2.14.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
191
- adcp-2.14.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
192
- adcp-2.14.0.dist-info/RECORD,,
189
+ adcp-2.16.0.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
190
+ adcp-2.16.0.dist-info/METADATA,sha256=eZC137xJ4Wd7Ka6kCyxPsE3iHWMMYqdmavn2mjP-Ab8,31359
191
+ adcp-2.16.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
192
+ adcp-2.16.0.dist-info/entry_points.txt,sha256=DQKpcGsJX8DtVI_SGApQ7tNvqUB4zkTLaTAEpFgmi3U,44
193
+ adcp-2.16.0.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
194
+ adcp-2.16.0.dist-info/RECORD,,
File without changes