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:AccountStoreimpl). 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:
ExplicitAccountswith 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 membership is not authorization. A caller-supplied
authorizecallback binds verified auth to each account. It is a required factory argument so missing policy fails visibly at boot. - Immutable post-construction. The input dict is copied into an
internal :class:
MappingProxyTypeso 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:
upsertand :meth:sync_governancereturnPERMISSION_DENIEDfor 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 returnNone— publisher-curated platforms expect explicit ids. Adopters who need a synth tenant forlist_creative_formats/provide_performance_feedback/preview_creative, or natural-key resolution, wrapresolve.
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",
metadata={"principals": {"https://buyer-alpha.example/"}},
),
"acct_beta": Account(
id="acct_beta", name="Beta", status="active",
metadata={"principals": {"https://buyer-beta.example/"}},
),
},
authorize=lambda account, auth: auth.principal in account.metadata["principals"],
)
Functions
def create_roster_account_store(*,
roster: Mapping[str, Account[TMeta]],
authorize: Callable[[Account[TMeta], AuthInfo], bool | Awaitable[bool]]) ‑> _RosterAccountStore[TMeta]-
Expand source code
def create_roster_account_store( *, roster: Mapping[str, Account[TMeta]], authorize: Callable[[Account[TMeta], AuthInfo], bool | Awaitable[bool]], ) -> _RosterAccountStore[TMeta]: """Build an :class:`AccountStore` backed by a fixed publisher- curated roster. ``authorize`` is the required security boundary binding a verified principal to an account. It may be synchronous or asynchronous. The callback is required so a roster cannot be mistaken for an authorization policy. Possession of an account id never grants access. 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. :param authorize: Principal/account authorization callback. Receives the candidate account and verified auth info. Return exactly ``True`` to grant access. May be async. ``False`` or an exception denies access. :returns: An :class:`AccountStore` whose: * :meth:`resolve` returns an authorized roster entry for an ``account_id``-arm ref, ``None`` otherwise. * :meth:`list` returns only authorized roster entries. * :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, authorize)Build an :class:
AccountStorebacked by a fixed publisher- curated roster.authorizeis the required security boundary binding a verified principal to an account. It may be synchronous or asynchronous. The callback is required so a roster cannot be mistaken for an authorization policy. Possession of an account id never grants access.The returned object conforms to the :class:
AccountStoreProtocol plus the optional :class:AccountStoreList, :class:AccountStoreUpsert, and :class:AccountStoreSyncGovernanceProtocols.upsertandsync_governancefail closed withPERMISSION_DENIEDper entry — adopters who need to support write paths use a custom :class:AccountStoreimplementation instead.:param roster: Mapping from
account_id→ :class:Account. Each value'sidMUST match its key; mismatch raises :class:ValueErrorat construction. The mapping is copied into an internal immutable view, so subsequent mutation of the caller's dict does not affect the store. :param authorize: Principal/account authorization callback. Receives the candidate account and verified auth info. Return exactlyTrueto grant access. May be async.Falseor an exception denies access.:returns: An :class:
AccountStorewhose:* :meth:<code>resolve</code> returns an authorized roster entry for an <code>account\_id</code>-arm ref, <code>None</code> otherwise. * :meth:<code>list</code> returns only authorized roster entries. * :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
iddoes not match its dict key.