diff --git a/SCHEMA_DELTAS.md b/SCHEMA_DELTAS.md index d3ef8d1bf..0f0db1c28 100644 --- a/SCHEMA_DELTAS.md +++ b/SCHEMA_DELTAS.md @@ -1,6 +1,3 @@ # Generated-types delta -## Field changes - -- `extensions/extension_meta.py` - - `AdcpExtensionFileSchema`: `-field_id` +_No field-shape changes detected._ diff --git a/examples/sales_proposal_mode_seller/src/platform.py b/examples/sales_proposal_mode_seller/src/platform.py index c673c3f8b..61d95b01c 100644 --- a/examples/sales_proposal_mode_seller/src/platform.py +++ b/examples/sales_proposal_mode_seller/src/platform.py @@ -155,7 +155,7 @@ def create_media_buy( "media_buy_id": media_buy_id, "buyer_ref": getattr(req, "buyer_ref", None), "status": "active", - "confirmed_at": datetime.now(timezone.utc).isoformat(), + "confirmed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "proposal_id": str(proposal_id) if proposal_id else None, "packages": [ { diff --git a/examples/sales_proposal_mode_seller/src/proposal_manager.py b/examples/sales_proposal_mode_seller/src/proposal_manager.py index 190db2a9d..6e60f2240 100644 --- a/examples/sales_proposal_mode_seller/src/proposal_manager.py +++ b/examples/sales_proposal_mode_seller/src/proposal_manager.py @@ -269,7 +269,7 @@ async def finalize_proposal( if pid in firm_cpm: entry["firm_cpm"] = firm_cpm[pid] expires_at = datetime.now(timezone.utc) + timedelta(hours=24) - committed_payload["expires_at"] = expires_at.isoformat() + committed_payload["expires_at"] = expires_at.isoformat().replace("+00:00", "Z") return FinalizeProposalSuccess( proposal=committed_payload, expires_at=expires_at, diff --git a/src/adcp/server/helpers.py b/src/adcp/server/helpers.py index 206a23102..648c339dc 100644 --- a/src/adcp/server/helpers.py +++ b/src/adcp/server/helpers.py @@ -402,7 +402,7 @@ def cancel_media_buy_response( "media_buy_id": media_buy_id, "status": "canceled", "canceled_by": canceled_by, - "canceled_at": canceled_at or datetime.now(timezone.utc).isoformat(), + "canceled_at": canceled_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "valid_actions": [], "sandbox": sandbox, } diff --git a/src/adcp/server/proposal.py b/src/adcp/server/proposal.py index 40cb7beaa..3ccda4ed5 100644 --- a/src/adcp/server/proposal.py +++ b/src/adcp/server/proposal.py @@ -301,7 +301,7 @@ def build(self) -> dict[str, Any]: if self._brief_alignment: proposal["brief_alignment"] = self._brief_alignment if self._expires_at: - proposal["expires_at"] = self._expires_at.isoformat() + proposal["expires_at"] = self._expires_at.isoformat().replace("+00:00", "Z") if self._budget_guidance: proposal["total_budget_guidance"] = self._budget_guidance if self._ext: diff --git a/src/adcp/server/responses.py b/src/adcp/server/responses.py index aa00ab7fd..bb2976d2e 100644 --- a/src/adcp/server/responses.py +++ b/src/adcp/server/responses.py @@ -28,6 +28,20 @@ async def get_products(): from adcp.server.helpers import valid_actions_for_status + +def _rfc3339_now() -> str: + """Current UTC time as an RFC 3339 timestamp with ``Z`` suffix. + + Python's :meth:`datetime.isoformat` emits ``+00:00`` for UTC, but + several strict schema validators in the AdCP ecosystem — notably + the ``zod.string().datetime()`` check that the AdCP storyboard + runner uses — reject the offset form by default. Normalizing to + the Zulu form (``...Z``) keeps response timestamps acceptable to + every common validator without losing precision. + """ + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + _logger = logging.getLogger("adcp.server") @@ -312,7 +326,7 @@ def media_buy_response( "media_buy_id": media_buy_id, "packages": _serialize(packages), "revision": revision if revision is not None else 1, - "confirmed_at": confirmed_at or datetime.now(timezone.utc).isoformat(), + "confirmed_at": confirmed_at or _rfc3339_now(), "sandbox": sandbox, } if buyer_ref is not None: @@ -407,7 +421,7 @@ def delivery_response( currency: ISO 4217 currency code. sandbox: Whether this is simulated data. """ - now = datetime.now(timezone.utc).isoformat() + now = _rfc3339_now() return { "reporting_period": reporting_period or {"start": now, "end": now}, "media_buy_deliveries": media_buy_deliveries, @@ -465,14 +479,14 @@ def list_creatives_response( Timestamp defaults: every Creative item in the spec requires ``created_date`` and ``updated_date`` (ISO 8601 UTC). For any dict item that omits either field, this helper fills it with the current - UTC timestamp (``datetime.now(timezone.utc).isoformat()``). Both - fields default to the same value when neither is provided, which - matches the intuitive meaning for a freshly-listed item. Explicit - caller-provided values are always preserved. Pydantic model items - are passed through ``_serialize`` unchanged — callers using typed - Creative models should set timestamps on the model. + UTC timestamp via :func:`_rfc3339_now` (Zulu suffix, RFC 3339). + Both fields default to the same value when neither is provided, + which matches the intuitive meaning for a freshly-listed item. + Explicit caller-provided values are always preserved. Pydantic + model items are passed through ``_serialize`` unchanged — callers + using typed Creative models should set timestamps on the model. """ - now = datetime.now(timezone.utc).isoformat() + now = _rfc3339_now() filled: list[Any] = [] for item in creatives: if isinstance(item, dict): diff --git a/src/adcp/types/_ergonomic.py b/src/adcp/types/_ergonomic.py index 37f22b6ff..6fbbf22f6 100644 --- a/src/adcp/types/_ergonomic.py +++ b/src/adcp/types/_ergonomic.py @@ -256,7 +256,7 @@ def _apply_coercion() -> None: PackageRequest, "creatives", Annotated[ - list[CreativeAsset] | None, + Sequence[CreativeAsset] | None, BeforeValidator(coerce_subclass_list(CreativeAsset)), ], ) @@ -281,7 +281,7 @@ def _apply_coercion() -> None: CreateMediaBuyRequest, "packages", Annotated[ - list[PackageRequest] | None, + Sequence[PackageRequest] | None, BeforeValidator(coerce_subclass_list(PackageRequest)), ], ) @@ -383,7 +383,7 @@ def _apply_coercion() -> None: ListCreativesResponse, "creatives", Annotated[ - list[Creative], + Sequence[Creative], BeforeValidator(coerce_subclass_list(Creative)), ], ) diff --git a/src/adcp/types/_generated.py b/src/adcp/types/_generated.py index 1a649a252..5adaaea9f 100644 --- a/src/adcp/types/_generated.py +++ b/src/adcp/types/_generated.py @@ -10,7 +10,7 @@ DO NOT EDIT MANUALLY. Generated from: https://github.com/adcontextprotocol/adcp/tree/main/schemas -Generation date: 2026-05-08 18:05:19 UTC +Generation date: 2026-05-19 21:40:28 UTC """ # ruff: noqa: E501, I001 diff --git a/src/adcp/types/capabilities.py b/src/adcp/types/capabilities.py index 6927ddba8..1f0c1b4b8 100644 --- a/src/adcp/types/capabilities.py +++ b/src/adcp/types/capabilities.py @@ -32,6 +32,8 @@ from __future__ import annotations +from typing import get_args as _get_args + from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( A2ui, Adcp, @@ -88,6 +90,9 @@ from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( Account as CapabilitiesAccount, ) +from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( + Adcp as _Adcp, +) # ``Capabilities`` (line 580 of the generated module) is the SI-block's # inner ``capabilities`` field type — modalities / components / commerce @@ -109,31 +114,49 @@ from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( Creative as CapabilitiesCreative, ) - -# ``Features2`` is the codegen name for the ``Signals.features`` type -# (numbered because ``Features`` already names the media_buy features -# block at line 142 of the generated module). Surface under a stable -# adopter-facing name so signals declarations read cleanly. -from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( - Features2 as SignalsFeatures, -) - -# ``Idempotency`` ships as a ``oneOf`` on the wire (``IdempotencySupported`` -# vs ``IdempotencyUnsupported``) — the codegen names them ``Idempotency`` -# and ``Idempotency3`` (with the numbered variant covering the -# ``supported: false`` arm). Surface the union halves under stable -# semantic names so adopters can construct either side without remembering -# which numbered variant is which. from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( Idempotency as IdempotencySupported, ) from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( - Idempotency3 as IdempotencyUnsupported, + MediaBuy as CapabilitiesMediaBuy, ) from adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response import ( - MediaBuy as CapabilitiesMediaBuy, + Signals as _Signals, ) +# ``Signals.features`` and the unsupported arm of the ``Adcp.idempotency`` +# discriminated union are inline schemas the codegen materializes under +# numbered class names (``Features`` / ``Idempotency``). Those +# numbers are not stable across regens: ``datamodel-code-generator`` +# 0.56.1 assigns them from a global counter whose traversal order shifts +# with both upstream schema layout AND filesystem-iteration order +# (APFS-on-macOS vs ext4-on-Linux), so the same pinned generator produces +# ``Features1`` in one environment and ``Features2`` in another. Reach +# the classes via their parents' field annotation — both ``Signals`` and +# ``Adcp`` are stable wire-spec class names, and the field annotations +# carry the union arm types directly. +_signals_features_arms = [ + arm for arm in _get_args(_Signals.model_fields["features"].annotation) if arm is not type(None) +] +if len(_signals_features_arms) != 1: + raise RuntimeError( + "capabilities: Signals.features annotation lost its concrete type " + f"(got {_signals_features_arms!r})" + ) +SignalsFeatures: type = _signals_features_arms[0] + +_idempotency_arms = [ + arm + for arm in _get_args(_Adcp.model_fields["idempotency"].annotation) + if arm is not IdempotencySupported and arm is not type(None) +] +if len(_idempotency_arms) != 1: + raise RuntimeError( + "capabilities: expected exactly one non-supported Idempotency arm, " + f"got {_idempotency_arms!r}" + ) +IdempotencyUnsupported: type = _idempotency_arms[0] + __all__ = [ "A2ui", "Adcp", diff --git a/src/adcp/types/generated_poc/enums/daast_tracking_event.py b/src/adcp/types/generated_poc/enums/daast_tracking_event.py index 293bc3f73..60c67ea25 100644 --- a/src/adcp/types/generated_poc/enums/daast_tracking_event.py +++ b/src/adcp/types/generated_poc/enums/daast_tracking_event.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: enums/daast_tracking_event.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations diff --git a/src/adcp/types/generated_poc/enums/daast_version.py b/src/adcp/types/generated_poc/enums/daast_version.py index 36b24df93..cf2ad375b 100644 --- a/src/adcp/types/generated_poc/enums/daast_version.py +++ b/src/adcp/types/generated_poc/enums/daast_version.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: enums/daast_version.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations diff --git a/src/adcp/types/generated_poc/enums/forecastable_metric.py b/src/adcp/types/generated_poc/enums/forecastable_metric.py index bf70f8a50..41a8bec22 100644 --- a/src/adcp/types/generated_poc/enums/forecastable_metric.py +++ b/src/adcp/types/generated_poc/enums/forecastable_metric.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: enums/forecastable_metric.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py index c2ffd0652..64fb536d7 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py @@ -1,13 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buy_delivery_response.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations from collections.abc import Sequence from enum import Enum from typing import Annotated, Any -from collections.abc import Sequence from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buys_response.py b/src/adcp/types/generated_poc/media_buy/get_media_buys_response.py index cd6f720d6..e7803fbfc 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buys_response.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buys_response.py @@ -1,13 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buys_response.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations from collections.abc import Sequence from enum import Enum from typing import Annotated -from collections.abc import Sequence from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py index eed31c44e..ee714a354 100644 --- a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py @@ -1,12 +1,11 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_response.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-05-19T21:40:22+00:00 from __future__ import annotations from collections.abc import Sequence from typing import Annotated -from collections.abc import Sequence from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field