Skip to content

Commit c111ba6

Browse files
bokelleysangilish
andauthored
fix(types): add update media buy response coercion (#864)
Co-authored-by: sangilish <56685007+sangilish@users.noreply.github.com>
1 parent 6fdcb77 commit c111ba6

3 files changed

Lines changed: 138 additions & 41 deletions

File tree

scripts/generate_ergonomic_coercion.py

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"ListCreativesResponse",
4242
"ListCreativeFormatsResponse",
4343
"CreateMediaBuyResponse1",
44+
"UpdateMediaBuyResponse1",
4445
"GetMediaBuyDeliveryResponse",
4546
]
4647

@@ -227,25 +228,45 @@ def get_symbol_name(cls) -> str:
227228
def generate_code() -> str:
228229
"""Generate the _ergonomic.py module content."""
229230
# Import all the types we need to analyze
231+
from pydantic import BaseModel as _PydBaseModel
232+
233+
from adcp.types.generated_poc.creative.list_creatives_request import (
234+
ListCreativesRequest,
235+
Sort,
236+
)
237+
from adcp.types.generated_poc.creative.list_creatives_response import ListCreativesResponse
230238
from adcp.types.generated_poc.media_buy import (
231239
create_media_buy_response as _cmbr_module,
232240
)
241+
from adcp.types.generated_poc.media_buy import (
242+
update_media_buy_response as _umbr_module,
243+
)
233244
from adcp.types.generated_poc.media_buy.create_media_buy_request import CreateMediaBuyRequest
234245
from adcp.types.generated_poc.media_buy.get_media_buy_delivery_response import (
235246
GetMediaBuyDeliveryResponse,
236247
)
248+
from adcp.types.generated_poc.media_buy.get_products_request import (
249+
GetProductsRequest,
250+
)
251+
from adcp.types.generated_poc.media_buy.get_products_response import GetProductsResponse
252+
from adcp.types.generated_poc.media_buy.list_creative_formats_request import (
253+
ListCreativeFormatsRequest,
254+
)
255+
from adcp.types.generated_poc.media_buy.list_creative_formats_response import (
256+
ListCreativeFormatsResponse,
257+
)
258+
from adcp.types.generated_poc.media_buy.package_request import PackageRequest
259+
from adcp.types.generated_poc.media_buy.package_update import PackageUpdate
237260

