Skip to content

Commit a53bfc1

Browse files
bokelleyclaude
andcommitted
merge: integrate PR #160 oneOf flattening with RC2 governance
Merge origin/main (including PR #160's validation-only oneOf flattening) into the governance-sdk branch. Regenerated types from RC2 schemas, removed stale SubAsset types (schema removed in RC2), fixed list field shadowing in GetPropertyListResponse, and updated tests for flattened GetPlanAuditLogsRequest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 1292c23 + 3171a95 commit a53bfc1

33 files changed

Lines changed: 422 additions & 1415 deletions

scripts/generate_ergonomic_coercion.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@
5050
("GetProductsRequest1", "media_buy.get_products_request"),
5151
("GetProductsRequest2", "media_buy.get_products_request"),
5252
("GetProductsRequest3", "media_buy.get_products_request"),
53-
("PackageUpdate1", "media_buy.package_update"),
54-
("PackageUpdate2", "media_buy.package_update"),
53+
("PackageUpdate", "media_buy.package_update"),
5554
]
5655

5756
# Types that should get subclass_list coercion (for list variance)
@@ -253,7 +252,7 @@ def generate_code() -> str:
253252
)
254253
from adcp.types.generated_poc.creative.list_creatives_response import ListCreativesResponse
255254
from adcp.types.generated_poc.media_buy.package_request import PackageRequest
256-
from adcp.types.generated_poc.media_buy.package_update import PackageUpdate1, PackageUpdate2
255+
from adcp.types.generated_poc.media_buy.package_update import PackageUpdate
257256

258257
# Map names to classes
259258
request_classes = {
@@ -276,8 +275,7 @@ def generate_code() -> str:
276275
"GetProductsRequest1": GetProductsRequest1,
277276
"GetProductsRequest2": GetProductsRequest2,
278277
"GetProductsRequest3": GetProductsRequest3,
279-
"PackageUpdate1": PackageUpdate1,
280-
"PackageUpdate2": PackageUpdate2,
278+
"PackageUpdate": PackageUpdate,
281279
}
282280

283281
# Analyze all types
@@ -396,10 +394,7 @@ def generate_code() -> str:
396394
lines.append(" Sort,")
397395
lines.append(")")
398396
lines.append("from adcp.types.generated_poc.media_buy.package_request import PackageRequest")
399-
lines.append("from adcp.types.generated_poc.media_buy.package_update import (")
400-
lines.append(" PackageUpdate1,")
401-
lines.append(" PackageUpdate2,")
402-
lines.append(")")
397+
lines.append("from adcp.types.generated_poc.media_buy.package_update import PackageUpdate")
403398

404399
# Add response type imports
405400
lines.append("from adcp.types.generated_poc.media_buy.create_media_buy_response import (")
@@ -443,8 +438,7 @@ def generate_code() -> str:
443438
"GetProductsRequest3",
444439
"PackageRequest",
445440
"CreateMediaBuyRequest",
446-
"PackageUpdate1",
447-
"PackageUpdate2",
441+
"PackageUpdate",
448442
# Response types
449443
"GetProductsResponse",
450444
"ListCreativesResponse",
@@ -539,8 +533,7 @@ def generate_code() -> str:
539533
lines.append(f" {type_name}.model_rebuild(force=True)")
540534
lines.append("")
541535

542-
# Handle PackageUpdate1 and PackageUpdate2 together if they have same coercions
543-
# (they're already handled in the loop above)
536+
# PackageUpdate is now a single class (flattened from validation-only oneOf)
544537

545538
# Add helper function
546539
lines.append("")

scripts/generate_types.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,63 @@ def rewrite_refs(obj, current_schema_rel_path: Path):
7070
return obj
7171

7272

