Module adcp.principal
Buyer-side orchestration for the AdCP 3.2 principal layer.
The generated request and response models describe one wire exchange. This module implements the stateful client workflow adopters otherwise have to repeat: read the current version, submit a guarded section replacement, and poll asynchronous destination proof until it reaches an actionable state.
Functions
async def sync_principal_configuration(client: PrincipalClient,
configuration: PrincipalConfiguration | Mapping[str, Any],
**kwargs: Any) ‑> PrincipalSyncOutcome-
Expand source code
async def sync_principal_configuration( client: PrincipalClient, configuration: PrincipalConfiguration | Mapping[str, Any], **kwargs: Any, ) -> PrincipalSyncOutcome: """Functional wrapper for :meth:`PrincipalManager.sync`.""" return await PrincipalManager(client).sync(configuration, **kwargs)Functional wrapper for :meth:
PrincipalManager.sync().
Classes
class DestinationSetupSnapshot (states: dict[str, AgentReportingDestinationState])-
Expand source code
@dataclass(frozen=True) class DestinationSetupSnapshot: """Authoritative setup states from one principal readback.""" states: dict[str, AgentReportingDestinationState] @property def validating(self) -> tuple[str, ...]: return self._with_state("validating") @property def ready(self) -> tuple[str, ...]: return self._with_state("ready") @property def action_required(self) -> tuple[str, ...]: return self._with_state("action_required") @property def inactive(self) -> tuple[str, ...]: return self._with_state("inactive") @property def rejected(self) -> tuple[str, ...]: return self._with_state("rejected") @property def settled(self) -> bool: """Whether no destination remains in provider-side validation.""" return not self.validating def _with_state(self, expected: str) -> tuple[str, ...]: return tuple( destination_id for destination_id, state in self.states.items() if _enum(state.state) == expected )Authoritative setup states from one principal readback.
Instance variables
prop action_required : tuple[str, ...]-
Expand source code
@property def action_required(self) -> tuple[str, ...]: return self._with_state("action_required") prop inactive : tuple[str, ...]-
Expand source code
@property def inactive(self) -> tuple[str, ...]: return self._with_state("inactive") prop ready : tuple[str, ...]-
Expand source code
@property def ready(self) -> tuple[str, ...]: return self._with_state("ready") prop rejected : tuple[str, ...]-
Expand source code
@property def rejected(self) -> tuple[str, ...]: return self._with_state("rejected") prop settled : bool-
Expand source code
@property def settled(self) -> bool: """Whether no destination remains in provider-side validation.""" return not self.validatingWhether no destination remains in provider-side validation.
var states : dict[str, AgentReportingDestinationState]prop validating : tuple[str, ...]-
Expand source code
@property def validating(self) -> tuple[str, ...]: return self._with_state("validating")
class PrincipalClient (*args, **kwargs)-
Expand source code
class PrincipalClient(Protocol): """Minimal client surface required by :class:`PrincipalManager`.""" async def get_principal(self, request: GetPrincipalRequest) -> TaskResult[GetPrincipalResponse]: raise NotImplementedError async def sync_principal( self, request: SyncPrincipalRequest ) -> TaskResult[SyncPrincipalResponse]: raise NotImplementedErrorMinimal client surface required by :class:
PrincipalManager.Ancestors
- typing.Protocol
- typing.Generic
Methods
async def get_principal(self, request: GetPrincipalRequest) ‑> TaskResult[GetPrincipalResponse]-
Expand source code
async def get_principal(self, request: GetPrincipalRequest) -> TaskResult[GetPrincipalResponse]: raise NotImplementedError async def sync_principal(self, request: SyncPrincipalRequest) ‑> TaskResult[SyncPrincipalResponse]-
Expand source code
async def sync_principal( self, request: SyncPrincipalRequest ) -> TaskResult[SyncPrincipalResponse]: raise NotImplementedError
class PrincipalConfigurationError (code: str, message: str)-
Expand source code
class PrincipalConfigurationError(RuntimeError): """A principal read, mutation, or destination setup operation failed.""" def __init__(self, code: str, message: str) -> None: super().__init__(message) self.code = codeA principal read, mutation, or destination setup operation failed.
Ancestors
- builtins.RuntimeError
- builtins.Exception
- builtins.BaseException
class PrincipalManager (client: PrincipalClient)-
Expand source code
class PrincipalManager: """Coordinate version-fenced principal configuration for one seller.""" def __init__(self, client: PrincipalClient) -> None: self._client = client async def read(self) -> GetPrincipalResponse: """Read principal state and normalize transport/payload failures.""" task = await self._client.get_principal(GetPrincipalRequest()) if not task.success or task.data is None: raise PrincipalConfigurationError( "PRINCIPAL_READ_FAILED", task.error or "get_principal failed" ) response = task.data if response.result.kind == "failed": raise PrincipalConfigurationError( "PRINCIPAL_READ_FAILED", _failure_message(response.result, "get_principal returned a failed result"), ) return response async def sync( self, configuration: PrincipalConfiguration | Mapping[str, Any], *, idempotency_key: str | None = None, use_version_fence: bool = True, expected_configuration_version: str | None = None, expected_principal_kind: object | None = None, dry_run: bool = False, wait_for_setup: bool = False, setup_timeout: float = 300.0, poll_interval: float = 1.0, ) -> PrincipalSyncOutcome: """Replace selected sections, optionally waiting for destination proof. With ``use_version_fence`` (the default), this first calls ``get_principal``. A current configuration version is copied to ``expected_configuration_version`` and the seller-resolved principal kind is asserted. Callers targeting a seller that explicitly declares ``optimistic_concurrency: false`` should disable the fence. """ desired = PrincipalConfiguration.model_validate(configuration) current: GetPrincipalResponse | None = None if use_version_fence: current = await self.read() current_result = current.result if expected_configuration_version is None and current_result.kind == "current": expected_configuration_version = current_result.configuration_version if expected_principal_kind is None: if current_result.kind == "current": expected_principal_kind = current_result.principal_kind elif current_result.kind == "recognized": expected_principal_kind = current_result.principal_kind payload: dict[str, Any] = { "idempotency_key": idempotency_key or str(uuid4()), "configuration": desired, "dry_run": dry_run, } if expected_configuration_version is not None: payload["expected_configuration_version"] = expected_configuration_version if expected_principal_kind is not None: payload["expected_principal_kind"] = expected_principal_kind task = await self._client.sync_principal(SyncPrincipalRequest.model_validate(payload)) if not task.success or task.data is None: raise PrincipalConfigurationError( "PRINCIPAL_SYNC_FAILED", task.error or "sync_principal failed" ) response = task.data result = response.result if result.kind == "failed": raise PrincipalConfigurationError( "PRINCIPAL_SYNC_FAILED", _failure_message(result, "sync_principal returned a failed result"), ) if result.kind == "validated": return PrincipalSyncOutcome( response, None, None, None, DestinationSetupSnapshot({}), None ) destination_ids = { destination.destination_id for destination in desired.reporting_destinations or [] } configuration_state = result.configuration destination_states = _destination_snapshot(configuration_state, destination_ids) if wait_for_setup and destination_states.validating: expected_destination_refs = { destination_id: state.destination_ref for destination_id, state in destination_states.states.items() } readback, destination_states = await self.wait_for_destinations( destination_ids, timeout=setup_timeout, poll_interval=poll_interval, expected_destination_refs=expected_destination_refs, ) read_result = readback.result if read_result.kind != "current": raise PrincipalConfigurationError( "PRINCIPAL_STATE_LOST", "get_principal stopped returning current state during setup polling", ) configuration_state = read_result.configuration principal_id = read_result.principal_id configuration_version = read_result.configuration_version else: principal_id = result.principal_id configuration_version = result.configuration_version return PrincipalSyncOutcome( response, principal_id, configuration_version, configuration_state, destination_states, configuration_state.declarations, ) async def wait_for_destinations( self, destination_ids: set[str], *, timeout: float = 300.0, poll_interval: float = 1.0, expected_destination_refs: Mapping[str, str] | None = None, ) -> tuple[GetPrincipalResponse, DestinationSetupSnapshot]: """Poll while requested destinations are ``validating``. ``ready``, ``action_required``, ``inactive``, and ``rejected`` are returned to the caller as actionable settled states. The helper never follows or executes an untrusted setup URL. """ if timeout <= 0 or poll_interval <= 0: raise ValueError("timeout and poll_interval must be positive") if not destination_ids: raise ValueError("destination_ids must not be empty") deadline = time.monotonic() + timeout while True: response = await self.read() result = response.result if result.kind != "current": raise PrincipalConfigurationError( "PRINCIPAL_NOT_CONFIGURED", "get_principal did not return current state during setup polling", ) snapshot = _destination_snapshot(result.configuration, destination_ids) missing = destination_ids.difference(snapshot.states) if missing: raise PrincipalConfigurationError( "DESTINATION_STATE_MISSING", "get_principal omitted requested destination(s): " + ", ".join(sorted(missing)), ) if expected_destination_refs: replaced = [ destination_id for destination_id, destination_ref in expected_destination_refs.items() if snapshot.states[destination_id].destination_ref != destination_ref ] if replaced: raise PrincipalConfigurationError( "DESTINATION_GENERATION_CHANGED", "get_principal replaced requested destination generation(s): " + ", ".join(sorted(replaced)), ) if snapshot.settled: return response, snapshot remaining = deadline - time.monotonic() if remaining <= 0: pending = ", ".join(snapshot.validating) raise TimeoutError(f"reporting destination setup did not settle in time: {pending}") await asyncio.sleep(min(poll_interval, remaining))Coordinate version-fenced principal configuration for one seller.
Methods
async def read(self) ‑> GetPrincipalResponse-
Expand source code
async def read(self) -> GetPrincipalResponse: """Read principal state and normalize transport/payload failures.""" task = await self._client.get_principal(GetPrincipalRequest()) if not task.success or task.data is None: raise PrincipalConfigurationError( "PRINCIPAL_READ_FAILED", task.error or "get_principal failed" ) response = task.data if response.result.kind == "failed": raise PrincipalConfigurationError( "PRINCIPAL_READ_FAILED", _failure_message(response.result, "get_principal returned a failed result"), ) return responseRead principal state and normalize transport/payload failures.
async def sync(self,
configuration: PrincipalConfiguration | Mapping[str, Any],
*,
idempotency_key: str | None = None,
use_version_fence: bool = True,
expected_configuration_version: str | None = None,
expected_principal_kind: object | None = None,
dry_run: bool = False,
wait_for_setup: bool = False,
setup_timeout: float = 300.0,
poll_interval: float = 1.0) ‑> PrincipalSyncOutcome-
Expand source code
async def sync( self, configuration: PrincipalConfiguration | Mapping[str, Any], *, idempotency_key: str | None = None, use_version_fence: bool = True, expected_configuration_version: str | None = None, expected_principal_kind: object | None = None, dry_run: bool = False, wait_for_setup: bool = False, setup_timeout: float = 300.0, poll_interval: float = 1.0, ) -> PrincipalSyncOutcome: """Replace selected sections, optionally waiting for destination proof. With ``use_version_fence`` (the default), this first calls ``get_principal``. A current configuration version is copied to ``expected_configuration_version`` and the seller-resolved principal kind is asserted. Callers targeting a seller that explicitly declares ``optimistic_concurrency: false`` should disable the fence. """ desired = PrincipalConfiguration.model_validate(configuration) current: GetPrincipalResponse | None = None if use_version_fence: current = await self.read() current_result = current.result if expected_configuration_version is None and current_result.kind == "current": expected_configuration_version = current_result.configuration_version if expected_principal_kind is None: if current_result.kind == "current": expected_principal_kind = current_result.principal_kind elif current_result.kind == "recognized": expected_principal_kind = current_result.principal_kind payload: dict[str, Any] = { "idempotency_key": idempotency_key or str(uuid4()), "configuration": desired, "dry_run": dry_run, } if expected_configuration_version is not None: payload["expected_configuration_version"] = expected_configuration_version if expected_principal_kind is not None: payload["expected_principal_kind"] = expected_principal_kind task = await self._client.sync_principal(SyncPrincipalRequest.model_validate(payload)) if not task.success or task.data is None: raise PrincipalConfigurationError( "PRINCIPAL_SYNC_FAILED", task.error or "sync_principal failed" ) response = task.data result = response.result if result.kind == "failed": raise PrincipalConfigurationError( "PRINCIPAL_SYNC_FAILED", _failure_message(result, "sync_principal returned a failed result"), ) if result.kind == "validated": return PrincipalSyncOutcome( response, None, None, None, DestinationSetupSnapshot({}), None ) destination_ids = { destination.destination_id for destination in desired.reporting_destinations or [] } configuration_state = result.configuration destination_states = _destination_snapshot(configuration_state, destination_ids) if wait_for_setup and destination_states.validating: expected_destination_refs = { destination_id: state.destination_ref for destination_id, state in destination_states.states.items() } readback, destination_states = await self.wait_for_destinations( destination_ids, timeout=setup_timeout, poll_interval=poll_interval, expected_destination_refs=expected_destination_refs, ) read_result = readback.result if read_result.kind != "current": raise PrincipalConfigurationError( "PRINCIPAL_STATE_LOST", "get_principal stopped returning current state during setup polling", ) configuration_state = read_result.configuration principal_id = read_result.principal_id configuration_version = read_result.configuration_version else: principal_id = result.principal_id configuration_version = result.configuration_version return PrincipalSyncOutcome( response, principal_id, configuration_version, configuration_state, destination_states, configuration_state.declarations, )Replace selected sections, optionally waiting for destination proof.
With
use_version_fence(the default), this first callsget_principal. A current configuration version is copied toexpected_configuration_versionand the seller-resolved principal kind is asserted. Callers targeting a seller that explicitly declaresoptimistic_concurrency: falseshould disable the fence. async def wait_for_destinations(self,
destination_ids: set[str],
*,
timeout: float = 300.0,
poll_interval: float = 1.0,
expected_destination_refs: Mapping[str, str] | None = None) ‑> tuple[GetPrincipalResponse, DestinationSetupSnapshot]-
Expand source code
async def wait_for_destinations( self, destination_ids: set[str], *, timeout: float = 300.0, poll_interval: float = 1.0, expected_destination_refs: Mapping[str, str] | None = None, ) -> tuple[GetPrincipalResponse, DestinationSetupSnapshot]: """Poll while requested destinations are ``validating``. ``ready``, ``action_required``, ``inactive``, and ``rejected`` are returned to the caller as actionable settled states. The helper never follows or executes an untrusted setup URL. """ if timeout <= 0 or poll_interval <= 0: raise ValueError("timeout and poll_interval must be positive") if not destination_ids: raise ValueError("destination_ids must not be empty") deadline = time.monotonic() + timeout while True: response = await self.read() result = response.result if result.kind != "current": raise PrincipalConfigurationError( "PRINCIPAL_NOT_CONFIGURED", "get_principal did not return current state during setup polling", ) snapshot = _destination_snapshot(result.configuration, destination_ids) missing = destination_ids.difference(snapshot.states) if missing: raise PrincipalConfigurationError( "DESTINATION_STATE_MISSING", "get_principal omitted requested destination(s): " + ", ".join(sorted(missing)), ) if expected_destination_refs: replaced = [ destination_id for destination_id, destination_ref in expected_destination_refs.items() if snapshot.states[destination_id].destination_ref != destination_ref ] if replaced: raise PrincipalConfigurationError( "DESTINATION_GENERATION_CHANGED", "get_principal replaced requested destination generation(s): " + ", ".join(sorted(replaced)), ) if snapshot.settled: return response, snapshot remaining = deadline - time.monotonic() if remaining <= 0: pending = ", ".join(snapshot.validating) raise TimeoutError(f"reporting destination setup did not settle in time: {pending}") await asyncio.sleep(min(poll_interval, remaining))Poll while requested destinations are
validating.ready,action_required,inactive, andrejectedare returned to the caller as actionable settled states. The helper never follows or executes an untrusted setup URL.
class PrincipalSyncOutcome (response: SyncPrincipalResponse,
principal_id: str | None,
configuration_version: str | None,
configuration: PrincipalState | None,
destinations: DestinationSetupSnapshot,
declarations: PrincipalDeclarationsState | None)-
Expand source code
@dataclass(frozen=True) class PrincipalSyncOutcome: """Applied configuration plus negotiated and setup-state projections.""" response: SyncPrincipalResponse principal_id: str | None configuration_version: str | None configuration: PrincipalState | None destinations: DestinationSetupSnapshot declarations: PrincipalDeclarationsState | None @property def selected_async_adcp_version(self) -> str | None: if self.declarations is None: return None return self.declarations.selected_async_adcp_versionApplied configuration plus negotiated and setup-state projections.
Instance variables
var configuration : PrincipalState | Nonevar configuration_version : str | Nonevar declarations : PrincipalDeclarationsState | Nonevar destinations : DestinationSetupSnapshotvar principal_id : str | Nonevar response : SyncPrincipalResponseprop selected_async_adcp_version : str | None-
Expand source code
@property def selected_async_adcp_version(self) -> str | None: if self.declarations is None: return None return self.declarations.selected_async_adcp_version