Module adcp.server.spec_compat

Built-in spec-compatibility hooks for pre-v3 / pre-4.4 buyers.

Wire into serve(pre_validation_hooks=spec_compat_hooks()) to accept the long tail of legacy clients without writing boilerplate in every adopter repo. Adopters who want strict-4.4-only rejection of pre-v3/pre-4.4 buyers leave this off and let the validator reject malformed requests.

The hooks in this module are intentionally pure-Python with no transport or framework dependencies so they compose cleanly with adopter-specific hooks::

from adcp.server import spec_compat_hooks, serve

# Standalone:
serve(handler, pre_validation_hooks=spec_compat_hooks())

# Combined with adopter-specific hooks:
serve(handler, pre_validation_hooks=compose_pre_validation_hooks(
    spec_compat_hooks(),
    {"create_media_buy": my_custom_hook},
))

# Selective opt-out (skip sync_creatives coercions entirely):
serve(handler, pre_validation_hooks=spec_compat_hooks(exclude={"sync_creatives"}))

# Private creative-format registry:
serve(handler, pre_validation_hooks=spec_compat_hooks(
    creative_agent_url="https://creative.example.com"
))

Global variables

var CANONICAL_CREATIVE_AGENT_URL

Canonical agent_url for the AdCP standard creative-format registry.

Injected when wrapping a bare format_id string from a pre-4.4 buyer. Override via spec_compat_hooks(creative_agent_url=...) for private creative-format registries.

Functions

def compose_pre_validation_hooks(*hook_maps: Mapping[str, PreValidationHookChain] | None) ‑> dict[str, tuple[Callable[[str, dict[str, typing.Any]], dict[str, typing.Any]], ...]]
Expand source code
def compose_pre_validation_hooks(
    *hook_maps: Mapping[str, PreValidationHookChain] | None,
) -> dict[str, tuple[PreValidationHook, ...]]:
    """Compose ordered pre-validation hook maps.

    Later maps append to earlier maps for overlapping tool names. Each
    tool's hooks run left-to-right, feeding the returned args from one hook
    into the next.
    """
    composed: dict[str, list[PreValidationHook]] = {}
    for hook_map in hook_maps:
        if hook_map is None:
            continue
        for tool_name, chain in hook_map.items():
            composed.setdefault(tool_name, []).extend(_flatten_pre_validation_hooks(chain))
    return {tool_name: tuple(hooks) for tool_name, hooks in composed.items()}

Compose ordered pre-validation hook maps.

Later maps append to earlier maps for overlapping tool names. Each tool's hooks run left-to-right, feeding the returned args from one hook into the next.

def spec_compat_hooks(*,
exclude: Collection[str] | None = None,
creative_agent_url: str = 'https://creative.adcontextprotocol.org') ‑> dict[str, collections.abc.Callable[[str, dict[str, typing.Any]], dict[str, typing.Any]] | collections.abc.Sequence[collections.abc.Callable[[str, dict[str, typing.Any]], dict[str, typing.Any]]]]
Expand source code
def spec_compat_hooks(
    *,
    exclude: Collection[str] | None = None,
    creative_agent_url: str = CANONICAL_CREATIVE_AGENT_URL,
) -> PreValidationHooks:
    """Return built-in spec-compat hooks for pre-v3 / pre-4.4 buyers.

    .. deprecated:: 5.2
        This heuristic-coercion approach is being replaced by the typed
        per-tool adapter registry under :mod:`adcp.compat.legacy`. The
        adapter path validates against the buyer's claimed
        ``adcp_version`` instead of guessing wire shapes, scales
        bounded (one module per tool) instead of growing the hook dict
        unboundedly, and matches the JS SDK's approach.

        For ``sync_creatives``, the adapter (now shipped) replaces the
        format_id wrap / asset_type inference / image-demotion logic
        that this hook used to provide. Buyers claiming
        ``adcp_major_version=2`` or ``adcp_version="2.5"`` automatically
        route through it.

        Calling this function emits a :class:`DeprecationWarning`.
        Migrate by removing the ``pre_validation_hooks=spec_compat_hooks()``
        argument from your ``serve()`` call. Removal target: 6.0.
    """
    import warnings as _warnings

    _warnings.warn(
        "spec_compat_hooks() is deprecated and will be removed in 6.0. "
        "Buyers claiming adcp_version='2.5' or adcp_major_version=2 now "
        "route through adcp.compat.legacy adapters automatically — drop "
        "the pre_validation_hooks=spec_compat_hooks() argument from "
        "serve(). See the adcp.compat.legacy docstring for the migration "
        "path.",
        DeprecationWarning,
        stacklevel=2,
    )
    return _spec_compat_hooks_impl(exclude=exclude, creative_agent_url=creative_agent_url)

Return built-in spec-compat hooks for pre-v3 / pre-4.4 buyers.

Deprecated since version: 5.2

This heuristic-coercion approach is being replaced by the typed per-tool adapter registry under :mod:adcp.compat.legacy. The adapter path validates against the buyer's claimed adcp_version instead of guessing wire shapes, scales bounded (one module per tool) instead of growing the hook dict unboundedly, and matches the JS SDK's approach.

For sync_creatives, the adapter (now shipped) replaces the format_id wrap / asset_type inference / image-demotion logic that this hook used to provide. Buyers claiming adcp_major_version=2 or adcp_version="2.5" automatically route through it.

Calling this function emits a :class:DeprecationWarning. Migrate by removing the pre_validation_hooks=spec_compat_hooks() argument from your serve() call. Removal target: 6.0.

Classes

class PreValidationHookError (*, index: int, hook_name: str, message: str)
Expand source code
class PreValidationHookError(Exception):
    """A single hook in an ordered pre-validation chain failed.

    Carries the zero-based ``index`` of the failing hook within its chain and
    the resolved ``hook_name`` so the dispatcher can surface both in the
    ``INVALID_REQUEST`` message — naming the exact callable instead of a
    generic "pre_validation_hook raised ...".
    """

    def __init__(self, *, index: int, hook_name: str, message: str) -> None:
        self.index = index
        self.hook_name = hook_name
        super().__init__(f"pre_validation_hook[{index}] {hook_name} {message}")

A single hook in an ordered pre-validation chain failed.

Carries the zero-based index of the failing hook within its chain and the resolved hook_name so the dispatcher can surface both in the INVALID_REQUEST message — naming the exact callable instead of a generic "pre_validation_hook raised …".

Ancestors

  • builtins.Exception
  • builtins.BaseException