Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 81 additions & 77 deletions SCHEMA_DELTAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,94 @@

## Files added

- `core/package_delivery_metric_value.py` — PackageDeliveryMetricValue, Qualifier
- `core/reporting_adjustment.py` — AccountingPeriod, ReasonCode, ReportingAdjustment
- `core/reporting_adjustment_receipt.py` — ReportingAdjustmentReceipt, ReportingAdjustmentRejectionCode, Status
- `core/reporting_file_object_ref.py` — ReportingFileObjectReference
- `core/reporting_ledger_changed_webhook.py` — ChangeKind, ReportingLedgerChangedWebhook
- `core/reporting_native_version_ref.py` — ReportingNativeVersionReference
- `core/reporting_reliability_statistics.py` — AdjustmentMagnitudeItem, Basis, Evidence, LatencyPercentiles, ReportingReliabilityMeasurementPeriod, ReportingReliabilityStatistics
- `enums/reach_aggregation.py` — ReachAggregation
- `enums/vendor_relationship.py` — VendorRelationship
- `account/sync_governance_response.py` — Account, GovernanceAgent, Status, SyncGovernanceResponse1, SyncGovernanceResponse2
- `brand/creative_approval_response.py` — CreativeApprovalResponse1, CreativeApprovalResponse2, CreativeApprovalResponse3, CreativeApprovalResponse4
- `compliance/comply_test_controller_response.py` — AttestationMode, ComplyResponseArm, ComplyTestControllerResponse1, ComplyTestControllerResponse2, ComplyTestControllerResponse3, ComplyTestControllerResponse4, ComplyTestControllerResponse5, ComplyTestControllerResponse6, ComplyTestControllerResponse7, ComplyTestControllerResponse8, Error, Forced, IdentifierMatchProof, Method, Purpose, RecordedCalls, RecordedCalls1, RecordedCalls2, Suggestion
- `content_standards/create_content_standards_response.py` — CreateContentStandardsResponse1, CreateContentStandardsResponse2
- `content_standards/list_content_standards_response.py` — ListContentStandardsResponse1, ListContentStandardsResponse2
- `content_standards/update_content_standards_response.py` — UpdateContentStandardsResponse1, UpdateContentStandardsResponse2

## Field changes

