Module adcp.decisioning.roster_store

Roster-backed :class:AccountStore factory for resolution='explicit' publisher-curated platforms.

Use when the publisher curates accounts out-of-band (admin UI, config file, publisher-managed DB row) and buyers pass account_id on every request. The adopter holds a fixed allowlist of accounts; the SDK provides the :class:AccountStore plumbing.

Pairs with the existing reference adapters. Pick by asking who creates the account?:

  • Buyer self-onboards via sync_accounts — multi-tenant adopter store (custom :class:AccountStore impl). Framework owns persistence; the buyer's first request to a tenant-scoped tool resolves from a prior sync. (LinkedIn, some retail-media operators.)
  • Upstream OAuth API owns the roster — :class:ExplicitAccounts with a loader that calls the upstream /me/adaccounts. Returns whatever the OAuth bearer is authorized for. (Snap, Meta, TikTok.)
  • Publisher ops curates the roster out-of-band — :func:create_roster_account_store() (this module). Adopter keeps the persistence layer; the SDK provides the AccountStore. (Most SSPs, broadcasters, and retail-media networks where AE/CSM provisions the account in an internal admin tool before the buyer ever calls.)

Design Notes

  • Roster IS the allowlist. Auth-based filtering happens upstream of this layer — the framework's account-resolution gate enforces principal-vs-account scope. The store does not consult ctx to filter list.
  • Immutable post-construction. The input dict is copied into an internal :class:MappingProxyType so external mutation of the caller's dict cannot widen the allowlist after the fact. Adopters who need a dynamic roster wrap :func:create_roster_account_store() in their own factory and rebuild on change.
  • Write paths fail closed per-entry. :meth:upsert and :meth:sync_governance return PERMISSION_DENIED for every input entry rather than silently no-oping (which would lie to the buyer about their write succeeding) or operation-level raising (which would fail the whole batch on a single bad entry). Per-entry rejection matches the wire shape and lets the buyer correlate the failure to their request entry.
  • Resolve returns None on miss. {brand, operator}-shaped refs and ref-less calls return None — publisher-curated platforms expect explicit ids. Adopters who need a synth tenant for list_creative_formats / provide_performance_feedback / preview_creative, or natural-key resolution, wrap resolve.

Example::

from adcp.decisioning import Account, create_roster_account_store

store = create_roster_account_store(
    roster={
        "acct_alpha": Account(id="acct_alpha", name="Alpha", status="active"),
        "acct_beta":  Account(id="acct_beta",  name="Beta",  status="active"),
    },
)

Functions

def create_roster_account_store(*, roster: Mapping[str, Account[TMeta]]) ‑> adcp.decisioning.roster_store._RosterAccountStore[~TMeta]
Expand source code
def create_roster_account_store(
    *,
    roster: Mapping[str, Account[TMeta]],
) -> _RosterAccountStore[TMeta]:
    """Build an :class:`AccountStore` backed by a fixed publisher-
    curated roster.

    The returned object conforms to the :class:`AccountStore` Protocol
    plus the optional :class:`AccountStoreList`,
    :class:`AccountStoreUpsert`, and :class:`AccountStoreSyncGovernance`
    Protocols. ``upsert`` and ``sync_governance`` fail closed with
    ``PERMISSION_DENIED`` per entry — adopters who need to support
    write paths use a custom :class:`AccountStore` implementation
    instead.

    :param roster: Mapping from ``account_id`` → :class:`Account`. Each
        value's ``id`` MUST match its key; mismatch raises
        :class:`ValueError` at construction. The mapping is copied into
        an internal immutable view, so subsequent mutation of the
        caller's dict does not affect the store.

    :returns: An :class:`AccountStore` whose:

        * :meth:`resolve` returns the roster entry for an
          ``account_id``-arm ref, ``None`` otherwise.
        * :meth:`list` returns every roster entry.
        * :meth:`upsert` rejects every input entry with
          ``PERMISSION_DENIED``.
        * :meth:`sync_governance` rejects every input entry with
          ``PERMISSION_DENIED``.

    :raises ValueError: When any roster value's ``id`` does not match
        its dict key.
    """
    return _RosterAccountStore(roster)

Build an :class:AccountStore backed by a fixed publisher- curated roster.

The returned object conforms to the :class:AccountStore Protocol plus the optional :class:AccountStoreList, :class:AccountStoreUpsert, and :class:AccountStoreSyncGovernance Protocols. upsert and sync_governance fail closed with PERMISSION_DENIED per entry — adopters who need to support write paths use a custom :class:AccountStore implementation instead.

:param roster: Mapping from account_id → :class:Account. Each value's id MUST match its key; mismatch raises :class:ValueError at construction. The mapping is copied into an internal immutable view, so subsequent mutation of the caller's dict does not affect the store.

:returns: An :class:AccountStore whose:

* :meth:<code>resolve</code> returns the roster entry for an
  <code>account\_id</code>-arm ref, <code>None</code> otherwise.
* :meth:<code>list</code> returns every roster entry.
* :meth:<code>upsert</code> rejects every input entry with
  <code>PERMISSION\_DENIED</code>.
* :meth:<code>sync\_governance</code> rejects every input entry with
  <code>PERMISSION\_DENIED</code>.

:raises ValueError: When any roster value's id does not match its dict key.