|
| 1 | +"""AudiencePlatform Protocol — covers the ``audience-sync`` specialism. |
| 2 | +
|
| 3 | +Used standalone (LiveRamp, Oracle Data Cloud, Salesforce CDP) or |
| 4 | +composed with ``sales-social`` (Snap/Meta/TikTok). The framework owns |
| 5 | +cross-platform threading + idempotency + cross-tenant scoping; the |
| 6 | +adopter answers "given this audience, what happened on my system?" |
| 7 | +
|
| 8 | +The slug mirrors ``schemas/cache/enums/specialism.json``. |
| 9 | +
|
| 10 | +Two methods: |
| 11 | +
|
| 12 | +* :meth:`sync_audiences` — push audiences to the platform (creates, |
| 13 | + updates, deletes per the wire spec) |
| 14 | +* :meth:`poll_audience_statuses` — batch-poll current status for one |
| 15 | + or more audiences |
| 16 | +
|
| 17 | +Mirrors the JS-side ``AudiencePlatform`` interface at |
| 18 | +``src/lib/server/decisioning/specialisms/audiences.ts``. |
| 19 | +""" |
| 20 | + |
| 21 | +from __future__ import annotations |
| 22 | + |
| 23 | +from collections.abc import Mapping, Sequence |
| 24 | +from typing import TYPE_CHECKING, Any, Generic, Protocol, runtime_checkable |
| 25 | + |
| 26 | +from typing_extensions import TypeVar |
| 27 | + |
| 28 | +if TYPE_CHECKING: |
| 29 | + from adcp.decisioning.context import RequestContext |
| 30 | + from adcp.decisioning.types import MaybeAsync |
| 31 | + from adcp.types import ( |
| 32 | + SyncAudiencesAudience, |
| 33 | + SyncAudiencesSuccessResponse, |
| 34 | + ) |
| 35 | + |
| 36 | +#: Per-platform metadata generic; matches ``RequestContext[TMeta]`` and |
| 37 | +#: ``Account[TMeta]`` upstream. |
| 38 | +TMeta = TypeVar("TMeta", default=dict[str, Any]) |
| 39 | + |
| 40 | +# Note on adopter-facing row types: the wire schema doesn't export a |
| 41 | +# top-level ``Audience`` type — the row shape is defined inline on |
| 42 | +# ``SyncAudiencesRequest.audiences[]``. Adopters import |
| 43 | +# :class:`adcp.types.SyncAudiencesAudience` directly for typing. |
| 44 | +# The wire success response is :class:`adcp.types.SyncAudiencesSuccessResponse`, |
| 45 | +# which wraps per-audience result rows in ``{audiences: [...]}`` with |
| 46 | +# the spec's status enum (``created`` / ``updated`` / ``unchanged`` / |
| 47 | +# ``deleted`` / ``failed``; note ``rejected`` is NOT a valid wire |
| 48 | +# status — use ``failed`` for buyer-rejected audiences). |
| 49 | + |
| 50 | + |
| 51 | +@runtime_checkable |
| 52 | +class AudiencePlatform(Protocol, Generic[TMeta]): |
| 53 | + """Sync first-party CRM audiences with delta upsert semantics. |
| 54 | +
|
| 55 | + Methods may be sync (return ``T`` directly) or async (return |
| 56 | + ``Awaitable[T]``); the dispatch adapter detects via |
| 57 | + :func:`asyncio.iscoroutinefunction` and runs sync methods on a |
| 58 | + thread pool. |
| 59 | +
|
| 60 | + Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable |
| 61 | + rejection (``AUDIENCE_TOO_SMALL``, ``REFERENCE_NOT_FOUND``, etc.); |
| 62 | + the framework projects to the wire structured-error envelope. |
| 63 | + """ |
| 64 | + |
| 65 | + def sync_audiences( |
| 66 | + self, |
| 67 | + audiences: Sequence[SyncAudiencesAudience], |
| 68 | + ctx: RequestContext[TMeta], |
| 69 | + ) -> MaybeAsync[SyncAudiencesSuccessResponse]: |
| 70 | + """Push audiences to the platform. |
| 71 | +
|
| 72 | + Framework handles batching, idempotency, and cross-tenant |
| 73 | + scoping; the adopter handles match-rate computation and |
| 74 | + activation lifecycle. |
| 75 | +
|
| 76 | + Sync acknowledgment with status changes via |
| 77 | + ``ctx.publish_status_change``: return per-audience result rows |
| 78 | + immediately (``'pending'`` / ``'matching'`` are valid sync |
| 79 | + outcomes). The match-rate computation and activation pipeline |
| 80 | + run in the background — call |
| 81 | + ``ctx.publish_status_change(resource_type='audience', ...)`` |
| 82 | + from the platform's webhook handler / job queue / cron when |
| 83 | + each audience reaches a terminal state. |
| 84 | +
|
| 85 | + :param audiences: List of audience rows projected from the |
| 86 | + wire ``SyncAudiencesRequest.audiences[]`` field. Adopter |
| 87 | + ergonomic — receives the list directly rather than the |
| 88 | + full request. |
| 89 | + :raises adcp.decisioning.AdcpError: for buyer-fixable |
| 90 | + rejection (e.g., ``AUDIENCE_TOO_SMALL``). |
| 91 | + """ |
| 92 | + ... |
| 93 | + |
| 94 | + def poll_audience_statuses( |
| 95 | + self, |
| 96 | + audience_ids: Sequence[str], |
| 97 | + ctx: RequestContext[TMeta], |
| 98 | + ) -> MaybeAsync[Mapping[str, str]]: |
| 99 | + """Batch-poll current status for one or more audiences. |
| 100 | +
|
| 101 | + Sync — this is a state-read, not a mutating operation. Useful |
| 102 | + for buyer-side polling outside the framework's task envelope |
| 103 | + (e.g., querying long-lived audiences) and for adapter code |
| 104 | + that needs to check N audiences at once. |
| 105 | +
|
| 106 | + Returns a ``dict[audience_id, AudienceStatus]``. Audiences not |
| 107 | + found are omitted from the map (callers handle missing keys); |
| 108 | + raise ``AdcpError(code='REFERENCE_NOT_FOUND')`` only when the |
| 109 | + entire batch is unresolvable for the tenant. |
| 110 | +
|
| 111 | + Single-audience polling is |
| 112 | + ``poll_audience_statuses([id], ctx).get(id)``. The batch shape |
| 113 | + composes with upstream identity-graph APIs that natively |
| 114 | + return per-audience-id arrays — adopters do NOT need to wrap |
| 115 | + a single-id lookup over an N-call loop. |
| 116 | +
|
| 117 | + Adopter-internal helper — not surfaced as a wire tool. Used |
| 118 | + by adopter code orchestrating cross-platform audience flows |
| 119 | + and by the framework's optional bulk-status middleware. |
| 120 | + """ |
| 121 | + ... |
| 122 | + |
| 123 | + |
| 124 | +__all__ = ["AudiencePlatform"] |
0 commit comments