238-
# Resolve the CreateMediaBuyResponse success variant. Different
261+
# Resolve success variants. Different
239262
# datamodel-codegen versions emit the variants under shifting names:
240-
# sometimes `CreateMediaBuyResponse1`/`...2`, sometimes `CreateMediaBuyResponse`
263+
# sometimes `FooResponse1`/`...2`, sometimes `FooResponse`
241264
# (success, unnumbered) + `CreateMediaBuyResponseN`. Find the success
242265
# variant by scanning the module for a pydantic model whose fields include
243266
# the success-only `media_buy_id` key.
244-
from pydantic import BaseModel as _PydBaseModel
245-
246-
def _find_success_variant() -> type[_PydBaseModel] | None:
247-
for name in dir(_cmbr_module):
248-
obj = getattr(_cmbr_module, name)
267+
def _find_media_buy_success_variant(module: Any) -> type[_PydBaseModel] | None:
268+
for name in dir(module):
269+
obj = getattr(module, name)
249270
if (
250271
isinstance(obj, type)
251272
and issubclass(obj, _PydBaseModel)
@@ -254,28 +275,8 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
254275
return obj
255276
return None
256277

257-
CreateMediaBuyResponse1 = _find_success_variant()
258-
from adcp.types.generated_poc.media_buy.get_products_request import (
259-
Field1 as GetProductsField,
260-
GetProductsRequest,
261-
)
262-
263-
# Response types
264-
from adcp.types.generated_poc.media_buy.get_products_response import GetProductsResponse
265-
from adcp.types.generated_poc.media_buy.list_creative_formats_request import (
266-
ListCreativeFormatsRequest,
267-
)
268-
from adcp.types.generated_poc.media_buy.list_creative_formats_response import (
269-
ListCreativeFormatsResponse,
270-
)
271-
from adcp.types.generated_poc.creative.list_creatives_request import (
272-
Field1 as ListCreativesField,
273-
ListCreativesRequest,
274-
Sort,
275-
)
276-
from adcp.types.generated_poc.creative.list_creatives_response import ListCreativesResponse
277-
from adcp.types.generated_poc.media_buy.package_request import PackageRequest
278-
from adcp.types.generated_poc.media_buy.package_update import PackageUpdate
278+
create_media_buy_response1 = _find_media_buy_success_variant(_cmbr_module)
279+
update_media_buy_response1 = _find_media_buy_success_variant(_umbr_module)
279280

280281
# Map names to classes
281282
request_classes = {
@@ -291,8 +292,10 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
291292
"ListCreativeFormatsResponse": ListCreativeFormatsResponse,
292293
"GetMediaBuyDeliveryResponse": GetMediaBuyDeliveryResponse,
293294
}
294-
if CreateMediaBuyResponse1 is not None:
295-
response_classes["CreateMediaBuyResponse1"] = CreateMediaBuyResponse1
295+
if create_media_buy_response1 is not None:
296+
response_classes["CreateMediaBuyResponse1"] = create_media_buy_response1
297+
if update_media_buy_response1 is not None:
298+
response_classes["UpdateMediaBuyResponse1"] = update_media_buy_response1
296299

297300
nested_classes = {
298301
"Sort": Sort,
@@ -422,14 +425,22 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
422425
# Add response type imports. CreateMediaBuyResponse1's numeric suffix can
423426
# shift between codegen versions; in schema versions that collapse the
424427
# response envelope, there may be no success-specific response variant.
425-
if CreateMediaBuyResponse1 is not None:
426-
cmbr_name = CreateMediaBuyResponse1.__name__
428+
if create_media_buy_response1 is not None:
429+
cmbr_name = create_media_buy_response1.__name__
427430
lines.append("from adcp.types.generated_poc.media_buy.create_media_buy_response import (")
428431
if cmbr_name == "CreateMediaBuyResponse1":
429432
lines.append(" CreateMediaBuyResponse1,")
430433
else:
431434
lines.append(f" {cmbr_name} as CreateMediaBuyResponse1,")
432435
lines.append(")")
436+
if update_media_buy_response1 is not None:
437+
umbr_name = update_media_buy_response1.__name__
438+
lines.append("from adcp.types.generated_poc.media_buy.update_media_buy_response import (")
439+
if umbr_name == "UpdateMediaBuyResponse1":
440+
lines.append(" UpdateMediaBuyResponse1,")
441+
else:
442+
lines.append(f" {umbr_name} as UpdateMediaBuyResponse1,")
443+
lines.append(")")
433444
lines.append("from adcp.types.generated_poc.media_buy.get_media_buy_delivery_response import (")
434445
lines.append(" GetMediaBuyDeliveryResponse,")
435446
lines.append(" MediaBuyDelivery,")
@@ -461,6 +472,7 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
461472
"PackageRequest",
462473
"PackageUpdate",
463474
"CreateMediaBuyResponse1",
475+
"UpdateMediaBuyResponse1",
464476
"GetMediaBuyDeliveryResponse",
465477
"MediaBuyDelivery",
466478
"NotificationType",
@@ -500,6 +512,7 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
500512
"ListCreativesResponse",
501513
"ListCreativeFormatsResponse",
502514
"CreateMediaBuyResponse1",
515+
"UpdateMediaBuyResponse1",
503516
"GetMediaBuyDeliveryResponse",
504517
]
505518

@@ -523,8 +536,16 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
523536
elif c["type"] == "ext":
524537
field_comments.append(f'{c["field"]}: ExtensionObject | dict | None')
525538
elif c["type"] == "subclass_list":
539+
from collections.abc import Sequence as AbcSequence
540+
541+
field_info = cls.model_fields[c["field"]]
542+
ann = field_info.annotation
543+
base_ann = get_base_type(ann)
544+
is_seq = get_origin(base_ann if base_ann is not None else ann) is AbcSequence
545+
container = "Sequence" if is_seq else "list"
526546
field_comments.append(
527-
f'{c["field"]}: list[{c["target_class"].__name__}] (accepts subclass instances)'
547+
f'{c["field"]}: {container}[{c["target_class"].__name__}] '
548+
"(accepts subclass instances)"
528549
)
529550

530551
lines.append(f" # Apply coercion to {type_name}")
@@ -540,7 +561,8 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
540561
lines.append(f" {type_name},")
541562
lines.append(f' "{field}",')
542563
lines.append(
543-
f" Annotated[{target} | None, BeforeValidator(coerce_to_enum({target}))],"
564+
f" Annotated[{target} | None, "
565+
f"BeforeValidator(coerce_to_enum({target}))],"
544566
)
545567
lines.append(" )")
546568
elif c["type"] == "enum_list":
@@ -558,15 +580,17 @@ def _find_success_variant() -> type[_PydBaseModel] | None:
558580
lines.append(f" {type_name},")
559581
lines.append(f' "{field}",')
560582
lines.append(
561-
" Annotated[ContextObject | None, BeforeValidator(coerce_to_model(ContextObject))],"
583+
" Annotated[ContextObject | None, "
584+
"BeforeValidator(coerce_to_model(ContextObject))],"
562585
)
563586
lines.append(" )")
564587
elif c["type"] == "ext":
565588
lines.append(" _patch_field_annotation(")
566589
lines.append(f" {type_name},")
567590
lines.append(f' "{field}",')
568591
lines.append(
569-
" Annotated[ExtensionObject | None, BeforeValidator(coerce_to_model(ExtensionObject))],"
592+
" Annotated[ExtensionObject | None, "
593+
"BeforeValidator(coerce_to_model(ExtensionObject))],"
570594
)
571595
lines.append(" )")
572596
elif c["type"] == "subclass_list":

src/adcp/types/_ergonomic.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
from adcp.types.generated_poc.media_buy.create_media_buy_response import (
8585
CreateMediaBuyResponse1,
8686
)
87+
from adcp.types.generated_poc.media_buy.update_media_buy_response import (
88+
UpdateMediaBuyResponse1,
89+
)
8790
from adcp.types.generated_poc.media_buy.get_media_buy_delivery_response import (
8891
GetMediaBuyDeliveryResponse,
8992
MediaBuyDelivery,
@@ -238,7 +241,7 @@ def _apply_coercion() -> None:
238241
# Apply coercion to PackageRequest
239242
# - pacing: Pacing | str | None
240243
# - creative_assignments: list[CreativeAssignment] (accepts subclass instances)
241-
# - creatives: list[CreativeAsset] (accepts subclass instances)
244+
# - creatives: Sequence[CreativeAsset] (accepts subclass instances)
242245
# - context: ContextObject | dict | None
243246
# - ext: ExtensionObject | dict | None
244247
_patch_field_annotation(
@@ -275,7 +278,7 @@ def _apply_coercion() -> None:
275278
PackageRequest.model_rebuild(force=True)
276279

277280
# Apply coercion to CreateMediaBuyRequest
278-
# - packages: list[PackageRequest] (accepts subclass instances)
281+
# - packages: Sequence[PackageRequest] (accepts subclass instances)
279282
# - advertiser_industry: AdvertiserIndustry | str | None
280283
# - context: ContextObject | dict | None
281284
# - ext: ExtensionObject | dict | None
@@ -391,7 +394,7 @@ def _apply_coercion() -> None:
391394
# Apply coercion to ListCreativesResponse
392395
# - context: ContextObject | dict | None
393396
# - status: TaskStatus | str | None
394-
# - creatives: list[Creative] (accepts subclass instances)
397+
# - creatives: Sequence[Creative] (accepts subclass instances)
395398
# - errors: list[Error] (accepts subclass instances)
396399
# - ext: ExtensionObject | dict | None
397400
_patch_field_annotation(
@@ -499,11 +502,38 @@ def _apply_coercion() -> None:
499502
)
500503
CreateMediaBuyResponse1.model_rebuild(force=True)
501504

505+
# Apply coercion to UpdateMediaBuyResponse1
506+
# - affected_packages: Sequence[Package] (accepts subclass instances)
507+
# - packages: list[Package] (accepts subclass instances)
508+
# - media_buy_status: MediaBuyStatus | str | None
509+
_patch_field_annotation(
510+
UpdateMediaBuyResponse1,
511+
"affected_packages",
512+
Annotated[
513+
Sequence[Package] | None,
514+
BeforeValidator(coerce_subclass_list(Package)),
515+
],
516+
)
517+
_patch_field_annotation(
518+
UpdateMediaBuyResponse1,
519+
"packages",
520+
Annotated[
521+
list[Package] | None,
522+
BeforeValidator(coerce_subclass_list(Package)),
523+
],
524+
)
525+
_patch_field_annotation(
526+
UpdateMediaBuyResponse1,
527+
"media_buy_status",
528+
Annotated[MediaBuyStatus | None, BeforeValidator(coerce_to_enum(MediaBuyStatus))],
529+
)
530+
UpdateMediaBuyResponse1.model_rebuild(force=True)
531+
502532
# Apply coercion to GetMediaBuyDeliveryResponse
503533
# - context: ContextObject | dict | None
504534
# - status: TaskStatus | str | None
505535
# - notification_type: NotificationType | str | None
506-
# - media_buy_deliveries: list[MediaBuyDelivery] (accepts subclass instances)
536+
# - media_buy_deliveries: Sequence[MediaBuyDelivery] (accepts subclass instances)
507537
# - errors: list[Error] (accepts subclass instances)
508538
# - ext: ExtensionObject | dict | None
509539
_patch_field_annotation(

tests/test_type_coercion.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,49 @@ class ExtendedPackage(Package):
435435
# Internal field is preserved at runtime
436436
assert response.packages[0].campaign_id == "campaign-456" # type: ignore[attr-defined]
437437

438+
def test_update_media_buy_response_accepts_package_subclasses(self):
439+
"""UpdateMediaBuySuccessResponse package fields accept Package subclasses."""
440+
from pydantic import Field
441+
442+
from adcp.types import Package, UpdateMediaBuySuccessResponse
443+
444+
class ExtendedPackage(Package):
445+
"""Extended with internal tracking fields."""
446+
447+
campaign_id: str | None = Field(None, exclude=True)
448+
449+
package = ExtendedPackage(
450+
package_id="pkg1",
451+
campaign_id="campaign-456",
452+
)
453+
454+
response = UpdateMediaBuySuccessResponse(
455+
media_buy_id="mb1",
456+
buyer_ref="buyer-ref",
457+
packages=[package], # type: ignore[list-item] # Ignoring due to Python list covariance limitation
458+
affected_packages=[package],
459+
)
460+
461+
assert response.packages is not None
462+
assert response.affected_packages is not None
463+
assert response.packages[0].package_id == "pkg1"
464+
assert response.affected_packages[0].package_id == "pkg1"
465+
assert response.packages[0].campaign_id == "campaign-456" # type: ignore[attr-defined]
466+
assert (
467+
response.affected_packages[0].campaign_id == "campaign-456" # type: ignore[attr-defined]
468+
)
469+
470+
def test_update_media_buy_response_accepts_media_buy_status_string(self):
471+
"""UpdateMediaBuySuccessResponse.media_buy_status accepts strings."""
472+
from adcp.types import MediaBuyStatus, UpdateMediaBuySuccessResponse
473+
474+
response = UpdateMediaBuySuccessResponse(
475+
media_buy_id="mb1",
476+
media_buy_status="active",
477+
)
478+
479+
assert response.media_buy_status == MediaBuyStatus.active
480+
438481
def test_get_media_buy_delivery_response_accepts_dict_context(self):
439482
"""GetMediaBuyDeliveryResponse.context accepts dict."""
440483
from datetime import datetime, timezone

0 commit comments

Comments
 (0)