Module adcp.decisioning.property_list
Property-list resolver and product intersection helper for get_products.
Capability-gated framework support for the property_list buyer-side filter
on get_products.
When an adopter declares
Features(property_list_filtering=True) on their
:class:~adcp.decisioning.platform.DecisioningCapabilities and supplies a
:class:PropertyListFetcher, the framework automatically:
- Fetches the buyer's authorized property IDs from
request.property_list.agent_url. - Applies :func:
filter_products_by_property_list()to the platform's response. - Sets
response.property_list_applied = Trueon the returned envelope.
Adopters who want to apply the filter themselves (e.g., pushed down into a DB
query) set Features(property_list_filtering=False) and call these helpers
directly inside their get_products implementation.
Reference pattern: :mod:adcp.decisioning.webhook_emit (capability-gated
post-adapter side effect).
Functions
def filter_products_by_property_list(products: list[Any], allowed_property_ids: set[str]) ‑> list[typing.Any]-
Expand source code
def filter_products_by_property_list( products: list[Any], allowed_property_ids: set[str], ) -> list[Any]: """Filter a product list to those matching the buyer's authorized property IDs. Respects ``publisher_properties.selection_type``: * ``'all'`` — product covers all publisher properties; always included. * ``'by_id'`` — intersect the product's ``property_ids`` with ``allowed_property_ids``. * ``'by_tag'`` — property tags cannot be matched against a property ID list; this entry does not contribute to inclusion. Respects ``product.property_targeting_allowed``: * ``False`` (default, "all or nothing") — the product's full set of ``by_id`` property IDs must be a subset of ``allowed_property_ids``. * ``True`` (permissive) — any non-empty intersection is sufficient. A product is included if ANY of its ``publisher_properties`` entries passes the filter. This models the semantics of a product covering inventory from multiple publishers: if ANY publisher's inventory is in the buyer's allowed set, the product is relevant. :param products: Products from the platform's ``get_products`` response. :param allowed_property_ids: Set of property_id strings the buyer is authorized to spend on (result of :func:`resolve_property_list`). :returns: Filtered product list; original order preserved. """ return [p for p in products if _product_matches(p, allowed_property_ids)]Filter a product list to those matching the buyer's authorized property IDs.
Respects
publisher_properties.selection_type:'all'— product covers all publisher properties; always included.'by_id'— intersect the product'sproperty_idswithallowed_property_ids.'by_tag'— property tags cannot be matched against a property ID list; this entry does not contribute to inclusion.
Respects
product.property_targeting_allowed:False(default, "all or nothing") — the product's full set ofby_idproperty IDs must be a subset ofallowed_property_ids.True(permissive) — any non-empty intersection is sufficient.
A product is included if ANY of its
publisher_propertiesentries passes the filter. This models the semantics of a product covering inventory from multiple publishers: if ANY publisher's inventory is in the buyer's allowed set, the product is relevant.:param products: Products from the platform's
get_productsresponse. :param allowed_property_ids: Set of property_id strings the buyer is authorized to spend on (result of :func:resolve_property_list()). :returns: Filtered product list; original order preserved. async def maybe_apply_property_list_filter(*,
params: Any,
response: Any,
fetcher: PropertyListFetcher | None,
capability_enabled: bool) ‑> Any-
Expand source code
async def maybe_apply_property_list_filter( *, params: Any, response: Any, fetcher: PropertyListFetcher | None, capability_enabled: bool, ) -> Any: """Post-adapter gate: apply property-list filtering to a get_products response. Called by the ``get_products`` handler shim after the platform method returns. The gate is a no-op when either: * ``capability_enabled`` is False (adopter hasn't declared the feature). * ``params.property_list`` is absent (buyer didn't send a list reference). When ``fetcher`` is None but the gate would otherwise fire, a WARNING is emitted and the response is returned unmodified (defense-in-depth; :func:`validate_property_list_config` should have caught this at boot). Uses :meth:`model_copy` to avoid mutating the platform's return value. :raises AdcpError: ``recovery='transient'`` propagated from :func:`resolve_property_list` on fetch failure. """ if not capability_enabled: return response property_list_ref = getattr(params, "property_list", None) if property_list_ref is None: return response if fetcher is None: logger.warning( "[adcp.property_list] property_list_filtering capability is declared " "and the request carries a property_list reference, but no " "PropertyListFetcher was wired — filter skipped. Pass " "property_list_fetcher= to " "adcp.decisioning.serve.create_adcp_server_from_platform." ) return response allowed = await resolve_property_list(property_list_ref, fetcher=fetcher) products: list[Any] = list(getattr(response, "products", None) or []) filtered = filter_products_by_property_list(products, allowed) return response.model_copy( update={"products": filtered, "property_list_applied": True} )Post-adapter gate: apply property-list filtering to a get_products response.
Called by the
get_productshandler shim after the platform method returns. The gate is a no-op when either:capability_enabledis False (adopter hasn't declared the feature).params.property_listis absent (buyer didn't send a list reference).
When
fetcheris None but the gate would otherwise fire, a WARNING is emitted and the response is returned unmodified (defense-in-depth; :func:validate_property_list_config()should have caught this at boot).Uses :meth:
model_copyto avoid mutating the platform's return value.:raises AdcpError:
recovery='transient'propagated from :func:resolve_property_list()on fetch failure. def property_list_capability_enabled(platform: Any) ‑> bool-
Expand source code
def property_list_capability_enabled(platform: Any) -> bool: """Return True if ``platform.capabilities.media_buy.features.property_list_filtering`` is set. Centralises the three-level ``getattr`` chain used in both ``handler.py`` and ``serve.py`` so they can't drift apart. """ media_buy = getattr(getattr(platform, "capabilities", None), "media_buy", None) features = getattr(media_buy, "features", None) return bool(getattr(features, "property_list_filtering", False))Return True if
platform.capabilities.media_buy.features.property_list_filteringis set.Centralises the three-level
getattrchain used in bothhandler.pyandserve.pyso they can't drift apart. async def resolve_property_list(ref: Any,
*,
fetcher: PropertyListFetcher) ‑> set[str]-
Expand source code
async def resolve_property_list( ref: Any, *, fetcher: PropertyListFetcher, ) -> set[str]: """Fetch the buyer's authorized property IDs from the agent at ``ref.agent_url``. :param ref: ``PropertyList`` wire object (has ``agent_url``, ``list_id``, ``auth_token``). :param fetcher: Adopter-supplied :class:`PropertyListFetcher`. :returns: Set of allowed property_id strings. :raises AdcpError: ``recovery='transient'`` on any fetch failure. ``auth_token`` is never included in the error details. """ from adcp.decisioning.types import AdcpError list_id: str = ref.list_id agent_url: str = str(ref.agent_url) auth_token: str | None = getattr(ref, "auth_token", None) try: ids = await fetcher.fetch(agent_url, list_id, auth_token=auth_token) return set(ids) except Exception as exc: # Log the raw exception server-side; never include it in the wire # error message — the exception repr may carry auth_token or other # credential-shaped values from the upstream HTTP response. logger.warning( "[adcp.property_list] fetch failed for list_id=%r agent_url=%r: %s", list_id, agent_url, exc, ) raise AdcpError( "SERVICE_UNAVAILABLE", message=( f"Property list fetch failed for list_id={list_id!r} " f"from agent_url={agent_url!r}" ), recovery="transient", details={"list_id": list_id, "agent_url": agent_url}, ) from excFetch the buyer's authorized property IDs from the agent at
ref.agent_url.:param ref:
PropertyListwire object (hasagent_url,list_id,auth_token). :param fetcher: Adopter-supplied :class:PropertyListFetcher. :returns: Set of allowed property_id strings. :raises AdcpError:recovery='transient'on any fetch failure.auth_tokenis never included in the error details. def validate_property_list_config(*,
capability_enabled: bool,
fetcher: PropertyListFetcher | None) ‑> None-
Expand source code
def validate_property_list_config( *, capability_enabled: bool, fetcher: PropertyListFetcher | None, ) -> None: """Boot-time fail-fast: raise when property_list_filtering=True but no fetcher. Mirrors :func:`~adcp.decisioning.webhook_emit.validate_webhook_sender_for_platform`: a declared capability without the required runtime dependency would silently skip filtering at request time — a buyer who sends ``property_list`` would receive unfiltered products with ``property_list_applied`` absent or False, mismatching what the seller's ``get_adcp_capabilities`` advertised. :raises AdcpError: ``recovery='terminal'`` when misconfigured. """ if not capability_enabled: return if fetcher is not None: return from adcp.decisioning.types import AdcpError raise AdcpError( "INVALID_REQUEST", message=( "Features.property_list_filtering=True is declared in capabilities " "but no PropertyListFetcher was wired. Buyers who send " "property_list on get_products requests would have their list " "filter silently skipped. Pass property_list_fetcher= to " "adcp.decisioning.serve.create_adcp_server_from_platform, " "or set Features(property_list_filtering=False) to opt out." ), recovery="terminal", details={"missing": "property_list_fetcher"}, )Boot-time fail-fast: raise when property_list_filtering=True but no fetcher.
Mirrors :func:
~adcp.decisioning.webhook_emit.validate_webhook_sender_for_platform: a declared capability without the required runtime dependency would silently skip filtering at request time — a buyer who sendsproperty_listwould receive unfiltered products withproperty_list_appliedabsent or False, mismatching what the seller'sget_adcp_capabilitiesadvertised.:raises AdcpError:
recovery='terminal'when misconfigured.
Classes
class PropertyListFetcher (*args, **kwargs)-
Expand source code
@runtime_checkable class PropertyListFetcher(Protocol): """Adopter-supplied protocol for fetching a buyer's authorized property IDs. The framework calls :meth:`fetch` when ``property_list_filtering`` is declared in capabilities and the request carries a ``PropertyList`` reference. Adopters plug in their own HTTP client — the framework ships no hidden HTTP dependency. Typical implementation:: class MyFetcher: def __init__(self, client: httpx.AsyncClient) -> None: self._client = client async def fetch( self, agent_url: str, list_id: str, *, auth_token: str | None = None, ) -> list[str]: headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {} resp = await self._client.get( f"{agent_url}/property-lists/{list_id}", headers=headers, ) resp.raise_for_status() return resp.json()["property_ids"] Wire the fetcher via:: create_adcp_server_from_platform(platform, property_list_fetcher=MyFetcher(client)) """ async def fetch( self, agent_url: str, list_id: str, *, auth_token: str | None = None, ) -> list[str]: """Fetch and return the list of allowed property_id strings. :param agent_url: Buyer agent URL from the wire ``PropertyList`` reference. :param list_id: Property list identifier. :param auth_token: Optional JWT/bearer token. Never log this value. :returns: List of allowed property_id strings (``^[a-z0-9_]+$`` format). :raises Exception: Any exception; the framework wraps it in :class:`~adcp.decisioning.types.AdcpError` with ``recovery='transient'``. """ ...Adopter-supplied protocol for fetching a buyer's authorized property IDs.
The framework calls :meth:
fetchwhenproperty_list_filteringis declared in capabilities and the request carries aPropertyListreference. Adopters plug in their own HTTP client — the framework ships no hidden HTTP dependency.Typical implementation::
class MyFetcher: def __init__(self, client: httpx.AsyncClient) -> None: self._client = client async def fetch( self, agent_url: str, list_id: str, *, auth_token: str | None = None, ) -> list[str]: headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {} resp = await self._client.get( f"{agent_url}/property-lists/{list_id}", headers=headers, ) resp.raise_for_status() return resp.json()["property_ids"]Wire the fetcher via::
create_adcp_server_from_platform(platform, property_list_fetcher=MyFetcher(client))Ancestors
- typing.Protocol
- typing.Generic
Methods
async def fetch(self, agent_url: str, list_id: str, *, auth_token: str | None = None) ‑> list[str]-
Expand source code
async def fetch( self, agent_url: str, list_id: str, *, auth_token: str | None = None, ) -> list[str]: """Fetch and return the list of allowed property_id strings. :param agent_url: Buyer agent URL from the wire ``PropertyList`` reference. :param list_id: Property list identifier. :param auth_token: Optional JWT/bearer token. Never log this value. :returns: List of allowed property_id strings (``^[a-z0-9_]+$`` format). :raises Exception: Any exception; the framework wraps it in :class:`~adcp.decisioning.types.AdcpError` with ``recovery='transient'``. """ ...Fetch and return the list of allowed property_id strings.
:param agent_url: Buyer agent URL from the wire
PropertyListreference. :param list_id: Property list identifier. :param auth_token: Optional JWT/bearer token. Never log this value. :returns: List of allowed property_id strings (^[a-z0-9_]+$format). :raises Exception: Any exception; the framework wraps it in :class:~adcp.decisioning.types.AdcpErrorwithrecovery='transient'.