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
1 change: 1 addition & 0 deletions MIGRATION_v9_to_v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ the SDK-relevant inventory is [docs/adcp-3.3-sdk-inventory.md](docs/adcp-3.3-sdk
| `model_dump()` leaves an unset schema default off | A property the schema lists outside `required` with a `default` (or a `const`) is emitted only when it was set: by the document that was parsed, by a keyword argument, or by assignment. `sandbox: false`, `timezone: "inventory_local"`, `assignment_limit: 50` and the other schema defaults no longer appear on a dump unless you set them; the attribute still reads the default. Required discriminators (`asset_type`, `pricing_model`) are emitted either way. Pass `exclude_unset=False` for the SDK 9 output. Golden-file tests that compare whole dumps need regenerating. If you relied on a default reaching the peer, set it explicitly. `model_dump_as_received()` is deprecated: it returns the same document as `model_dump(mode="json")` and is removed in SDK 11. Generated root models (a union root such as `LocalizedCreativeAsset`) now subclass `AdCPRootModel` and dump with `exclude_none=True` like every other AdCP model. |
| Unknown keys on open objects are kept and written back | `AdCPBaseModel` defaults to `extra='allow'`: a property the pinned model does not declare (a field a newer spec added, a sibling arm's property the schema admits) survives `model_validate` → `model_dump` in `model_extra`, including a `null` value. Generated classes whose schema object is closed (`additionalProperties: false`) keep `extra='forbid'`. SDK 9 dropped unknown keys (`extra='ignore'`). If your code passed stray keyword arguments and relied on them being dropped, remove them; `ADCP_STRICT_VALIDATION=1` turns the default into `forbid` so tests find them. |
| Permissive adagents.json resolution requires the fetched domain | `resolve_properties_for_agent(doc, agent_url, mode="permissive")` raises `ValueError` without `publisher_domain=`. Pass the domain the document was fetched for: a bare entry's top-level properties are then kept only when every identifier is a `type: "domain"` under that domain, so a file on another host can no longer claim a publisher's site, app bundle or DOOH venue through a bare entry. Properties identified by bundle, store or venue IDs, and properties whose `publisher_domain` names another domain, are dropped from the bare fallback. `allow_any_domain=True` restores the SDK 9 result. Typed selectors and strict mode are unchanged. See [docs/adagents-discovery.md](docs/adagents-discovery.md) for `properties_for_publisher_domain` and `verify_cross_domain_properties`. |
| Generated enums name each value once | The `_1` alias members codegen emitted when an `anyOf` arm repeated an enum value (`System.zip_1`, `Field17.ad_request_url_1`, 19 enums on 10.0.0b4) are gone; each value has one member, the first-declared one. Code that referenced an alias name (`System.zip_1`) must use the real member (`System.zip`); `System("zip")` and `member.value` are unchanged. |
52 changes: 52 additions & 0 deletions scripts/post_generate_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7631,6 +7631,57 @@ def point_rootmodels_at_adcp_root() -> None:
print(f" Pointed root models at AdCPRootModel in {modified} file(s)")


def dedupe_enum_member_values() -> None:
"""Drop the ``<name>_N`` enum members that repeat a value (#1782).

When an object's ``anyOf`` arm re-declares a property with a narrower
``enum`` (``postal-country-system.json`` arm 0 narrows ``system`` to
``['zip', 'zip_plus_four']``), datamodel-code-generator merges the arm's
values into the root enum without deduplicating, so the generated
``StrEnum`` carries ``zip = 'zip'`` and ``zip_1 = 'zip'``. Python makes the
second member an alias of the first, so nothing breaks at runtime, but
``__members__``, autocomplete and ``member.name`` then offer names that are
not AdCP values. Keep the first member for each value and remove the rest;
``tests/test_generated_enums_have_no_aliases.py`` holds the tree to it.
"""
modified = 0
removed = 0
for path in sorted(OUTPUT_DIR.rglob("*.py")):
source = path.read_text()
if "Enum)" not in source:
continue
tree = ast.parse(source)
lines = source.splitlines(keepends=True)
doomed: list[tuple[int, int]] = []
for node in tree.body:
if not isinstance(node, ast.ClassDef) or not any(
isinstance(base, ast.Name) and base.id.endswith("Enum") for base in node.bases
):
continue
seen: set[object] = set()
for statement in node.body:
if (
isinstance(statement, ast.Assign)
and len(statement.targets) == 1
and isinstance(statement.targets[0], ast.Name)
and isinstance(statement.value, ast.Constant)
):
value = statement.value.value
if value in seen:
assert statement.end_lineno is not None
doomed.append((statement.lineno, statement.end_lineno))
else:
seen.add(value)
if not doomed:
continue
for start, end in sorted(doomed, reverse=True):
del lines[start - 1 : end]
removed += 1
path.write_text("".join(lines))
modified += 1
print(f" Removed {removed} duplicate-valued enum member(s) from {modified} module(s)")


def preserve_request_signing_operation_strings() -> None:
"""Keep request-signing operation lists source-compatible with beta.5.

Expand Down Expand Up @@ -8781,6 +8832,7 @@ def main(argv: list[str] | None = None) -> int:
fix_product_fields_item_reference,
fix_audience_evidence_attestation_subject,
fix_legacy_purchase_accepted_losses,
dedupe_enum_member_values,
drop_empty_model_arms_from_array_fields,
preserve_request_signing_operation_strings,
enforce_change_term_runtime_constraints,
Expand Down
1 change: 1 addition & 0 deletions scripts/post_generation_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"fix_product_fields_item_reference": true,
"fix_audience_evidence_attestation_subject": true,
"fix_legacy_purchase_accepted_losses": true,
"dedupe_enum_member_values": true,
"drop_empty_model_arms_from_array_fields": true,
"preserve_request_signing_operation_strings": true,
"enforce_change_term_runtime_constraints": true,
Expand Down
2 changes: 0 additions & 2 deletions src/adcp/types/domains/core/assets/asset_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,6 @@ class Field1(StrEnum):
content = 'content'
ad_request_url = 'ad_request_url'
clickthrough_url = 'clickthrough_url'
ad_request_url_1 = 'ad_request_url'
clickthrough_url_1 = 'clickthrough_url'


class Location5(AdCPBaseModel):
Expand Down
2 changes: 0 additions & 2 deletions src/adcp/types/domains/core/assets/display_tag_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ class Field17(StrEnum):
content = 'content'
ad_request_url = 'ad_request_url'
clickthrough_url = 'clickthrough_url'
ad_request_url_1 = 'ad_request_url'
clickthrough_url_1 = 'clickthrough_url'


class Location18(AdCPBaseModel):
Expand Down
8 changes: 0 additions & 8 deletions src/adcp/types/domains/core/postal_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ class System(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
zip_1 = 'zip'
zip_plus_four_1 = 'zip_plus_four'


class PostalArea11(AdCPBaseModel):
Expand Down Expand Up @@ -76,8 +74,6 @@ class System1(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
outward_1 = 'outward'
full_1 = 'full'


class PostalArea12(AdCPBaseModel):
Expand Down Expand Up @@ -115,8 +111,6 @@ class System2(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
fsa_1 = 'fsa'
full_1 = 'full'


class PostalArea13(AdCPBaseModel):
Expand Down Expand Up @@ -260,8 +254,6 @@ class System9(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
postal_code_1 = 'postal_code'
custom_1 = 'custom'


class PostalArea110(AdCPBaseModel):
Expand Down
8 changes: 0 additions & 8 deletions src/adcp/types/domains/core/postal_country_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ class System(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
zip_1 = 'zip'
zip_plus_four_1 = 'zip_plus_four'


class PostalCountrySystem1(AdCPBaseModel):
Expand Down Expand Up @@ -74,8 +72,6 @@ class System11(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
outward_1 = 'outward'
full_1 = 'full'


class PostalCountrySystem2(AdCPBaseModel):
Expand Down Expand Up @@ -113,8 +109,6 @@ class System12(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
fsa_1 = 'fsa'
full_1 = 'full'


class PostalCountrySystem3(AdCPBaseModel):
Expand Down Expand Up @@ -258,8 +252,6 @@ class System19(StrEnum):
au_postcode = 'au_postcode'
ch_plz = 'ch_plz'
at_plz = 'at_plz'
postal_code_1 = 'postal_code'
custom_1 = 'custom'


class PostalCountrySystem10(AdCPBaseModel):
Expand Down
2 changes: 0 additions & 2 deletions src/adcp/types/domains/creative/list_creatives_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ class IndicatorTypesEvaluatedEnum(StrEnum):
budget_constrained = 'budget_constrained'
flight_extension_opportunity = 'flight_extension_opportunity'
scale_budget_opportunity = 'scale_budget_opportunity'
creative_fatigue_1 = 'creative_fatigue'
creative_quality_opportunity_1 = 'creative_quality_opportunity'


class Indicator(AdCPBaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,6 @@ class TransitionMode(StrEnum):
instant = 'instant'
animated = 'animated'
scroll_linked = 'scroll_linked'
instant_1 = 'instant'
animated_1 = 'animated'


class ScrollReference(StrEnum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,6 @@ class TransitionMode(StrEnum):
instant = 'instant'
animated = 'animated'
scroll_linked = 'scroll_linked'
instant_1 = 'instant'
animated_1 = 'animated'


class ScrollReference(StrEnum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ class Code(StrEnum):
inventory_shortfall_forecast = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk = 'flight_change_creates_pacing_risk'
fields_ignored_due_to_precedence = 'fields_ignored_due_to_precedence'
inventory_shortfall_forecast_1 = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk_1 = 'flight_change_creates_pacing_risk'


class Status(StrEnum):
Expand Down
2 changes: 0 additions & 2 deletions src/adcp/types/domains/media_buy/buy_products_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ class Code(StrEnum):
inventory_shortfall_forecast = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk = 'flight_change_creates_pacing_risk'
fields_ignored_due_to_precedence = 'fields_ignored_due_to_precedence'
inventory_shortfall_forecast_1 = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk_1 = 'flight_change_creates_pacing_risk'


class Status(StrEnum):
Expand Down
12 changes: 0 additions & 12 deletions src/adcp/types/domains/media_buy/get_media_buys_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ class IndicatorTypesEvaluatedEnum(StrEnum):
budget_constrained = 'budget_constrained'
flight_extension_opportunity = 'flight_extension_opportunity'
scale_budget_opportunity = 'scale_budget_opportunity'
budget_constrained_1 = 'budget_constrained'
flight_extension_opportunity_1 = 'flight_extension_opportunity'
scale_budget_opportunity_1 = 'scale_budget_opportunity'


class ProposalStatus(StrEnum):
Expand Down Expand Up @@ -114,13 +111,6 @@ class IndicatorTypesEvaluatedEnum1(StrEnum):
budget_constrained = 'budget_constrained'
flight_extension_opportunity = 'flight_extension_opportunity'
scale_budget_opportunity = 'scale_budget_opportunity'
creative_diversity_low_1 = 'creative_diversity_low'
audience_saturation_1 = 'audience_saturation'
inventory_shortfall_forecast_1 = 'inventory_shortfall_forecast'
pacing_risk_1 = 'pacing_risk'
budget_constrained_1 = 'budget_constrained'
flight_extension_opportunity_1 = 'flight_extension_opportunity'
scale_budget_opportunity_1 = 'scale_budget_opportunity'


class Cancellation1(AdCPBaseModel):
Expand Down Expand Up @@ -151,8 +141,6 @@ class IndicatorTypesEvaluatedEnum2(StrEnum):
budget_constrained = 'budget_constrained'
flight_extension_opportunity = 'flight_extension_opportunity'
scale_budget_opportunity = 'scale_budget_opportunity'
creative_fatigue_1 = 'creative_fatigue'
creative_quality_opportunity_1 = 'creative_quality_opportunity'


class Snapshot(AdCPBaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ class Code(StrEnum):
inventory_shortfall_forecast = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk = 'flight_change_creates_pacing_risk'
fields_ignored_due_to_precedence = 'fields_ignored_due_to_precedence'
inventory_shortfall_forecast_1 = 'inventory_shortfall_forecast'
flight_change_creates_pacing_risk_1 = 'flight_change_creates_pacing_risk'


class Status(StrEnum):
Expand Down
85 changes: 85 additions & 0 deletions tests/test_generated_enums_have_no_aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""A generated enum names each value once (#1782).

datamodel-code-generator merges an ``anyOf`` arm's narrower ``enum`` into the
root enum without deduplicating, so ``postal_country_system.System`` carried
``zip = 'zip'`` and ``zip_1 = 'zip'`` (19 enums on 10.0.0b4). Python makes the
second member an alias of the first, so nothing breaks at runtime, but
``__members__``, autocomplete and ``member.name`` then offer names that are not
AdCP values. ``post_generate_fixes.dedupe_enum_member_values`` removes the
repeats; these tests hold the whole generated tree to it.
"""

from __future__ import annotations

import importlib
import pkgutil
from enum import Enum
from pathlib import Path

import adcp.types.domains as domains
from scripts import post_generate_fixes as fixes


def _generated_enums() -> list[type[Enum]]:
found: list[type[Enum]] = []
for info in pkgutil.walk_packages(domains.__path__, domains.__name__ + "."):
try:
module = importlib.import_module(info.name)
except ImportError: # a removed-schema shim that raises on import
continue
for obj in vars(module).values():
if (
isinstance(obj, type)
and issubclass(obj, Enum)
and obj.__module__ == module.__name__
):
found.append(obj)
return found


def test_no_generated_enum_has_an_alias_member() -> None:
offenders = {
f"{enum.__module__}.{enum.__name__}": [
alias for alias, member in enum.__members__.items() if alias != member.name
]
for enum in _generated_enums()
}
offenders = {name: aliases for name, aliases in offenders.items() if aliases}
assert offenders == {}, offenders


def test_the_postal_system_enum_names_zip_once() -> None:
from adcp.types.domains.core.postal_country_system import System

assert [m.value for m in System].count("zip") == 1
assert "zip_1" not in System.__members__
assert System("zip") is System.zip


def test_the_pass_keeps_the_first_member_for_each_value(tmp_path: Path, monkeypatch) -> None:
generated = tmp_path / "domains"
(generated / "core").mkdir(parents=True)
target = generated / "core" / "thing.py"
target.write_text(
"from adcp.types._str_enum import StrEnum\n\n\n"
"class System(StrEnum):\n"
" zip = 'zip'\n"
" zip_plus_four = 'zip_plus_four'\n"
" zip_1 = 'zip'\n"
" zip_plus_four_1 = 'zip_plus_four'\n\n\n"
"class Untouched(StrEnum):\n"
" a = 'a'\n"
" b = 'b'\n\n\n"
"class NotAnEnum:\n"
" x = 'a'\n"
" y = 'a'\n"
)
monkeypatch.setattr(fixes, "OUTPUT_DIR", generated)
fixes.dedupe_enum_member_values()
once = target.read_text()
fixes.dedupe_enum_member_values()
assert target.read_text() == once, "the pass must be idempotent"
assert "zip_1" not in once and "zip_plus_four_1" not in once
assert " zip = 'zip'\n zip_plus_four = 'zip_plus_four'\n" in once
assert " a = 'a'\n b = 'b'\n" in once
assert " x = 'a'\n y = 'a'\n" in once
Loading