Module adcp.decisioning.media_buy_store

create_media_buy_store() — opt-in framework wiring that gates targeting_overlay echo on the seller's declared specialisms.

The seller's spec contract on schemas/cache/<v>/media-buy/get-media-buys-response.json requires that sellers claiming the property-lists or collection-lists specialism echo persisted property_list / collection_list references inside the packages[].targeting_overlay they return on get_media_buys. This factory lets adopters wire that contract once, at the framework boundary, instead of every adapter persisting + echoing by hand.

Mirrors the JS-side createMediaBuyStore from @adcp/sdk@6.7 (commit dda2a77e) with one shape change: the JS factory builds the persistence itself on top of an AdcpStateStore. The Python SDK doesn't ship an AdcpStateStore Protocol, so adopters supply their own :class:MediaBuyStore implementation; this factory wraps it with specialism-aware gating. Adopters who want a reference impl can crib the in-memory pattern from the test suite.

Usage::

from adcp.decisioning import create_media_buy_store

platform.media_buy_store = create_media_buy_store(
    adopter_store, capabilities=platform.capabilities,
)

Behavior:

  • Seller claims property-lists or collection-lists → every method delegates to the adopter store.
  • Seller claims neither → persist_from_create and merge_from_update are no-ops; backfill returns the response unchanged. The adopter store is never invoked.

The wrapper is always a fresh object so adopters can reason about identity at the assignment site without aliasing surprises.

Functions

def create_media_buy_store(adopter_store: MediaBuyStore,
*,
capabilities: DecisioningCapabilities) ‑> MediaBuyStore
Expand source code
def create_media_buy_store(
    adopter_store: MediaBuyStore,
    *,
    capabilities: DecisioningCapabilities,
) -> MediaBuyStore:
    """Wrap an adopter :class:`MediaBuyStore` with specialism-aware
    ``targeting_overlay`` echo gating.

    :param adopter_store: The persistence + echo implementation. Must
        satisfy the :class:`MediaBuyStore` Protocol — three methods
        (``persist_from_create``, ``merge_from_update``, ``backfill``)
        sync or async.
    :param capabilities: The seller's :class:`DecisioningCapabilities`.
        Read once at construction to decide whether the wrapper
        delegates or no-ops; not re-read per request, so adopters who
        mutate ``capabilities.specialisms`` after building the store
        won't see the change reflected. Build-time decision matches the
        boot-time validation pattern used elsewhere
        (``validate_platform``).

    :returns: A :class:`MediaBuyStore` wrapper. When ``capabilities.specialisms``
        intersects ``{property-lists, collection-lists}``, every method
        delegates to ``adopter_store``. Otherwise every method is a
        no-op pass-through and the adopter store is never invoked.

    The returned object is always distinct from ``adopter_store`` — even
    on the no-op path — so adopters can reason about identity at the
    assignment site (``platform.media_buy_store = ...``) without
    aliasing the underlying persistence layer.
    """
    # ``capabilities.specialisms`` is ``list[Specialism | str]`` per #479
    # — spec-known slugs are coerced to ``Specialism`` enum members at
    # construction; novel/typo slugs pass through as strings. Extract the
    # slug uniformly before set intersection.
    declared_slugs = {
        entry.value if hasattr(entry, "value") else entry for entry in capabilities.specialisms
    }
    claimed = declared_slugs & _OVERLAY_ECHO_SPECIALISMS
    if claimed:
        return _ActiveMediaBuyStore(adopter_store)
    return _NoopMediaBuyStore(adopter_store)

Wrap an adopter :class:MediaBuyStore with specialism-aware targeting_overlay echo gating.

:param adopter_store: The persistence + echo implementation. Must satisfy the :class:MediaBuyStore Protocol — three methods (persist_from_create, merge_from_update, backfill) sync or async. :param capabilities: The seller's :class:DecisioningCapabilities. Read once at construction to decide whether the wrapper delegates or no-ops; not re-read per request, so adopters who mutate capabilities.specialisms after building the store won't see the change reflected. Build-time decision matches the boot-time validation pattern used elsewhere (validate_platform).

:returns: A :class:MediaBuyStore wrapper. When capabilities.specialisms intersects {property-lists, collection-lists}, every method delegates to adopter_store. Otherwise every method is a no-op pass-through and the adopter store is never invoked.

The returned object is always distinct from adopter_store — even on the no-op path — so adopters can reason about identity at the assignment site (platform.media_buy_store = ...) without aliasing the underlying persistence layer.

Classes

class MediaBuyStore (*args, **kwargs)
Expand source code
@runtime_checkable
class MediaBuyStore(Protocol):
    """Adopter-supplied persistence + echo for ``targeting_overlay``.

    Three methods cover the full lifecycle of a per-package overlay:

    * :meth:`persist_from_create` records overlays from a successful
      ``create_media_buy``, joining the request's per-package overlay
      with the response's seller-assigned ``package_id`` (or
      ``buyer_ref`` when present).
    * :meth:`merge_from_update` applies ``update_media_buy`` patches
      with deep-merge semantics: keys absent from the patch keep prior
      values, keys present with non-null values replace, keys present
      and ``None`` clear.
    * :meth:`backfill` fills in missing
      ``packages[].targeting_overlay`` on a ``get_media_buys`` response
      from the persisted store. Mutates the response in place; packages
      the seller already echoed are left untouched.

    Methods may be sync or async — the wrapper awaits at call time.
    """

    def persist_from_create(
        self,
        account_id: str,
        request: Any,
        result: Any,
    ) -> Any: ...

    def merge_from_update(
        self,
        account_id: str,
        media_buy_id: str,
        patch: Any,
    ) -> Any: ...

    def backfill(self, account_id: str, result: Any) -> Any: ...

Adopter-supplied persistence + echo for targeting_overlay.

Three methods cover the full lifecycle of a per-package overlay:

  • :meth:persist_from_create records overlays from a successful create_media_buy, joining the request's per-package overlay with the response's seller-assigned package_id (or buyer_ref when present).
  • :meth:merge_from_update applies update_media_buy patches with deep-merge semantics: keys absent from the patch keep prior values, keys present with non-null values replace, keys present and None clear.
  • :meth:backfill fills in missing packages[].targeting_overlay on a get_media_buys response from the persisted store. Mutates the response in place; packages the seller already echoed are left untouched.

Methods may be sync or async — the wrapper awaits at call time.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def backfill(self, account_id: str, result: Any) ‑> Any
Expand source code
def backfill(self, account_id: str, result: Any) -> Any: ...
def merge_from_update(self, account_id: str, media_buy_id: str, patch: Any) ‑> Any
Expand source code
def merge_from_update(
    self,
    account_id: str,
    media_buy_id: str,
    patch: Any,
) -> Any: ...
def persist_from_create(self, account_id: str, request: Any, result: Any) ‑> Any
Expand source code
def persist_from_create(
    self,
    account_id: str,
    request: Any,
    result: Any,
) -> Any: ...