73+
def flatten_validation_oneof(schema: dict) -> dict:
74+
"""Flatten anyOf/oneOf that only express required-field constraints.
75+
76+
JSON Schema uses anyOf/oneOf with required-only branches to express
77+
"at least one of these field groups must be set." datamodel-code-generator
78+
misinterprets this as a type union, generating separate variant classes
79+
(e.g., FrequencyCap1, FrequencyCap2, FrequencyCap3) plus a RootModel wrapper.
80+
81+
This function detects the pattern and removes the anyOf/oneOf, keeping
82+
only the intersection of required fields so a single class is generated.
83+
84+
Follow-up to #155: enables consumer subclassing without RootModel or
85+
Union type alias barriers.
86+
"""
87+
if "properties" not in schema:
88+
return schema
89+
90+
branch_key = None
91+
branches = None
92+
for key in ("anyOf", "oneOf"):
93+
if key in schema:
94+
branch_key = key
95+
branches = schema[key]
96+
break
97+
98+
if not branches:
99+
return schema
100+
101+
# All branches must contain only 'required' (and optionally 'not')
102+
if not all(set(b.keys()) <= {"required", "not"} for b in branches):
103+
return schema
104+
105+
# All branches are required-only — this is a validation constraint, not a type union
106+
# Compute the intersection of required fields (fields required in ALL branches)
107+
branch_required = [set(b.get("required", [])) for b in branches]
108+
always_required = set.intersection(*branch_required) if branch_required else set()
109+
110+
# Include any top-level required fields
111+
top_required = set(schema.get("required", []))
112+
always_required |= top_required
113+
114+
title = schema.get("title", "unknown")
115+
branch_count = len(branches)
116+
117+
# Remove the anyOf/oneOf
118+
del schema[branch_key]
119+
120+
# Set required to the intersection (or remove if empty)
121+
if always_required:
122+
schema["required"] = sorted(always_required)
123+
elif "required" in schema:
124+
del schema["required"]
125+
126+
print(f" flattened {branch_key} ({branch_count} branches) in {title}")
127+
return schema
128+
129+
73130
def flatten_schemas():
74131
"""
75132
Copy schemas to temp directory, preserving directory structure.
@@ -113,6 +170,9 @@ def flatten_schemas():
113170
# Rewrite $ref paths: convert absolute paths to relative, hyphens to underscores
114171
schema = rewrite_refs(schema, rel_path)
115172

173+
# Flatten validation-only anyOf/oneOf into single-class schemas
174+
schema = flatten_validation_oneof(schema)
175+
116176
with open(output_file, "w") as f:
117177
json.dump(schema, f, indent=2)
118178

@@ -361,6 +421,16 @@ def main():
361421
restore_unchanged_files()
362422

363423
# Generate ergonomic coercion module (type coercion for better API ergonomics)
424+
# Reset _ergonomic.py first — the old version may import variant classes
425+
# that no longer exist after schema flattening (e.g., PackageUpdate1).
426+
ergonomic_file = REPO_ROOT / "src" / "adcp" / "types" / "_ergonomic.py"
427+
if ergonomic_file.exists():
428+
ergonomic_file.write_text(
429+
'"""Auto-generated ergonomic coercion — regenerating..."""\n'
430+
"\ndef apply_ergonomic_coercion() -> None:\n"
431+
" pass\n"
432+
)
433+
364434
ergonomic_script = REPO_ROOT / "scripts" / "generate_ergonomic_coercion.py"
365435
if ergonomic_script.exists():
366436
print("\nGenerating ergonomic coercion module...")

scripts/post_generate_fixes.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,13 @@ def fix_constr_type_annotations():
298298

299299

300300
# Types to unwrap from RootModel to Union type alias.
301-
# ALL Request/Response types are unwrapped so consumers can subclass them
302-
# with model_config overrides (extra='forbid', custom validators, etc.).
303-
# Value-type RootModels (PricingOption, Destination, etc.) keep the RootModel
304-
# wrapper + __getattr__ proxy since nobody subclasses them.
301+
# Only genuine discriminated unions (different field shapes per variant) belong here.
302+
# "Validation-only" oneOf types (same fields, different required combos) are now
303+
# handled at the schema level by flatten_validation_oneof() in generate_types.py,
304+
# which produces a single BaseModel class — no RootModel or unwrapping needed.
305+
# Removed from this set (now single classes): GetCreativeDeliveryRequest,
306+
# GetSignalsRequest, ProvidePerformanceFeedbackRequest, SiSendMessageRequest,
307+
# UpdateMediaBuyRequest.
305308
# See: https://github.com/adcontextprotocol/adcp-client-python/issues/155
306309
_UNWRAP_TO_UNION: set[str] = {
307310
"AcquireRightsResponse",
@@ -315,27 +318,22 @@ def fix_constr_type_annotations():
315318
"GetAccountFinancialsResponse",
316319
"GetBrandIdentityResponse",
317320
"GetContentStandardsResponse",
318-
"GetCreativeDeliveryRequest",
319321
"GetCreativeFeaturesResponse",
320322
"GetMediaBuyArtifactsResponse",
321323
"GetPlanAuditLogsRequest",
322324
"GetProductsRequest",
323325
"GetRightsResponse",
324-
"GetSignalsRequest",
325326
"ListContentStandardsResponse",
326327
"LogEventResponse",
327328
"PreviewCreativeRequest",
328329
"PreviewCreativeResponse",
329-
"ProvidePerformanceFeedbackRequest",
330330
"ProvidePerformanceFeedbackResponse",
331-
"SiSendMessageRequest",
332331
"SyncAccountsResponse",
333332
"SyncAudiencesResponse",
334333
"SyncCatalogsResponse",
335334
"SyncCreativesResponse",
336335
"SyncEventSourcesResponse",
337336
"UpdateContentStandardsResponse",
338-
"UpdateMediaBuyRequest",
339337
"UpdateMediaBuyResponse",
340338
"UpdateRightsResponse",
341339
"ValidateContentDeliveryResponse",
@@ -526,6 +524,42 @@ def add_rootmodel_getattr_proxy():
526524
print(" No RootModel union types needed __getattr__ proxy")
527525

528526

527+
def fix_list_field_shadowing():
528+
"""Fix models where a field named 'list' shadows the builtin list type.
529+
530+
GetPropertyListResponse has a field named 'list' which shadows the builtin
531+
list type in annotations like list[Identifier]. We add a _list = list alias
532+
before the class and replace bare list[] usage in annotations.
533+
"""
534+
target = OUTPUT_DIR / "property" / "get_property_list_response.py"
535+
if not target.exists():
536+
return
537+
538+
content = target.read_text()
539+
if "_list = list" in content:
540+
return # Already fixed
541+
542+
# Add alias before the class definition
543+
content = content.replace(
544+
"\n\nclass GetPropertyListResponse(",
545+
"\n\n_list = list # alias to avoid shadowing by field name\n\n\nclass GetPropertyListResponse(",
546+
)
547+
548+
# Replace bare list[] in annotations (but not the 'list' field itself)
549+
# Only replace list[ when used as a type annotation, not as a field name
550+
import re
551+
552+
# Replace list[identifier...] and dict[str, list[identifier...]] patterns
553+
content = re.sub(
554+
r'(?<![._a-zA-Z])list\[identifier\.',
555+
'_list[identifier.',
556+
content,
557+
)
558+
559+
target.write_text(content)
560+
print(" Fixed list field shadowing in get_property_list_response.py")
561+
562+
529563
def main():
530564
"""Apply all post-generation fixes."""
531565
print("Applying post-generation fixes...")
@@ -539,6 +573,7 @@ def main():
539573
fix_constr_type_annotations()
540574
unwrap_rootmodel_unions()
541575
add_rootmodel_getattr_proxy()
576+
fix_list_field_shadowing()
542577

543578
print("\n✓ Post-generation fixes complete\n")
544579

src/adcp/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@
294294
ListContentStandardsSuccessResponse,
295295
LogEventErrorResponse,
296296
LogEventSuccessResponse,
297-
MediaSubAsset,
298297
PercentOfMediaSignalPricingOption,
299298
PlatformDeployment,
300299
PlatformDestination,
@@ -331,7 +330,6 @@
331330
SyncCreativesSuccessResponse,
332331
SyncEventSourcesErrorResponse,
333332
SyncEventSourcesSuccessResponse,
334-
TextSubAsset,
335333
UpdateContentStandardsErrorResponse,
336334
UpdateContentStandardsSuccessResponse,
337335
UpdateMediaBuyErrorResponse,
@@ -698,7 +696,6 @@ def get_adcp_version() -> str:
698696
"ListContentStandardsErrorResponse",
699697
"LogEventSuccessResponse",
700698
"LogEventErrorResponse",
701-
"MediaSubAsset",
702699
"PlatformDeployment",
703700
"PlatformDestination",
704701
"PreviewCreativeBatchRequest",
@@ -734,7 +731,6 @@ def get_adcp_version() -> str:
734731
"SyncCreativesErrorResponse",
735732
"SyncEventSourcesSuccessResponse",
736733
"SyncEventSourcesErrorResponse",
737-
"TextSubAsset",
738734
"UpdateContentStandardsSuccessResponse",
739735
"UpdateContentStandardsErrorResponse",
740736
"UpdateMediaBuySuccessResponse",

src/adcp/types/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
GetProductsResponse,
188188
GetPropertyListRequest,
189189
GetPropertyListResponse,
190+
GetSignalsRequest,
190191
GetSignalsResponse,
191192
Gtin,
192193
HtmlAsset,
@@ -403,7 +404,6 @@
403404
GetProductsWholesaleRequest,
404405
GetSignalsDiscoveryRequest,
405406
GetSignalsLookupRequest,
406-
GetSignalsRequest,
407407
HtmlPreviewRender,
408408
InlineDaastAsset,
409409
InlineVastAsset,
@@ -413,7 +413,6 @@
413413
LogEventErrorResponse,
414414
LogEventSuccessResponse,
415415
MediaBuyDeliveryStatus,
416-
MediaSubAsset,
417416
PercentOfMediaSignalPricingOption,
418417
PlatformDeployment,
419418
PlatformDestination,
@@ -450,7 +449,6 @@
450449
SyncCreativesSuccessResponse,
451450
SyncEventSourcesErrorResponse,
452451
SyncEventSourcesSuccessResponse,
453-
TextSubAsset,
454452
UpdateContentStandardsErrorResponse,
455453
UpdateContentStandardsSuccessResponse,
456454
UpdateMediaBuyErrorResponse,
@@ -923,7 +921,6 @@
923921
"ListContentStandardsSuccessResponse",
924922
"LogEventErrorResponse",
925923
"LogEventSuccessResponse",
926-
"MediaSubAsset",
927924
"PlatformDeployment",
928925
"PlatformDestination",
929926
"PreviewCreativeBatchRequest",
@@ -957,7 +954,6 @@
957954
"SyncCreativeResult",
958955
"SyncEventSourcesErrorResponse",
959956
"SyncEventSourcesSuccessResponse",
960-
"TextSubAsset",
961957
"UpdateContentStandardsErrorResponse",
962958
"UpdateContentStandardsSuccessResponse",
963959
"UpdateMediaBuyErrorResponse",

0 commit comments

Comments
 (0)