- `a2ui/si_catalog.py`
- **classes added**: Variant4
- **classes removed**: Variant2
- `account/list_account_changes_response.py`
- **classes added**: Status22
- `ListAccountChangesResponse`: `+status`
- `adagents.py`
- **classes added**: AuthorizedAgents10, AuthorizedAgents11, AuthorizedAgents12, AuthorizedAgents13, AuthorizedAgents15, AuthorizedAgents16, AuthorizedAgents17, AuthorizedAgents18, AuthorizedAgents19, AuthorizedAgents20, AuthorizedAgents22, AuthorizedAgents23, AuthorizedAgents24, AuthorizedAgents25, AuthorizedAgents26, AuthorizedAgents27, AuthorizedAgents29, AuthorizedAgents30, AuthorizedAgents31, AuthorizedAgents32, AuthorizedAgents33, AuthorizedAgents34, AuthorizedAgents36, AuthorizedAgents37, AuthorizedAgents38, AuthorizedAgents39, AuthorizedAgents40, AuthorizedAgents41, AuthorizedAgents43, AuthorizedAgents44, AuthorizedAgents45, AuthorizedAgents46, AuthorizedAgents47, AuthorizedAgents48, AuthorizedAgents8, AuthorizedAgents9
- `brand/search_brands_response.py`
- **classes added**: SearchBrandsResponse
- `bundled/protocol/get_adcp_capabilities_response.py`
- **classes added**: AdjustmentMagnitudeItem, Basis, Evidence, LatencyPercentiles, PeriodTimezonePolicy, ReliabilityStatistic, ReportingReliabilityMeasurementPeriod
- `Alignment`: `+source_timezone`
- `ReportingDelivery`: `+ledger_notification`, `+reliability_statistics`, `+reliable_reporting_version`, `+revision_content_task`
- `Schedule`: `+period_timezone_policy`
- `compliance/comply_test_controller_request.py`
- `Operation`: `+advance_within_retention`, `+probe_scheduler_dst`, `+publish_adjustment`, `+publish_nonempty`, `+publish_official_adjustment`, `+revoke_access`, `+suppress_readiness`
- `core/committed_metric.py`
- `CommittedMetric2`: `+methodology_version`
- **classes added**: CallerEventType, Disclosure6, GeoPostalAreasAdditionalPropertyEnum
- **classes removed**: Disclosure2
- `core/audience_evidence.py`
- **classes removed**: Subject17, Subject27
- `core/creative_representation.py`
- **classes removed**: Assets, CreativeRepresentation1, CreativeRepresentation2, CreativeRepresentation3, CreativeRepresentation4, CreativeRepresentation5
- `CreativeRepresentation`: `+format_kind`, `+representation_id`, `+source` `-root`
- `core/delivery_metric_aggregate.py`
- **classes added**: Field0
- **classes removed**: DeliveryMetricAggregate1
- `core/forecast_vendor_metric_value.py`
- `ForecastVendorMetricValue`: `+measurable_play_seconds`, `+measurable_plays`
- `core/notification_config.py`
- `EventType`: `+reporting_ledger_changed`
- `core/reporting_capabilities.py`
- `VendorMetric`: `+vendor_relationship`
- `core/reporting_delivery_capabilities.py`
- `ReportingDeliveryCapabilities`: `+ledger_notification`, `+reliability_statistics`, `+reliable_reporting_version`, `+revision_content_task`
- `core/reporting_file_entry.py`
- `ReportingFileEntry`: `+native_version_ref`
- `core/reporting_obligation.py`
- `ReportingObligation`: `+accepted_adjustment_receipt_count`, `+adjustment_count`, `+adjustment_receipt_count`, `+pending_adjustment_count`
- `core/reporting_receipt.py`
- `ReportingReceipt`: `+supersedes_reporting_receipt_id`
- `core/reporting_report_definition.py`
- **classes added**: ContractVersion
- `ReportCalendarTimezoneBasis`: `+schedule_timezone`
- `RestatementPolicy`: `+official_correction_mode`
- `core/reporting_revision.py`
- `ReportingRevision`: `+revision_content_sha256`
- `core/reporting_schedule.py`
- `ReportingScheduleAlignment`: `+source_timezone`
- `core/reporting_schedule_offering.py`
- **classes added**: PeriodTimezonePolicy
- `ReportingScheduleOffering`: `+period_timezone_policy`
- `core/reporting_status_issue.py`
- `Code`: `+ADJUSTMENT_RECEIPT_REJECTED`, `+ADJUSTMENT_RECEIPT_REQUIRED`, `+RECEIPT_REJECTED`, `+RECEIPT_REQUIRED`
- `core/vendor_metric_value.py`
- `VendorMetricValue`: `+measurable_play_seconds`, `+measurable_plays`, `+vendor_relationship`
- `core/x_entity_types.py`
- `XEntityTypes`: `+reporting_adjustment`
- `enums/notification_type.py`
- `NotificationType`: `+reporting_ledger_changed`
- **classes added**: DeliveryMetricAggregate1
- `core/format.py`
- **classes added**: Assets10, Assets9
- **classes removed**: Assets38, Assets39
- `Assets16`: `-requirements`
- `Assets18`: `+requirements`
- `Assets21`: `-requirements`
- `Assets23`: `+asset_group_id`, `+assets`, `+max_count`, `+min_count`, `+required`, `+selection_mode` `-asset_type`
- `Assets24`: `+root` `-asset_type`, `-item_type`, `-requirements`
- `Assets25`: `+asset_type`, `+requirements` `-asset_group_id`, `-assets`, `-item_type`, `-max_count`, `-min_count`, `-required`, `-selection_mode`
- `Assets26`: `+asset_type`, `+requirements` `-root`
- `Assets33`: `-requirements`
- `Assets35`: `+requirements`
- `core/package_format_snapshot.py`
- `PackageFormatSnapshot17`: `+format_kind`, `+params`
- `core/postal_area_support.py`
- **classes added**: PostalAreaSupportAdditionalPropertyEnum
- `core/product_format_declaration.py`
- **classes added**: ProductFormatDeclaration1, ProductFormatDeclaration10, ProductFormatDeclaration11, ProductFormatDeclaration12, ProductFormatDeclaration13, ProductFormatDeclaration14, ProductFormatDeclaration15, ProductFormatDeclaration16, ProductFormatDeclaration2, ProductFormatDeclaration3, ProductFormatDeclaration4, ProductFormatDeclaration5, ProductFormatDeclaration6, ProductFormatDeclaration7, ProductFormatDeclaration8, ProductFormatDeclaration9
- `ProductFormatDeclaration`: `+root` `-applies_to_channels`, `-canonical_formats_only`, `-display_name`, `-experimental`, `-format_option_id`, `-format_schema`, `-format_shape`, `-locale_policy`, `-macro_resolution_capabilities`, `-publisher_domain`, `-sample_render_url`, `-seller_preference`, `-technical_requirements_complete`, `-tracker_execution_contract`, `-v1_format_ref`
- `core/transformer.py`
- **classes removed**: InputFormat34, Transformer1, Transformer2
- `Transformer`: `+description`, `+input_format_ids`, `+input_formats`, `+metadata`, `+multiplicity`, `+name`, `+output_capability_ids`, `+output_format_ids`, `+params`, `+pricing_options`, `+transformer_id`, `+voice_synthesis_ref` `-root`
- `creative/list_creatives_response.py`
- **classes added**: Creative
- **classes removed**: AssignedPackage1, Assignments1, Creatives, Creatives1
- `creative/preview_creative_request.py`
- **classes added**: Input10
- **classes removed**: Input12
- `creative/sync_creatives_request.py`
- **classes removed**: Assets, Creative61, Creative62, Creative63, Input
- `Creative`: `+localization`, `+revision_id` `-root`
- `governance/check_governance_request.py`
- **classes added**: Subject8, Subject9
- `governance/sync_plans_response.py`
- **classes added**: Status45
- **classes removed**: Status44
- `media_buy/get_media_buy_delivery_request.py`
- `GetMediaBuyDeliveryRequest`: `+pagination`, `+reporting_revision_id`
- `media_buy/get_media_buy_delivery_response.py`
- **classes added**: ReportingRevisionBinding
- `AggregatedTotals`: `+reach_aggregation`
- `ByPackageItem`: `+metric_values`
- `ByPackageItem1`: `+currency`
- `GetMediaBuyDeliveryResponse`: `+pagination`, `+reporting_revision`, `+reporting_revision_binding`, `+reporting_rows`
- `MediaBuyDelivery`: `+currency`
- `Totals`: `-spend`
- `media_buy/get_reporting_status_request.py`
- `GetReportingStatusRequest`: `+changes_after`
- **classes added**: Status50
- **classes removed**: Status45
- `media_buy/decline_proposals_response.py`
- **classes added**: Results4, Results5
- `media_buy/get_reporting_status_response.py`
- `GetReportingStatusResponse`: `+adjustment_receipts`, `+adjustments`, `+changes_checkpoint`
- `media_buy/media_buy_delivery_webhook_result.py`
- `ByPackageItem`: `+metric_values`
- `MediaBuyDelivery`: `+currency`
- `Totals`: `-spend`
- **classes added**: Status
- `GetReportingStatusResponse`: `+status`
- `media_buy/package_request.py`
- `CommittedMetrics2`: `+methodology_version`
- `media_buy/sync_reporting_receipts_request.py`
- `SyncReportingReceiptsRequest`: `+adjustment_receipts`
- **classes removed**: Assets, Creative, Creative41, Creative42, Input
- `media_buy/package_update.py`
- **classes removed**: Assets, Creative, Creative51, Creative52, Input
- `media_buy/refine_proposals_response.py`
- **classes added**: Results11, Results16, Results17, Results18
- **classes removed**: Results13, Results6, Results7
- `Results12`: `+outcome`, `+proposal`, `+proposals`, `+reason`, `+reason_code`, `+source_proposal_id`, `+suggestions`, `+targeting_resolution`, `+unsatisfied_constraints`, `+unsatisfied_product_changes` `-root`
- `Results14`: `+root` `-outcome`, `-proposal`, `-proposals`, `-reason`, `-reason_code`, `-source_proposal_id`, `-suggestions`, `-targeting_resolution`, `-unsatisfied_constraints`, `-unsatisfied_product_changes`
- `Results8`: `+root` `-outcome`, `-proposal`, `-proposals`, `-reason`, `-reason_code`, `-source_proposal_id`, `-suggestions`, `-targeting_resolution`, `-unsatisfied_constraints`, `-unsatisfied_product_changes`
- `media_buy/sync_reporting_receipts_response.py`
- **classes added**: Results20, Results21
- **classes added**: Results22, Results23
- **classes removed**: Results18, Results19
- `Results20`: `+receipt` `-adjustment_receipt`
- `Results21`: `+receipt` `-adjustment_receipt`
- `protocol/sync_principal_response.py`
- **classes added**: Action32
- **classes removed**: Action29
- `sponsored_intelligence/si_sponsored_context_receipt.py`
- **classes added**: Status42
- **classes removed**: Status41
- **classes added**: Status45
- **classes removed**: Status42
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ dev = [
# Pin to exact version: codegen's variant numbering (e.g. CreateMediaBuyResponse1 vs
# CreateMediaBuyResponse) shifts between versions, producing diff churn and breaking
# generated-code imports that reference specific suffixes.
"datamodel-code-generator[http]==0.63.0",
"datamodel-code-generator[http]==0.64.0",
# Runs Starlette app lifespan under httpx.ASGITransport in tests —
# the canonical library for what httpx doesn't do natively. Used by
# tests/test_mcp_middleware_composition.py and future integration
Expand Down Expand Up @@ -340,7 +340,7 @@ skips = ["B101"] # Allow assert in code (we're not using -O optimization)

[dependency-groups]
dev = [
"datamodel-code-generator==0.63.0",
"datamodel-code-generator==0.64.0",
"pre-commit>=4.4.0",
"types-protobuf>=7.34.1.20260408",
# Pinned in the dev group so ``uv run mypy`` (used by the pre-commit
Expand Down
118 changes: 118 additions & 0 deletions scripts/post_generate_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,122 @@ def _ensure_configdict_import(content: str) -> str:
return "from pydantic import ConfigDict\n\n" + content


_TYPED_EXTRA_ASSIGNMENT = re.compile(
r"^(?P<class_name>[A-Za-z_]\w*)\.__annotations__\['__pydantic_extra__'\] = "
r"(?P<annotation>.+?)\n(?P=class_name)\.model_rebuild\(force=True\)\n?",
re.MULTILINE | re.DOTALL,
)


def _inline_typed_extra_annotations(content: str) -> tuple[str, int]:
"""Move generated typed-extra annotations into their Pydantic classes.

datamodel-code-generator 0.64 emits a post-class mutation of
``__annotations__`` followed by ``model_rebuild(force=True)`` for typed
``additionalProperties``. Pydantic does not rediscover fields added to
``__annotations__`` after class creation, so the generated model allows
arbitrary extra values instead of validating them against the schema.
Declaring ``__pydantic_extra__`` in the class body activates Pydantic's
documented typed-extra validation path.
"""
fixed = 0
while match := _TYPED_EXTRA_ASSIGNMENT.search(content):
class_name = match.group("class_name")
class_headers = list(
re.finditer(
rf"^class {re.escape(class_name)}\b[^\n]*:\n",
content[: match.start()],
re.MULTILINE,
)
)
if not class_headers:
raise ValueError(
f"Generated typed-extra assignment has no class declaration: {class_name}"
)

annotation_lines = match.group("annotation").splitlines()
declaration = f" __pydantic_extra__: {annotation_lines[0]}\n"
declaration += "".join(f" {line}\n" for line in annotation_lines[1:])

insertion_offset = class_headers[-1].end()
content = content[: match.start()] + content[match.end() :]
content = content[:insertion_offset] + declaration + content[insertion_offset:]
fixed += 1

return content, fixed


def fix_typed_additional_properties() -> None:
"""Make schema-valued ``additionalProperties`` validate at runtime."""
fixed = 0
modified_files = 0
for py_path in OUTPUT_DIR.rglob("*.py"):
content = py_path.read_text()
updated, file_fixed = _inline_typed_extra_annotations(content)
if not file_fixed:
continue
py_path.write_text(updated)
fixed += file_fixed
modified_files += 1

print(
f" Inlined {fixed} typed additionalProperties annotation(s) "
f"across {modified_files} file(s)"
)


def _remove_unused_pydantic_field_import(source: str) -> tuple[str, bool]:
"""Remove a generated ``Field`` import when the module never references it."""
tree = ast.parse(source)
if any(
isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id == "Field"
for node in ast.walk(tree)
):
return source, False

lines = source.splitlines(keepends=True)
changed = False
for node in reversed(list(ast.walk(tree))):
if not isinstance(node, ast.ImportFrom) or node.module != "pydantic":
continue
if not any(alias.name == "Field" and alias.asname is None for alias in node.names):
continue

remaining = [
alias for alias in node.names if alias.name != "Field" or alias.asname is not None
]
start = node.lineno - 1
end = node.end_lineno or node.lineno
if remaining:
names = ", ".join(
alias.name if alias.asname is None else f"{alias.name} as {alias.asname}"
for alias in remaining
)
newline = "\n" if lines[end - 1].endswith("\n") else ""
lines[start:end] = [f"from pydantic import {names}{newline}"]
else:
if start > 0 and not lines[start - 1].strip():
start -= 1
del lines[start:end]
changed = True

return "".join(lines), changed


def remove_unused_pydantic_field_imports() -> None:
"""Remove spurious ``Field`` imports emitted for generated enum modules."""
modified_files = 0
for py_path in OUTPUT_DIR.rglob("*.py"):
source = py_path.read_text()
updated, changed = _remove_unused_pydantic_field_import(source)
if not changed:
continue
py_path.write_text(updated)
modified_files += 1

print(f" Removed unused pydantic.Field imports from {modified_files} file(s)")


def _find_indented_field_block(content: str, field_name: str) -> tuple[int, int] | None:
"""Return absolute offsets for a generated four-space field block."""
cursor = 0
Expand Down Expand Up @@ -5315,6 +5431,7 @@ def main(argv: list[str] | None = None):
fix_preview_creative_request_discriminator,
add_deprecated_field_metadata,
apply_open_payload_config,
fix_typed_additional_properties,
fix_deprecated_rootmodel_fields,
fix_constr_type_annotations,
unwrap_rootmodel_unions,
Expand Down Expand Up @@ -5365,6 +5482,7 @@ def main(argv: list[str] | None = None):
fix_update_rights_legacy_response_defaults,
fix_list_creatives_format_reference_xor,
rewrite_generated_enums_to_strenum,
remove_unused_pydantic_field_imports,
strip_extra_blank_lines_at_eof,
]
for fix in fixes:
Expand Down
Loading
Loading