Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b746a04
feat(types)!: emit scalar schema roots as plain scalar subclasses
KonstantinMirin Oct 2, 2026
ad22318
chore(types): regenerate models for the scalar root rewrite
KonstantinMirin Oct 2, 2026
7332a63
fix(types): stop the SDK reading .root on its own scalar schemas
KonstantinMirin Oct 2, 2026
2facf32
test(types): cover the scalar root contract
KonstantinMirin Oct 2, 2026
8847ae9
feat(types)!: emit a composing schema root as what it composes
KonstantinMirin Oct 3, 2026
3ef739e
chore(types): regenerate models for the composing-root rewrite
KonstantinMirin Oct 3, 2026
67ea1fe
fix: stop the SDK reading .root on its own composing roots
KonstantinMirin Oct 3, 2026
5c643a9
test(types): cover the composing-root contract
KonstantinMirin Oct 3, 2026
844c81f
chore(types): rebuild SCHEMA_DELTAS.md from the base-to-HEAD comparison
KonstantinMirin Oct 3, 2026
d7b140a
Merge remote-tracking branch 'upstream/main' into fix/scalar-str-codegen
KonstantinMirin Oct 3, 2026
2d0fe47
Merge upstream/main (v8.0.0-20) into fix/rootmodel-objects-discrimina…
KonstantinMirin Oct 3, 2026
4ecdf56
Merge remote-tracking branch 'upstream/main' into fix/scalar-str-codegen
KonstantinMirin Oct 3, 2026
5b1c6f2
Merge upstream/main (63bfadb4e) into fix/rootmodel-objects-discrimina…
KonstantinMirin Oct 3, 2026
5a5efc8
Merge upstream/main (63bfadb4e) into fix/scalar-str-codegen
KonstantinMirin Oct 3, 2026
4ec78ed
Merge remote-tracking branch 'origin/main' into land/1286
bokelley Oct 4, 2026
db0ad23
Merge branch 'land/1286' into land/1353
bokelley Oct 4, 2026
15fb8c0
chore(types): regenerate on top of the scalar-root rewrite (#1286)
bokelley Oct 4, 2026
22007d1
docs(types): rebuild SCHEMA_DELTAS.md as the main-to-head comparison
bokelley Oct 4, 2026
d84c783
fix(types): build the union-root TypeAdapters on first use, not at im…
bokelley Oct 4, 2026
d6a17f9
Merge remote-tracking branch 'origin/main' into land/1353
bokelley Oct 4, 2026
b227fc7
Merge remote-tracking branch 'origin/main' into land/1286
bokelley Oct 4, 2026
5c9c1ee
Merge branch 'land/1286' into land/1353
bokelley Oct 4, 2026
56e68ce
test(types): unwrap Annotated union roots in the hierarchy ratchet
bokelley Oct 4, 2026
9a1a8fb
Merge remote-tracking branch 'origin/main' into land/1353
bokelley Oct 4, 2026
e0c1b08
Merge remote-tracking branch 'origin/main' into land/1353
bokelley Oct 4, 2026
2d25652
docs(types): rebuild SCHEMA_DELTAS.md as the main-to-head comparison
bokelley Oct 4, 2026
ca8c1af
fix(codegen): keep a composing root wrapped when another module's fie…
bokelley Oct 4, 2026
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
495 changes: 167 additions & 328 deletions SCHEMA_DELTAS.md

Large diffs are not rendered by default.

730 changes: 482 additions & 248 deletions scripts/post_generate_fixes.py

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions src/adcp/decisioning/refine.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def build_refinement_applied(

:param refines: ``request.refine`` (length N).
:param outcomes: Adopter's per-entry outcomes (must also be length N).
:returns: Wire-shape ``RefinementApplied`` (RootModel) instances (one per entry).
:returns: Wire-shape ``RefinementApplied`` instances (one per entry).
:raises ValueError: ``len(outcomes) != len(refines)``. Developer-facing,
not buyer-facing — adopter-side bug.
"""
Expand All @@ -170,7 +170,6 @@ def build_refinement_applied(
)

from adcp.types import (
RefinementApplied,
RefinementApplied1,
RefinementApplied2,
RefinementApplied3,
Expand Down Expand Up @@ -215,7 +214,7 @@ def build_refinement_applied(
raise ValueError(
f"Unknown refine scope {scope!r}; expected " "'request' | 'product' | 'proposal'."
)
out.append(RefinementApplied(root=applied))
out.append(applied)
return out


Expand Down
4 changes: 3 additions & 1 deletion src/adcp/feed_mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,9 @@ def _event_entity(event: WholesaleFeedEvent) -> FeedEntity:

def _apply_event(self, event: WholesaleFeedEvent) -> None:
event_type = str(event.event_type)
payload = event.payload
# Each branch below knows its arm from event_type, which is a string
# rather than the union's discriminator, so the payload stays untyped.
payload: Any = event.payload
if event_type in ("product.created", "product.updated"):
product = getattr(payload, "product", None)
if product is not None:
Expand Down
12 changes: 10 additions & 2 deletions src/adcp/reporting_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
import idna
import jsonschema
import rfc8785
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel, TypeAdapter, ValidationError

from adcp._deferred_adapters import deferred_adapter
from adcp.reporting import ReportingInspectionContext, ReportingObservation
from adcp.signing._bounded_http import ResponseTooLargeError, async_read_limited_bytes
from adcp.signing._idna_canonicalize import canonicalize_host
Expand All @@ -36,6 +37,13 @@
ReportingReportDefinition,
)

# ReportingControlTotal is a union, so validation goes through an adapter.


@deferred_adapter
def _control_total_adapter() -> TypeAdapter[ReportingControlTotal]:
return TypeAdapter(ReportingControlTotal)


class ReportingInspectionCode(str, Enum):
RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE"
Expand Down Expand Up @@ -542,7 +550,7 @@ def _default_control_totals(
try:
payload = target.model_dump(mode="json", exclude_none=True)
payload["value"] = _decimal_string(value, str(target.value_type))
totals.append(ReportingControlTotal.model_validate(payload))
totals.append(_control_total_adapter().validate_python(payload))
except (ValueError, ValidationError) as error:
raise ReportingInspectionError(
ReportingInspectionCode.CONTROL_TOTAL_MISMATCH,
Expand Down
12 changes: 10 additions & 2 deletions src/adcp/server/principal.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from urllib.parse import urlsplit, urlunsplit
from uuid import uuid4

from pydantic import BaseModel
from pydantic import BaseModel, TypeAdapter

from adcp._deferred_adapters import deferred_adapter
from adcp.server.idempotency import canonical_json_sha256
from adcp.types import (
AgentDeclarations,
Expand All @@ -37,6 +38,13 @@
SyncPrincipalResponse,
)

# AgentReportingDestination is a union, so validation goes through an adapter.


@deferred_adapter
def _reporting_destination_adapter() -> TypeAdapter[AgentReportingDestination]:
return TypeAdapter(AgentReportingDestination)


@dataclass(frozen=True)
class PrincipalIdentity:
Expand Down Expand Up @@ -299,7 +307,7 @@ def _normalize_destination(destination: AgentReportingDestination) -> AgentRepor
recipient["identity"] = _normalize_coordinate(
identity, field="reporting_destinations.recipient.identity"
)
return AgentReportingDestination.model_validate(payload)
return _reporting_destination_adapter().validate_python(payload)


class PrincipalService:
Expand Down
7 changes: 3 additions & 4 deletions src/adcp/types/_forward_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import (
PublisherDomain as BundledPublisherDomain,
)
from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData
from adcp.types.generated_poc.core.canonical_format_kind import CanonicalFormatKind
from adcp.types.generated_poc.core.canonical_product import PublisherDomain
from adcp.types.generated_poc.core.creative_manifest import CreativeManifest
Expand Down Expand Up @@ -498,9 +497,9 @@ def _apply_forward_compat() -> None:
_patch_model_field(response, "offers", GenericAlias(list, _ReadbackOffer))
response.model_rebuild(force=True)

# These eager wrappers captured build/preview validators before the patches.
# Refresh both levels so completed task callbacks retain typed manifests.
AdcpAsyncResponseData.model_rebuild(force=True)
# This eager wrapper captured build/preview validators before the patches.
# Rebuilding it re-resolves AdcpAsyncResponseData, a union type alias, so
# completed task callbacks retain typed manifests.
McpWebhookPayload.model_rebuild(force=True)

_patch_model_field(Format, "assets", list[FormatAssetUnion] | None)
Expand Down
28 changes: 25 additions & 3 deletions src/adcp/types/error_narrowing.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ def _split_at_variant(
_MAX_NARROW_INPUT_SIZE = 500


def _asset_variant_index(loc: tuple[Any, ...]) -> int | None:
"""Index of the asset-variant arm in ``loc``, or ``None``.

Pydantic spells that arm by the wrapper class name when the union is
wrapped (``"AssetVariant"``) and by its members when the union is inline
(``"tagged-union[ImageAsset,VideoAsset,...]"``). Both name the same arm,
and an error under it beats the sibling open-asset arm.
"""
for index, segment in enumerate(loc):
if segment == "AssetVariant" or (
isinstance(segment, str) and segment.startswith("tagged-union[")
):
return index
return None


def narrow_union_errors(
errors: Any,
) -> list[Any]:
Expand Down Expand Up @@ -154,8 +170,9 @@ def narrow_union_errors(

for err in errors_list:
loc = tuple(err.get("loc", ()))
if "AssetVariant" in loc:
asset_variant_prefixes.add(loc[: loc.index("AssetVariant")])
index = _asset_variant_index(loc)
if index is not None:
asset_variant_prefixes.add(loc[:index])

for err in errors_list:
loc = tuple(err.get("loc", ()))
Expand All @@ -169,7 +186,12 @@ def narrow_union_errors(
buckets.setdefault(prefix, []).append((variant, err))

if not buckets:
return errors_list
# Nothing to narrow by variant name. Return what survived the
# asset-variant drop above, which is already the focused set; an
# inline tagged union spells its arm in a form no variant-name
# heuristic recognizes, and falling back to the input here would put
# the dropped sibling arm back.
return [dict(err) for err in passthrough] if passthrough else errors_list

# Defensive copy of the dicts we're about to surface — the caller
# might mutate the returned list and we don't want that to leak
Expand Down
30 changes: 10 additions & 20 deletions src/adcp/types/generated_poc/a2ui/bound_value.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# generated by datamodel-codegen:
# filename: a2ui/bound_value.json
# timestamp: 2026-06-01T00:32:59+00:00
# timestamp: 2026-10-02T23:18:44+00:00

from __future__ import annotations

from typing import Any, Annotated
from typing import Annotated

from adcp.types.base import AdCPBaseModel
from pydantic import ConfigDict, Field, RootModel
from pydantic import ConfigDict, Field


class A2UiBoundValue1(AdCPBaseModel):
Expand Down Expand Up @@ -49,20 +49,10 @@ class A2UiBoundValue5(AdCPBaseModel):
path: str


class A2UiBoundValue(
RootModel[
A2UiBoundValue1 | A2UiBoundValue2 | A2UiBoundValue3 | A2UiBoundValue4 | A2UiBoundValue5
]
):
root: Annotated[
A2UiBoundValue1 | A2UiBoundValue2 | A2UiBoundValue3 | A2UiBoundValue4 | A2UiBoundValue5,
Field(
description='A value that can be a literal or bound to a path in the data model',
title='A2UI Bound Value',
),
]
def __getattr__(self, name: str) -> Any:
"""Proxy attribute access to the wrapped type."""
if name.startswith('_'):
raise AttributeError(name)
return getattr(self.root, name)
A2UiBoundValue = Annotated[
A2UiBoundValue1 | A2UiBoundValue2 | A2UiBoundValue3 | A2UiBoundValue4 | A2UiBoundValue5,
Field(
description='A value that can be a literal or bound to a path in the data model',
title='A2UI Bound Value',
),
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/get_account_financials_request.json
# timestamp: 2026-06-05T16:54:33+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand All @@ -20,7 +20,7 @@ class GetAccountFinancialsRequest(AdcpVersionEnvelope):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2,
account_ref.AccountReference,
Field(description='Account to query financials for. Must be an operator-billed account.'),
]
period: Annotated[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/list_account_changes_request.json
# timestamp: 2026-10-02T13:37:43+00:00
# timestamp: 2026-10-04T00:53:27+00:00

from __future__ import annotations

Expand Down Expand Up @@ -47,7 +47,7 @@ class ListAccountChangesRequest(AdCPBaseModel):
),
] = None
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2,
account_ref.AccountReference,
Field(
description='Account whose change feed to read. The resolved account and returned cursor are bound to the authenticated principal.'
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/list_accounts_request.json
# timestamp: 2026-08-17T23:02:13+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand Down Expand Up @@ -30,7 +30,7 @@ class ListAccountsRequest(AdcpVersionEnvelope):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2 | None,
account_ref.AccountReference | None,
Field(
description='Optional exact account filter. Use `account_id` to retrieve one known seller/storefront account, or the complete natural key (`brand` + `operator` + optional `operator_unit`, fixed `currency`, buyer-selected account `timezone`, and `sandbox`) for buyer-declared accounts. When present, the seller returns only matching accounts visible to the authenticated caller.'
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/report_usage_request.json
# timestamp: 2026-08-17T23:02:13+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand All @@ -21,7 +21,7 @@ class UsageItem(AdCPBaseModel):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account for this usage record.')
account_ref.AccountReference, Field(description='Account for this usage record.')
]
media_buy_id: Annotated[
str | None,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/sync_accounts_request.json
# timestamp: 2026-09-01T19:37:00+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand All @@ -27,7 +27,7 @@ class Accounts(AdCPBaseModel):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2 | None,
account_ref.AccountReference | None,
Field(
description='Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`.'
),
Expand Down Expand Up @@ -135,7 +135,7 @@ class Accounts1(AdCPBaseModel):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2,
account_ref.AccountReference,
Field(
description='Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`.'
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/sync_governance_request.json
# timestamp: 2026-08-28T20:03:29+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand Down Expand Up @@ -48,7 +48,7 @@ class Account(AdCPBaseModel):
extra='forbid',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2,
account_ref.AccountReference,
Field(
description='Account to sync governance agents for. Use account_id for account-id namespaces or brand + operator for buyer-declared accounts.'
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: account/sync_governance_response.json
# timestamp: 2026-09-05T19:08:34+00:00
# timestamp: 2026-10-02T23:10:32+00:00

from __future__ import annotations

Expand Down Expand Up @@ -63,7 +63,7 @@ class Account(AdCPBaseModel):
extra='allow',
)
account: Annotated[
account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account reference, echoed from request')
account_ref.AccountReference, Field(description='Account reference, echoed from request')
]
status: Annotated[
Status,
Expand Down
Loading
Loading