Module adcp.decisioning.platform
DecisioningPlatform base class + capabilities declaration.
:class:DecisioningPlatform is the adopter-facing base. Adopters subclass
it, attach an :class:AccountStore, declare :class:DecisioningCapabilities,
and implement specialism methods (get_products, create_media_buy,
sync_audiences, etc.) directly on the class. The dispatch adapter
discovers methods via hasattr at server boot, validates against the
declared capabilities, and routes requests through the framework's
existing transport machinery.
Global variables
var GOVERNANCE_SPECIALISMS : frozenset[str]-
Mirrors every
governance-*slug inschemas/cache/enums/specialism.json— includinggovernance-aware-seller. A seller agent that composes with a buyer's governance agent reads governance context per-request; the gate must catch it claiming the specialism without wiring the StateReader, just like the spend-authority and delivery-monitor governance agents themselves.
Classes
class DecisioningCapabilities (specialisms: list[Specialism | str] = <factory>,
creative_agents: list[Any] = <factory>,
config: dict[str, Any] = <factory>,
governance_aware: bool = False,
webhook_signing_managed_externally: bool = False,
auto_paginate: bool = False,
adcp: Adcp | None = None,
account: CapabilitiesAccount | None = None,
media_buy: CapabilitiesMediaBuy | None = None,
signals: Signals | None = None,
governance: Governance | None = None,
sponsored_intelligence: SponsoredIntelligence | None = None,
brand: Brand | None = None,
creative: CapabilitiesCreative | None = None,
measurement: Measurement | None = None,
request_signing: RequestSigning | None = None,
webhook_signing: WebhookSigning | None = None,
identity: Identity | None = None,
compliance_testing: ComplianceTesting | None = None,
experimental_features: list[ExperimentalFeature | str] | None = None,
supported_protocols: list[SupportedProtocol] | None = None,
channels: list[str] = <factory>,
pricing_models: list[str] = <factory>,
supported_billing: list[str] = <factory>)-
Expand source code
@dataclass class DecisioningCapabilities: """What a platform claims to support. Read by ``validate_platform`` at server boot to confirm each declared specialism has the methods it requires, and surfaced via the framework's auto-generated ``get_adcp_capabilities`` response so buyers can pre-flight without trial-and-error tool calls. Capability declaration shape mirrors the AdCP wire spec (``protocol/get-adcp-capabilities-response.json``). Adopters import the typed sub-models from :mod:`adcp.decisioning.capabilities` — that submodule re-exports under wire-spec names, so declarations read 1:1 against the spec:: from adcp.decisioning import DecisioningCapabilities from adcp.decisioning.capabilities import ( Account, MediaBuy, Targeting, GeoMetros, IdempotencySupported, Specialism, ) capabilities = DecisioningCapabilities( specialisms=[Specialism.sales_non_guaranteed.value], adcp=Adcp( major_versions=[3], idempotency=IdempotencySupported( supported=True, replay_ttl_seconds=86400, ), ), account=Account(supported_billing=["operator"]), media_buy=MediaBuy( supported_pricing_models=["cpm"], execution=Execution( targeting=Targeting(geo_countries=True), ), ), ) Wire capability blocks (one field per top-level wire field): :param adcp: Core protocol info — ``major_versions`` and ``idempotency``. Required on the wire; defaults to ``None`` means the framework will project a non-conformant response (the boot-time validator catches this). :param account: Account-management capabilities (billing, OAuth, sandbox). :param media_buy: Media-buy protocol capabilities — pricing models, reporting delivery methods, execution targeting, etc. Expected when ``media_buy`` is in ``supported_protocols``. ``execution.targeting.geo_postal_areas`` may be declared once using the native AdCP 3.1 country-keyed model; the framework auto-projects it to the deprecated fused boolean model for pre-3.1 capability callers. :param signals: Signals protocol capabilities. Only emit when ``signals`` is in ``supported_protocols``. :param governance: Governance protocol capabilities. :param sponsored_intelligence: SI protocol capabilities. :param brand: Brand protocol capabilities. :param creative: Creative protocol capabilities. :param measurement: Experimental measurement protocol capabilities. :param request_signing: RFC 9421 inbound request signing posture. :param webhook_signing: Outbound webhook-signing posture. :param identity: Operator key-scoping / compromise-response identity posture (advisory in 3.x). :param compliance_testing: Deterministic-testing capability via ``comply_test_controller``. Omit entirely if unsupported. :param experimental_features: Experimental surfaces implemented by the platform. Required for experimental capability blocks such as ``measurement``. :param supported_protocols: Override for the ``supported_protocols`` wire field. Default ``None`` = derive from :attr:`specialisms` via ``SPECIALISM_TO_PROTOCOLS``. Set explicitly when claiming a protocol whose specialisms aren't all listed (e.g. transitional state, generic seller passing the baseline storyboard without claiming a specific specialism). SDK-internal dispatch (not wire fields): :param specialisms: AdCP specialism slugs the platform claims — e.g. ``['sales-non-guaranteed', 'sales-broadcast-tv']``, ``['audience-sync']``, ``['signal-marketplace']``, or ``['signal-owned']``. Each maps to a ``Protocol`` class under :mod:`adcp.decisioning.specialisms`. Drives method-conformance validation at boot AND projects to the wire ``specialisms`` field. :param creative_agents: Optional list of creative-agent endpoints the platform delegates creative review/generation to. Empty list means "no creative-agent integration; review is in-house." :param config: Free-form adopter-defined config exposed on capabilities. Use sparingly — strongly-typed fields above are preferred. :param governance_aware: Set ``True`` ONLY when the platform implements ``governance-*`` specialisms AND has wired a custom :class:`adcp.decisioning.state.StateReader` that returns real :data:`adcp.decisioning.state.GovernanceContextJWS` values. Defaults ``False`` — non-governance adopters never touch this flag. Stage 3 dispatch (foundation PR's ``validate_platform``) will fail-fast at server boot when a platform claims a ``governance-*`` specialism without setting this flag and wiring a real ``StateReader`` — silent governance-gate skipping is a security regression the framework refuses to ship. The flag itself is the contract that lands now; the enforcement lands in Stage 3. See ``docs/proposals/decisioning-platform-dispatch-design.md#d15``. :param webhook_signing_managed_externally: Set ``True`` only when the platform advertises ``webhook_signing.supported=True`` but signs outbound webhooks through adopter-owned infrastructure rather than the SDK's :class:`adcp.webhook_sender.WebhookSender`. The framework then trusts the adopter's capability declaration when no SDK sender/supervisor is wired. Deprecated flat-declaration shortcuts (will be removed in v5): :param channels: Inventory channels the platform serves — ``'display'``, ``'video'``, etc. Not currently projected to any wire field (the spec's ``portfolio.primary_channels`` requires ``portfolio.publisher_domains`` alongside, which the flat ``channels`` field cannot supply). Use ``media_buy=MediaBuy(portfolio=Portfolio(...))`` instead. Deprecated; emits ``DeprecationWarning`` at projection. :param pricing_models: Pricing models — ``'cpm'``, ``'cpc'``, etc. Superseded by ``media_buy.supported_pricing_models``. The projection prefers the structured field when both are set; emits ``DeprecationWarning`` when ``pricing_models`` is set. :param supported_billing: Billing parties this seller invoices — any subset of ``{"operator", "agent", "advertiser"}``. Superseded by ``account.supported_billing``. The projection prefers the structured field when both are set; emits ``DeprecationWarning`` when ``supported_billing`` is set (alone or alongside ``account``). """ # SDK-internal dispatch (not wire fields) specialisms: list[Specialism | str] = field(default_factory=list) creative_agents: list[Any] = field(default_factory=list) config: dict[str, Any] = field(default_factory=dict) governance_aware: bool = False webhook_signing_managed_externally: bool = False # When True, the framework calls get_products and slices the full result # set to the requested page. Only suitable for in-memory / small-catalog # adopters whose get_products returns the complete unfiltered product set. # Adopters with DB-backed catalogs at production scale MUST leave this # False and handle cursor logic natively — returning 100k products only # to discard 99 950 is a silent production latency and memory spike. auto_paginate: bool = False # Wire capability blocks (mirror ``GetAdcpCapabilitiesResponse``) adcp: Adcp | None = None account: CapabilitiesAccount | None = None media_buy: CapabilitiesMediaBuy | None = None signals: Signals | None = None governance: Governance | None = None sponsored_intelligence: SponsoredIntelligence | None = None brand: Brand | None = None creative: CapabilitiesCreative | None = None measurement: Measurement | None = None request_signing: RequestSigning | None = None webhook_signing: WebhookSigning | None = None identity: Identity | None = None compliance_testing: ComplianceTesting | None = None experimental_features: list[ExperimentalFeature | str] | None = None supported_protocols: list[SupportedProtocol] | None = None # Deprecated flat-declaration shortcuts (removed in v5) channels: list[str] = field(default_factory=list) pricing_models: list[str] = field(default_factory=list) supported_billing: list[str] = field(default_factory=list) def __post_init__(self) -> None: """Normalize spec-known specialism strings to enum members. Accepts either ``Specialism`` enum members (the type-safe form adopters should prefer) or AdCP slug strings (back-compat with existing code, novel pre-spec slugs, and intentional-typo paths the validator wants to diagnose). Strings that match a known ``Specialism`` value are coerced; unknown strings pass through unchanged so :func:`adcp.decisioning.dispatch.validate_platform` can surface them with typo-detection or forward-compat warnings at server boot. Adopter code is encouraged to import ``Specialism`` from :mod:`adcp.decisioning.capabilities` and write ``specialisms=[Specialism.sales_non_guaranteed]`` for clean type checks. The string path stays available for config-driven declarations, downstream test code, and pre-spec experimental slugs. """ coerced: list[Specialism | str] = [] for entry in self.specialisms: if isinstance(entry, Specialism): coerced.append(entry) continue try: coerced.append(Specialism(entry)) except ValueError: # Novel / typo / pre-spec slug — keep as string so the # validator's typo-vs-novel-vs-unenforced classification # at boot can surface the right diagnostic. coerced.append(entry) self.specialisms = coerced # Deprecation warnings for legacy flat fields. Fire at # construction so ``stacklevel=2`` points at the adopter's # ``DecisioningCapabilities(...)`` declaration site (where the # legacy field was set), not at the MCP dispatcher that later # called ``get_adcp_capabilities``. Python's warnings registry # deduplicates by ``(message, module, lineno)`` so each unique # declaration warns once per process. if self.supported_billing: warnings.warn( ( "DecisioningCapabilities.supported_billing is deprecated; " "set ``account=Account(supported_billing=[...])`` instead. " "Will be removed in v5." ), DeprecationWarning, stacklevel=2, ) if self.pricing_models: warnings.warn( ( "DecisioningCapabilities.pricing_models is deprecated; " "set ``media_buy=MediaBuy(supported_pricing_models=[...])`` " "instead. Will be removed in v5." ), DeprecationWarning, stacklevel=2, ) if self.channels: warnings.warn( ( "DecisioningCapabilities.channels is deprecated and no longer " "projected to the wire (the spec's ``portfolio.primary_channels`` " "requires ``portfolio.publisher_domains`` alongside, which the " "flat ``channels`` field cannot supply). Set " "``media_buy=MediaBuy(portfolio=Portfolio(...))`` instead. " "Will be removed in v5." ), DeprecationWarning, stacklevel=2, ) # ``supported_protocols`` semantically rolls UP FROM specialisms # per spec — it's the storyboard commitment, with specialisms as # the sub-claims that contribute to it. The framework's # auto-derivation (see ``handler.py:get_adcp_capabilities``) is # ergonomic but inverts the spec's data direction. Adopters # leaning on auto-derive get a one-shot UserWarning steering # them toward declaring ``supported_protocols`` explicitly. The # auto-derive path is supported indefinitely; the warning is a # gentle nudge toward the spec-aligned form, not a deprecation. if self.supported_protocols is None and self.specialisms: warnings.warn( ( "DecisioningCapabilities.supported_protocols was not declared; " "the framework will auto-derive it from ``specialisms`` via " "``SPECIALISM_TO_PROTOCOLS``. Per spec, ``supported_protocols`` is " "the primary storyboard-commitment declaration — set it " "explicitly via ``supported_protocols=[SupportedProtocol.media_buy, " "...]`` so the spec's intent (specialisms roll up to protocols) " "is preserved at the declaration site. Auto-derivation is not " "deprecated; this warning fires once per declaration site." ), UserWarning, stacklevel=2, )What a platform claims to support.
Read by
validate_platformat server boot to confirm each declared specialism has the methods it requires, and surfaced via the framework's auto-generatedget_adcp_capabilitiesresponse so buyers can pre-flight without trial-and-error tool calls.Capability declaration shape mirrors the AdCP wire spec (
protocol/get-adcp-capabilities-response.json). Adopters import the typed sub-models from :mod:adcp.decisioning.capabilities— that submodule re-exports under wire-spec names, so declarations read 1:1 against the spec::from adcp.decisioning import DecisioningCapabilities from adcp.decisioning.capabilities import ( Account, MediaBuy, Targeting, GeoMetros, IdempotencySupported, Specialism, ) capabilities = DecisioningCapabilities( specialisms=[Specialism.sales_non_guaranteed.value], adcp=Adcp( major_versions=[3], idempotency=IdempotencySupported( supported=True, replay_ttl_seconds=86400, ), ), account=Account(supported_billing=["operator"]), media_buy=MediaBuy( supported_pricing_models=["cpm"], execution=Execution( targeting=Targeting(geo_countries=True), ), ), )Wire capability blocks (one field per top-level wire field):
:param adcp: Core protocol info —
major_versionsandidempotency. Required on the wire; defaults toNonemeans the framework will project a non-conformant response (the boot-time validator catches this). :param account: Account-management capabilities (billing, OAuth, sandbox). :param media_buy: Media-buy protocol capabilities — pricing models, reporting delivery methods, execution targeting, etc. Expected whenmedia_buyis insupported_protocols.execution.targeting.geo_postal_areasmay be declared once using the native AdCP 3.1 country-keyed model; the framework auto-projects it to the deprecated fused boolean model for pre-3.1 capability callers. :param signals: Signals protocol capabilities. Only emit whensignalsis insupported_protocols. :param governance: Governance protocol capabilities. :param sponsored_intelligence: SI protocol capabilities. :param brand: Brand protocol capabilities. :param creative: Creative protocol capabilities. :param measurement: Experimental measurement protocol capabilities. :param request_signing: RFC 9421 inbound request signing posture. :param webhook_signing: Outbound webhook-signing posture. :param identity: Operator key-scoping / compromise-response identity posture (advisory in 3.x). :param compliance_testing: Deterministic-testing capability viacomply_test_controller. Omit entirely if unsupported. :param experimental_features: Experimental surfaces implemented by the platform. Required for experimental capability blocks such asmeasurement. :param supported_protocols: Override for thesupported_protocolswire field. DefaultNone= derive from :attr:specialismsviaSPECIALISM_TO_PROTOCOLS. Set explicitly when claiming a protocol whose specialisms aren't all listed (e.g. transitional state, generic seller passing the baseline storyboard without claiming a specific specialism).SDK-internal dispatch (not wire fields):
:param specialisms: AdCP specialism slugs the platform claims — e.g.
['sales-non-guaranteed', 'sales-broadcast-tv'],['audience-sync'],['signal-marketplace'], or['signal-owned']. Each maps to aProtocolclass under :mod:adcp.decisioning.specialisms. Drives method-conformance validation at boot AND projects to the wirespecialismsfield. :param creative_agents: Optional list of creative-agent endpoints the platform delegates creative review/generation to. Empty list means "no creative-agent integration; review is in-house." :param config: Free-form adopter-defined config exposed on capabilities. Use sparingly — strongly-typed fields above are preferred. :param governance_aware: SetTrueONLY when the platform implementsgovernance-*specialisms AND has wired a custom :class:StateReaderthat returns real :data:GovernanceContextJWSvalues. DefaultsFalse— non-governance adopters never touch this flag.Stage 3 dispatch (foundation PR's <code>validate\_platform</code>) will fail-fast at server boot when a platform claims a ``governance-*`` specialism without setting this flag and wiring a real <code>StateReader</code> — silent governance-gate skipping is a security regression the framework refuses to ship. The flag itself is the contract that lands now; the enforcement lands in Stage 3. See ``docs/proposals/decisioning-platform-dispatch-design.md#d15``.:param webhook_signing_managed_externally: Set
Trueonly when the platform advertiseswebhook_signing.supported=Truebut signs outbound webhooks through adopter-owned infrastructure rather than the SDK's :class:WebhookSender. The framework then trusts the adopter's capability declaration when no SDK sender/supervisor is wired.Deprecated flat-declaration shortcuts (will be removed in v5):
:param channels: Inventory channels the platform serves —
'display','video', etc. Not currently projected to any wire field (the spec'sportfolio.primary_channelsrequiresportfolio.publisher_domainsalongside, which the flatchannelsfield cannot supply). Usemedia_buy=MediaBuy(portfolio=Portfolio(...))instead. Deprecated; emitsDeprecationWarningat projection. :param pricing_models: Pricing models —'cpm','cpc', etc. Superseded bymedia_buy.supported_pricing_models. The projection prefers the structured field when both are set; emitsDeprecationWarningwhenpricing_modelsis set. :param supported_billing: Billing parties this seller invoices — any subset of{"operator", "agent", "advertiser"}. Superseded byaccount.supported_billing. The projection prefers the structured field when both are set; emitsDeprecationWarningwhensupported_billingis set (alone or alongsideaccount).Instance variables
var account : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Account | Nonevar adcp : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Adcp | Nonevar auto_paginate : boolvar brand : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Brand | Nonevar channels : list[str]var compliance_testing : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.ComplianceTesting | Nonevar config : dict[str, typing.Any]var creative : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Creative | Nonevar creative_agents : list[typing.Any]var experimental_features : list[adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.ExperimentalFeature | str] | Nonevar governance : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Governance | Nonevar governance_aware : boolvar identity : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Identity | Nonevar measurement : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Measurement | Nonevar media_buy : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.MediaBuy | Nonevar pricing_models : list[str]var request_signing : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.RequestSigning | Nonevar signals : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Signals | Nonevar specialisms : list[adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.Specialism | str]var sponsored_intelligence : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.SponsoredIntelligence | Nonevar supported_billing : list[str]var supported_protocols : list[adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.SupportedProtocol] | Nonevar webhook_signing : adcp.types.generated_poc.bundled.protocol.get_adcp_capabilities_response.WebhookSigning | Nonevar webhook_signing_managed_externally : bool
class DecisioningPlatform-
Expand source code
class DecisioningPlatform: """Adopter-facing base class for the v6.0 framework. Subclasses set: * :attr:`capabilities` — what the platform claims to support * :attr:`accounts` — an :class:`AccountStore` instance defining how to resolve a wire reference + auth context to an :class:`Account` Then implement specialism methods directly on the subclass (``get_products``, ``create_media_buy``, ``sync_audiences``, etc.). Each method takes a typed Pydantic request model + a :class:`RequestContext[TMeta]` and returns a typed response (or raises :class:`AdcpError`). The dispatch adapter (:func:`adcp.decisioning.create_adcp_server_from_platform`) discovers methods via ``hasattr``, validates against ``capabilities.specialisms``, and routes requests through the framework's existing ``adcp.server.serve()`` infrastructure. Example:: class HelloSeller(DecisioningPlatform): capabilities = DecisioningCapabilities( specialisms=["sales-non-guaranteed"], channels=["display"], pricing_models=["cpm"], ) accounts = SingletonAccounts(account_id="hello") def get_products(self, req, ctx): return GetProductsResponse(products=[...]) def create_media_buy(self, req, ctx): return CreateMediaBuySuccess(media_buy_id="mb_1", ...) Per-method signatures are documented in the per-specialism Protocol classes under :mod:`adcp.decisioning.specialisms` — those are the canonical contract reference. The base class itself is intentionally minimal so adopters can mix in cross-cutting helpers without inheritance constraints. """ #: Required: the platform's capability declaration. Subclasses #: override. capabilities: DecisioningCapabilities = DecisioningCapabilities() #: Required: the platform's account-resolution strategy. #: Subclasses set to a :class:`SingletonAccounts`, #: :class:`ExplicitAccounts`, :class:`FromAuthAccounts`, or #: custom :class:`AccountStore` instance. Type erased to ``Any`` #: at the base because the typed shape is platform-specific #: (different ``TMeta`` per adopter); ``validate_platform`` #: confirms an :class:`AccountStore` instance is set. accounts: AccountStore[Any] = None # type: ignore[assignment] #: Optional: the adopter's production upstream API URL. Adapters #: that talk to a real upstream (GAM, Kevel, FreeWheel, etc.) set #: this to the canonical production endpoint #: (``"https://googleads.googleapis.com"``, #: ``"https://api.kevel.co"``, etc.). The value is fixed per #: platform — credentials and per-tenant routing flow through #: ``ctx.auth_info`` and ``ctx.account.metadata``, not through #: this URL. #: #: Leave ``None`` for platforms that don't talk to an HTTP #: upstream (pure in-process, in-memory, or composing via #: framework-level resolvers only). #: #: When :attr:`upstream_url` is ``None``, :meth:`upstream_for` #: refuses to construct a client for ``mode='live'`` / #: ``mode='sandbox'`` accounts (raising ``CONFIGURATION_ERROR``). #: ``mode='mock'`` accounts always read from #: ``account.metadata['mock_upstream_url']`` and never consult #: this attribute. upstream_url: str | None = None def get_adcp_capabilities_for_request( self, params: GetAdcpCapabilitiesRequest | dict[str, Any] | None = None, context: ToolContext | None = None, ) -> DecisioningCapabilities | None | Awaitable[DecisioningCapabilities | None]: """Optionally override capabilities for the current discovery request. The class-level :attr:`capabilities` declaration remains the default source of truth. Multi-tenant adopters can override this hook when a valid capability block depends on request context, such as tenant publisher domains or whether the tenant has an active webhook-signing credential. Return ``None`` to use :attr:`capabilities` unchanged, or return a complete :class:`DecisioningCapabilities` instance for this request. The framework still performs the canonical ``get_adcp_capabilities`` response projection; this hook is not a raw wire-response override. Manual postal compatibility projection is not needed; request-scoped overrides run before the framework projects ``geo_postal_areas`` for the caller's AdCP version. The hook may be synchronous or asynchronous. """ del params, context return None def upstream_for( self, ctx: RequestContext[Any], *, auth: UpstreamAuth | None = None, default_headers: dict[str, str] | None = None, timeout: float = 30.0, treat_404_as_none: bool = True, ) -> UpstreamHttpClient: """Return an :class:`UpstreamHttpClient` pointed at the right URL for this request's resolved account. Routing rules: - ``mode='live'`` / ``mode='sandbox'``: client at :attr:`upstream_url`. The adopter's production upstream URL is fixed per platform; only credentials vary per tenant (and flow through ``auth`` / ``ctx.auth_info``). - ``mode='mock'``: client at ``ctx.account.metadata['mock_upstream_url']``. The adopter populates this on mock-mode accounts; the framework points the client at the per-tenant fixture URL. Adapter business logic runs unchanged. Clients are cached per-platform-instance keyed by ``(base_url, id(auth))`` so repeated requests pool connections through one ``httpx.AsyncClient``. Different auth strategies get distinct clients (the auth is injected at construction and can't be swapped per-request from a cached client). :param ctx: The current request context. Required for ``ctx.account.mode`` and ``ctx.account.metadata``. :param auth: Auth strategy for the upstream. Defaults to :class:`NoAuth` (no header injected). Adopters typically pass a :class:`StaticBearer`, :class:`DynamicBearer`, or :class:`ApiKey`. The same auth is used regardless of mode — mock-mode fixtures usually accept any token, but adopters may want their adapter to send identical headers in mock and live so the wire shape matches end-to-end. :param default_headers: Headers included on every request (e.g. ``X-API-Version``). :param timeout: Per-request timeout in seconds. Default 30.0. :param treat_404_as_none: When ``True`` (default), GET/DELETE 404s return ``None`` rather than raising. :raises AdcpError: ``CONFIGURATION_ERROR`` when: - Account is ``mode='mock'`` but ``account.metadata['mock_upstream_url']`` is missing, empty, or non-string. Adopter must populate it on mock-mode accounts in their ``AccountStore.resolve``. - Account is ``mode='live'`` / ``mode='sandbox'`` but ``self.upstream_url`` is ``None``. Adopter must declare the production URL on their platform subclass. """ account = ctx.account mode = get_account_mode(account) if mode == "mock": base_url = get_mock_upstream_url(account) if base_url is None: raise AdcpError( "CONFIGURATION_ERROR", message=( "account is mode='mock' but no 'mock_upstream_url' " "string in metadata; populate it in " "AccountStore.resolve for mock-mode accounts. " "See docs/handler-authoring.md#mock-mode-upstream-routing." ), recovery="terminal", field="account.metadata.mock_upstream_url", ) else: # mode in {'live', 'sandbox'} — point at the platform's # declared production URL. Sandbox is the adopter's own # test infra; the URL is the same as live (credentials # + tenant routing change, not the URL). if self.upstream_url is None: raise AdcpError( "CONFIGURATION_ERROR", message=( f"platform {type(self).__name__!s} has no " f"upstream_url declared but resolved account is " f"mode={mode!r}. Set the class attribute " "upstream_url to the production upstream API URL, " "or mark the account mode='mock' and populate " "metadata['mock_upstream_url']." ), recovery="terminal", ) base_url = self.upstream_url return self._cached_upstream_client( base_url=base_url, auth=auth or NoAuth(), default_headers=default_headers, timeout=timeout, treat_404_as_none=treat_404_as_none, ) def _cached_upstream_client( self, *, base_url: str, auth: UpstreamAuth, default_headers: dict[str, str] | None, timeout: float, treat_404_as_none: bool, ) -> UpstreamHttpClient: """Per-instance cached :class:`UpstreamHttpClient` factory. Cache key is ``(base_url, id(auth))``. Pooling correctness requires keying on the auth instance — different ``DynamicBearer`` closures for different tenants need distinct clients so the token resolver doesn't get accidentally shared, and the ``UpstreamHttpClient`` itself owns the underlying ``httpx.AsyncClient`` connection pool. Cache lives on the platform instance (``__dict__`` lazy init); multi-platform processes don't cross-pollute. Adopter code does not mutate the cache; lifecycle is "create once, reuse for the platform instance's lifetime." """ cache: dict[tuple[str, int], UpstreamHttpClient] | None cache = getattr(self, "_upstream_client_cache", None) if cache is None: cache = {} self._upstream_client_cache = cache key = (base_url, id(auth)) existing = cache.get(key) if existing is not None: return existing client = UpstreamHttpClient( base_url=base_url, auth=auth, default_headers=default_headers, timeout=timeout, treat_404_as_none=treat_404_as_none, ) cache[key] = client return clientAdopter-facing base class for the v6.0 framework.
Subclasses set:
- :attr:
capabilities— what the platform claims to support - :attr:
accounts— an :class:AccountStoreinstance defining how to resolve a wire reference + auth context to an :class:Account
Then implement specialism methods directly on the subclass (
get_products,create_media_buy,sync_audiences, etc.). Each method takes a typed Pydantic request model + a :class:RequestContext[TMeta]and returns a typed response (or raises :class:AdcpError).The dispatch adapter (:func:
create_adcp_server_from_platform()) discovers methods viahasattr, validates againstcapabilities.specialisms, and routes requests through the framework's existingserve()infrastructure.Example::
class HelloSeller(DecisioningPlatform): capabilities = DecisioningCapabilities( specialisms=["sales-non-guaranteed"], channels=["display"], pricing_models=["cpm"], ) accounts = SingletonAccounts(account_id="hello") def get_products(self, req, ctx): return GetProductsResponse(products=[...]) def create_media_buy(self, req, ctx): return CreateMediaBuySuccess(media_buy_id="mb_1", ...)Per-method signatures are documented in the per-specialism Protocol classes under :mod:
adcp.decisioning.specialisms— those are the canonical contract reference. The base class itself is intentionally minimal so adopters can mix in cross-cutting helpers without inheritance constraints.Subclasses
- LazyPlatformRouter
- PlatformRouter
- adcp.decisioning.platform_router._RegistryPlatformAdapter
Class variables
var accounts : AccountStore[Any]-
Required: the platform's account-resolution strategy. Subclasses set to a :class:
SingletonAccounts, :class:ExplicitAccounts, :class:FromAuthAccounts, or custom :class:AccountStoreinstance. Type erased toAnyat the base because the typed shape is platform-specific (differentTMetaper adopter);validate_platformconfirms an :class:AccountStoreinstance is set. var capabilities : DecisioningCapabilities-
Required: the platform's capability declaration. Subclasses override.
var upstream_url : str | None-
When :attr:
upstream_urlisNone, :meth:upstream_forrefuses to construct a client formode='live'/mode='sandbox'accounts (raisingCONFIGURATION_ERROR).mode='mock'accounts always read fromaccount.metadata['mock_upstream_url']and never consult this attribute.
Methods
def get_adcp_capabilities_for_request(self,
params: GetAdcpCapabilitiesRequest | dict[str, Any] | None = None,
context: ToolContext | None = None) ‑> DecisioningCapabilities | None | Awaitable[DecisioningCapabilities | None]-
Expand source code
def get_adcp_capabilities_for_request( self, params: GetAdcpCapabilitiesRequest | dict[str, Any] | None = None, context: ToolContext | None = None, ) -> DecisioningCapabilities | None | Awaitable[DecisioningCapabilities | None]: """Optionally override capabilities for the current discovery request. The class-level :attr:`capabilities` declaration remains the default source of truth. Multi-tenant adopters can override this hook when a valid capability block depends on request context, such as tenant publisher domains or whether the tenant has an active webhook-signing credential. Return ``None`` to use :attr:`capabilities` unchanged, or return a complete :class:`DecisioningCapabilities` instance for this request. The framework still performs the canonical ``get_adcp_capabilities`` response projection; this hook is not a raw wire-response override. Manual postal compatibility projection is not needed; request-scoped overrides run before the framework projects ``geo_postal_areas`` for the caller's AdCP version. The hook may be synchronous or asynchronous. """ del params, context return NoneOptionally override capabilities for the current discovery request.
The class-level :attr:
capabilitiesdeclaration remains the default source of truth. Multi-tenant adopters can override this hook when a valid capability block depends on request context, such as tenant publisher domains or whether the tenant has an active webhook-signing credential.Return
Noneto use :attr:capabilitiesunchanged, or return a complete :class:DecisioningCapabilitiesinstance for this request. The framework still performs the canonicalget_adcp_capabilitiesresponse projection; this hook is not a raw wire-response override. Manual postal compatibility projection is not needed; request-scoped overrides run before the framework projectsgeo_postal_areasfor the caller's AdCP version. The hook may be synchronous or asynchronous. def upstream_for(self,
ctx: RequestContext[Any],
*,
auth: UpstreamAuth | None = None,
default_headers: dict[str, str] | None = None,
timeout: float = 30.0,
treat_404_as_none: bool = True) ‑> UpstreamHttpClient-
Expand source code
def upstream_for( self, ctx: RequestContext[Any], *, auth: UpstreamAuth | None = None, default_headers: dict[str, str] | None = None, timeout: float = 30.0, treat_404_as_none: bool = True, ) -> UpstreamHttpClient: """Return an :class:`UpstreamHttpClient` pointed at the right URL for this request's resolved account. Routing rules: - ``mode='live'`` / ``mode='sandbox'``: client at :attr:`upstream_url`. The adopter's production upstream URL is fixed per platform; only credentials vary per tenant (and flow through ``auth`` / ``ctx.auth_info``). - ``mode='mock'``: client at ``ctx.account.metadata['mock_upstream_url']``. The adopter populates this on mock-mode accounts; the framework points the client at the per-tenant fixture URL. Adapter business logic runs unchanged. Clients are cached per-platform-instance keyed by ``(base_url, id(auth))`` so repeated requests pool connections through one ``httpx.AsyncClient``. Different auth strategies get distinct clients (the auth is injected at construction and can't be swapped per-request from a cached client). :param ctx: The current request context. Required for ``ctx.account.mode`` and ``ctx.account.metadata``. :param auth: Auth strategy for the upstream. Defaults to :class:`NoAuth` (no header injected). Adopters typically pass a :class:`StaticBearer`, :class:`DynamicBearer`, or :class:`ApiKey`. The same auth is used regardless of mode — mock-mode fixtures usually accept any token, but adopters may want their adapter to send identical headers in mock and live so the wire shape matches end-to-end. :param default_headers: Headers included on every request (e.g. ``X-API-Version``). :param timeout: Per-request timeout in seconds. Default 30.0. :param treat_404_as_none: When ``True`` (default), GET/DELETE 404s return ``None`` rather than raising. :raises AdcpError: ``CONFIGURATION_ERROR`` when: - Account is ``mode='mock'`` but ``account.metadata['mock_upstream_url']`` is missing, empty, or non-string. Adopter must populate it on mock-mode accounts in their ``AccountStore.resolve``. - Account is ``mode='live'`` / ``mode='sandbox'`` but ``self.upstream_url`` is ``None``. Adopter must declare the production URL on their platform subclass. """ account = ctx.account mode = get_account_mode(account) if mode == "mock": base_url = get_mock_upstream_url(account) if base_url is None: raise AdcpError( "CONFIGURATION_ERROR", message=( "account is mode='mock' but no 'mock_upstream_url' " "string in metadata; populate it in " "AccountStore.resolve for mock-mode accounts. " "See docs/handler-authoring.md#mock-mode-upstream-routing." ), recovery="terminal", field="account.metadata.mock_upstream_url", ) else: # mode in {'live', 'sandbox'} — point at the platform's # declared production URL. Sandbox is the adopter's own # test infra; the URL is the same as live (credentials # + tenant routing change, not the URL). if self.upstream_url is None: raise AdcpError( "CONFIGURATION_ERROR", message=( f"platform {type(self).__name__!s} has no " f"upstream_url declared but resolved account is " f"mode={mode!r}. Set the class attribute " "upstream_url to the production upstream API URL, " "or mark the account mode='mock' and populate " "metadata['mock_upstream_url']." ), recovery="terminal", ) base_url = self.upstream_url return self._cached_upstream_client( base_url=base_url, auth=auth or NoAuth(), default_headers=default_headers, timeout=timeout, treat_404_as_none=treat_404_as_none, )Return an :class:
UpstreamHttpClientpointed at the right URL for this request's resolved account.Routing rules:
mode='live'/mode='sandbox': client at :attr:upstream_url. The adopter's production upstream URL is fixed per platform; only credentials vary per tenant (and flow throughauth/ctx.auth_info).mode='mock': client atctx.account.metadata['mock_upstream_url']. The adopter populates this on mock-mode accounts; the framework points the client at the per-tenant fixture URL. Adapter business logic runs unchanged.
Clients are cached per-platform-instance keyed by
(base_url, id(auth))so repeated requests pool connections through onehttpx.AsyncClient. Different auth strategies get distinct clients (the auth is injected at construction and can't be swapped per-request from a cached client).:param ctx: The current request context. Required for
ctx.account.modeandctx.account.metadata. :param auth: Auth strategy for the upstream. Defaults to :class:NoAuth(no header injected). Adopters typically pass a :class:StaticBearer, :class:DynamicBearer, or :class:ApiKey. The same auth is used regardless of mode — mock-mode fixtures usually accept any token, but adopters may want their adapter to send identical headers in mock and live so the wire shape matches end-to-end. :param default_headers: Headers included on every request (e.g.X-API-Version). :param timeout: Per-request timeout in seconds. Default 30.0. :param treat_404_as_none: WhenTrue(default), GET/DELETE 404s returnNonerather than raising.:raises AdcpError:
CONFIGURATION_ERRORwhen:- Account is ``mode='mock'`` but ``account.metadata['mock_upstream_url']`` is missing, empty, or non-string. Adopter must populate it on mock-mode accounts in their <code>AccountStore.resolve</code>. - Account is ``mode='live'`` / ``mode='sandbox'`` but <code>self.upstream\_url</code> is <code>None</code>. Adopter must declare the production URL on their platform subclass.
- :attr: