Module adcp.reporting.submissions.models
Immutable buyer intent records and closed diagnostics.
These records describe submission, not evidence selection or reconciliation. The caller must validate its receipt plan against the complete seller history. Received adjustment evidence must be retained separately, before model parsing.
Functions
def confirm_submission(submission: ReportingReceiptSubmission,
ordinal: int,
response: SyncReportingReceiptsResponse | bytes) ‑> ReportingReceiptSubmission-
Expand source code
def confirm_submission( submission: ReportingReceiptSubmission, ordinal: int, response: SyncReportingReceiptsResponse | bytes, ) -> ReportingReceiptSubmission: if ( type(ordinal) is not int or not 0 <= ordinal < submission.chunk_count or ordinal > submission.confirmed_chunks ): raise ReportingSubmissionError(ReportingSubmissionCode.HISTORY_CORRUPT) confirmed = _confirmation(submission, ordinal, response) if ordinal < submission.confirmed_chunks: # First confirmation is immutable. Concurrent exact replays may use # recorded/unchanged differently, but must agree on the actual evidence # and failures. A contradictory reply cannot overwrite retained results. previous = json.loads(submission._confirmed[ordinal]) repeated = json.loads(confirmed) for results in (previous, repeated): for item in results: if item["result"] in {"recorded", "unchanged"}: item["result"] = "recorded" if previous != repeated: raise ReportingSubmissionError(ReportingSubmissionCode.INVALID_RESPONSE) return submission chunks = (*submission._confirmed, confirmed) if _confirmation_bytes(chunks) > MAX_CONFIRMATION_BYTES: raise ReportingSubmissionError(ReportingSubmissionCode.INVALID_RESPONSE) return replace(submission, _confirmed=chunks) def decode_submission(scope: ReportingSubmissionScope,
row: Sequence[Any]) ‑> ReportingReceiptSubmission-
Expand source code
def decode_submission( scope: ReportingSubmissionScope, row: Sequence[Any] ) -> ReportingReceiptSubmission: result = None try: identifier, plan, plan_digest, confirmed, confirmed_digest, pending = row if ( len(plan.encode()) > MAX_SUBMISSION_BYTES or len(confirmed.encode()) > MAX_CONFIRMATION_BYTES ): raise ValueError if ( hashlib.sha256(plan.encode()).hexdigest() != plan_digest or hashlib.sha256(confirmed.encode()).hexdigest() != confirmed_digest ): raise ValueError candidate = ReportingReceiptSubmission( scope, identifier, plan.encode(), _decode_chunks(confirmed), ) validate_submission(candidate) if ( type(pending) is not bool or pending != candidate.pending or encode_submission(candidate) != (plan, plan_digest, confirmed, confirmed_digest) ): raise ValueError result = candidate except Exception: result = None if result is None: raise ReportingSubmissionError(ReportingSubmissionCode.HISTORY_CORRUPT) return result def encode_submission(submission: ReportingReceiptSubmission) ‑> tuple[str, str, str, str]-
Expand source code
def encode_submission(submission: ReportingReceiptSubmission) -> tuple[str, str, str, str]: """Exact text/hashes for PostgreSQL; JSONB is not the identity store.""" confirmed = b"[" + b",".join(submission._confirmed) + b"]" return ( submission._plan.decode(), hashlib.sha256(submission._plan).hexdigest(), confirmed.decode(), hashlib.sha256(confirmed).hexdigest(), )Exact text/hashes for PostgreSQL; JSONB is not the identity store.
def freeze_response(response: SyncReportingReceiptsResponse) ‑> bytes-
Expand source code
def freeze_response(response: SyncReportingReceiptsResponse) -> bytes: """Detach the caller's response once, before any await or storage lock.""" encoded = None try: encoded = canonical_json_utf8_v1(response.model_dump(mode="json", exclude_none=True)) if len(encoded) > MAX_RESPONSE_BYTES: raise ValueError except Exception: encoded = None if encoded is None: raise ReportingSubmissionError(ReportingSubmissionCode.INVALID_RESPONSE) return encodedDetach the caller's response once, before any await or storage lock.
def is_completed_legacy_replay(prior: ReportingReceiptSubmission,
proposed: ReportingReceiptSubmission) ‑> bool-
Expand source code
def is_completed_legacy_replay( prior: ReportingReceiptSubmission, proposed: ReportingReceiptSubmission ) -> bool: """Match a completed rc.6/rc.7 intent to its 3.2 proposal without changing its bytes. Both plans must already have passed full submission validation. Their IDs exclude the request version, so compare the frozen items as well as the ID. Pending old requests are never eligible for a cross-version replay. """ return ( not prior.pending and prior.adcp_version in _LEGACY_VERSIONS and proposed.adcp_version == _VERSION and prior.scope == proposed.scope and prior.submission_id == proposed.submission_id and json.loads(prior._plan)["items"] == json.loads(proposed._plan)["items"] )Match a completed rc.6/rc.7 intent to its 3.2 proposal without changing its bytes.
Both plans must already have passed full submission validation. Their IDs exclude the request version, so compare the frozen items as well as the ID. Pending old requests are never eligible for a cross-version replay.
def prepare_reporting_receipt_submission(scope: ReportingSubmissionScope,
receipts: Sequence[ReportingSubmissionReceipt]) ‑> ReportingReceiptSubmission-
Expand source code
def prepare_reporting_receipt_submission( scope: ReportingSubmissionScope, receipts: Sequence[ReportingSubmissionReceipt], ) -> ReportingReceiptSubmission: """Freeze an already validated receipt plan, with at most 100 items per call. Outbound typed values are normalized once, then retained byte-for-byte. This is not a capture or proof of raw inbound adjustment evidence. Requests contain the authorized resolved account only, with no caller-provided account, idempotency key, context, extensions or credentials. """ return _prepare_for_version(scope, receipts, _VERSION)Freeze an already validated receipt plan, with at most 100 items per call.
Outbound typed values are normalized once, then retained byte-for-byte. This is not a capture or proof of raw inbound adjustment evidence. Requests contain the authorized resolved account only, with no caller-provided account, idempotency key, context, extensions or credentials.
def validate_submission(submission: ReportingReceiptSubmission) ‑> None-
Expand source code
def validate_submission(submission: ReportingReceiptSubmission) -> None: """Check fresh bytes, bounds, identity and ordered confirmed prefix every time. Exact immutable-byte proofs avoid repeating plan and response schemas. No stored digest or mutable model is sufficient to hit either bounded cache. """ valid = False try: requests = _validated_requests(submission) if ( type(submission._confirmed) is not tuple or len(submission._confirmed) > len(requests) or _confirmation_bytes(submission._confirmed) > MAX_CONFIRMATION_BYTES ): raise ValueError for ordinal, chunk in enumerate(submission._confirmed): if type(chunk) is not bytes or len(chunk) > MAX_RESPONSE_BYTES: raise ValueError key = _confirmation_key(submission, requests[ordinal], chunk) if CONFIRMATIONS.get(key) is None: if _restore_confirmation(submission, ordinal, chunk) != chunk: raise ValueError CONFIRMATIONS.put(key, ()) valid = True except Exception: valid = False if not valid: raise ReportingSubmissionError(ReportingSubmissionCode.HISTORY_CORRUPT)Check fresh bytes, bounds, identity and ordered confirmed prefix every time.
Exact immutable-byte proofs avoid repeating plan and response schemas. No stored digest or mutable model is sufficient to hit either bounded cache.
Classes
class ReportingReceiptFailureCode (*args, **kwds)-
Expand source code
class ReportingReceiptFailureCode(str, Enum): """Known item failures; unrecognized seller codes map to UNKNOWN. Seller messages/details are deliberately not persisted or exposed. Each failed item and each error position is retained, including unknown codes. """ UNKNOWN = "UNKNOWN" INVALID_REQUEST = "INVALID_REQUEST" UNAUTHORIZED = "UNAUTHORIZED" RATE_LIMITED = "RATE_LIMITED" NOT_SUPPORTED = "NOT_SUPPORTED" IDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT" INVALID_REPORTING_RECORD = "INVALID_REPORTING_RECORD" REPORTING_RECORD_UNAVAILABLE = "REPORTING_RECORD_UNAVAILABLE" REPORTING_HISTORY_CORRUPT = "REPORTING_HISTORY_CORRUPT" REPORTING_IDENTITY_CONFLICT = "REPORTING_IDENTITY_CONFLICT" REPORTING_TIME_INVALID = "REPORTING_TIME_INVALID" RECEIPTS_NOT_ENABLED = "RECEIPTS_NOT_ENABLED" RECEIVED_AT_READ_ONLY = "RECEIVED_AT_READ_ONLY" RECEIPT_PROFILE_MISMATCH = "RECEIPT_PROFILE_MISMATCH" RECEIPT_TOTALS_MISMATCH = "RECEIPT_TOTALS_MISMATCH" RECEIPT_EVIDENCE_MISMATCH = "RECEIPT_EVIDENCE_MISMATCH" MATERIALIZATION_UNREADABLE = "MATERIALIZATION_UNREADABLE" ADJUSTMENT_REQUIRES_OFFICIAL = "ADJUSTMENT_REQUIRES_OFFICIAL" ADJUSTMENT_ORDER_INVALID = "ADJUSTMENT_ORDER_INVALID" ADJUSTMENT_DIGEST_MISMATCH = "ADJUSTMENT_DIGEST_MISMATCH" ACCEPTED_RECEIPT_TERMINAL = "ACCEPTED_RECEIPT_TERMINAL"Known item failures; unrecognized seller codes map to UNKNOWN.
Seller messages/details are deliberately not persisted or exposed. Each failed item and each error position is retained, including unknown codes.
Ancestors
- builtins.str
- enum.Enum
Class variables
var ACCEPTED_RECEIPT_TERMINALvar ADJUSTMENT_DIGEST_MISMATCHvar ADJUSTMENT_ORDER_INVALIDvar ADJUSTMENT_REQUIRES_OFFICIALvar IDEMPOTENCY_CONFLICTvar INVALID_REPORTING_RECORDvar INVALID_REQUESTvar MATERIALIZATION_UNREADABLEvar NOT_SUPPORTEDvar RATE_LIMITEDvar RECEIPTS_NOT_ENABLEDvar RECEIPT_EVIDENCE_MISMATCHvar RECEIPT_PROFILE_MISMATCHvar RECEIPT_TOTALS_MISMATCHvar RECEIVED_AT_READ_ONLYvar REPORTING_HISTORY_CORRUPTvar REPORTING_IDENTITY_CONFLICTvar REPORTING_RECORD_UNAVAILABLEvar REPORTING_TIME_INVALIDvar UNAUTHORIZEDvar UNKNOWN
class ReportingReceiptOutcome (ordinal: int,
kind: ReceiptKind,
result: ReceiptResult,
error_codes: tuple[ReportingReceiptFailureCode, ...] = ())-
Expand source code
@dataclass(frozen=True) class ReportingReceiptOutcome: """One confirmed item, in original input order, with fresh model views.""" ordinal: int kind: ReceiptKind result: ReceiptResult error_codes: tuple[ReportingReceiptFailureCode, ...] = () _submitted: bytes = field(default=b"", repr=False) _stored: bytes | None = field(default=None, repr=False) @property def submitted_receipt(self) -> ReportingSubmissionReceipt: return _receipt(self.kind, json.loads(self._submitted)) @property def receipt(self) -> ReportingSubmissionReceipt | None: """The seller's actual stored receipt on success, including received_at.""" return _receipt(self.kind, json.loads(self._stored)) if self._stored is not None else NoneOne confirmed item, in original input order, with fresh model views.
Instance variables
var error_codes : tuple[ReportingReceiptFailureCode, ...]var kind : Literal['receipt', 'adjustment_receipt']var ordinal : intprop receipt : ReportingSubmissionReceipt | None-
Expand source code
@property def receipt(self) -> ReportingSubmissionReceipt | None: """The seller's actual stored receipt on success, including received_at.""" return _receipt(self.kind, json.loads(self._stored)) if self._stored is not None else NoneThe seller's actual stored receipt on success, including received_at.
var result : Literal['recorded', 'unchanged', 'failed']prop submitted_receipt : ReportingSubmissionReceipt-
Expand source code
@property def submitted_receipt(self) -> ReportingSubmissionReceipt: return _receipt(self.kind, json.loads(self._submitted))
class ReportingReceiptSubmission (scope: ReportingSubmissionScope,
submission_id: str,
_plan: bytes)-
Expand source code
@dataclass(frozen=True) class ReportingReceiptSubmission: """Exact reserved plan and its immutable, fully confirmed chunk prefix. Construct with :func:`prepare_reporting_receipt_submission`. Stores validate persisted records before returning them. Private bytes keep mutable caller models, driver results, and representations out of the retained identity. """ scope: ReportingSubmissionScope = field(repr=False) submission_id: str = field(repr=False) _plan: bytes = field(repr=False) _confirmed: tuple[bytes, ...] = field(default=(), repr=False) @property def chunk_count(self) -> int: return len(_validated_requests(self)) @property def confirmed_chunks(self) -> int: return len(self._confirmed) @property def pending(self) -> bool: return self.confirmed_chunks < self.chunk_count @property def adcp_version(self) -> str: """The immutable request version, including for completed older plans.""" return str(json.loads(_validated_requests(self)[0])["adcp_version"]) def request(self, ordinal: int) -> SyncReportingReceiptsRequest: """Return a fresh typed view of one exact persisted request body.""" body = json.loads(_validated_requests(self)[ordinal]) return SyncReportingReceiptsRequest.model_validate(body) @property def outcomes(self) -> tuple[ReportingReceiptOutcome, ...]: plan = json.loads(self._plan) confirmed = { item["reporting_receipt_id"]: item for chunk in self._confirmed for item in json.loads(chunk) } outcomes = [] for ordinal, item in enumerate(plan["items"]): result = confirmed.get(item["body"]["reporting_receipt_id"]) if result is not None: stored = result.get("receipt") outcomes.append( ReportingReceiptOutcome( ordinal, item["kind"], result["result"], tuple(ReportingReceiptFailureCode(code) for code in result["errors"]), canonical_json_utf8_v1(item["body"]), canonical_json_utf8_v1(stored) if stored is not None else None, ) ) return tuple(outcomes)Exact reserved plan and its immutable, fully confirmed chunk prefix.
Construct with :func:
prepare_reporting_receipt_submission(). Stores validate persisted records before returning them. Private bytes keep mutable caller models, driver results, and representations out of the retained identity.Instance variables
prop adcp_version : str-
Expand source code
@property def adcp_version(self) -> str: """The immutable request version, including for completed older plans.""" return str(json.loads(_validated_requests(self)[0])["adcp_version"])The immutable request version, including for completed older plans.
prop chunk_count : int-
Expand source code
@property def chunk_count(self) -> int: return len(_validated_requests(self)) prop confirmed_chunks : int-
Expand source code
@property def confirmed_chunks(self) -> int: return len(self._confirmed) prop outcomes : tuple[ReportingReceiptOutcome, ...]-
Expand source code
@property def outcomes(self) -> tuple[ReportingReceiptOutcome, ...]: plan = json.loads(self._plan) confirmed = { item["reporting_receipt_id"]: item for chunk in self._confirmed for item in json.loads(chunk) } outcomes = [] for ordinal, item in enumerate(plan["items"]): result = confirmed.get(item["body"]["reporting_receipt_id"]) if result is not None: stored = result.get("receipt") outcomes.append( ReportingReceiptOutcome( ordinal, item["kind"], result["result"], tuple(ReportingReceiptFailureCode(code) for code in result["errors"]), canonical_json_utf8_v1(item["body"]), canonical_json_utf8_v1(stored) if stored is not None else None, ) ) return tuple(outcomes) prop pending : bool-
Expand source code
@property def pending(self) -> bool: return self.confirmed_chunks < self.chunk_count var scope : ReportingSubmissionScopevar submission_id : str
Methods
def request(self, ordinal: int) ‑> SyncReportingReceiptsRequest-
Expand source code
def request(self, ordinal: int) -> SyncReportingReceiptsRequest: """Return a fresh typed view of one exact persisted request body.""" body = json.loads(_validated_requests(self)[ordinal]) return SyncReportingReceiptsRequest.model_validate(body)Return a fresh typed view of one exact persisted request body.
class ReportingSubmissionCode (*args, **kwds)-
Expand source code
class ReportingSubmissionCode(str, Enum): INVALID_SCOPE = "INVALID_SUBMISSION_SCOPE" INVALID_PLAN = "INVALID_SUBMISSION_PLAN" UNAUTHORIZED = "SUBMISSION_NOT_AUTHORIZED" NOT_FOUND = "SUBMISSION_NOT_FOUND" HISTORY_CORRUPT = "SUBMISSION_HISTORY_CORRUPT" STORAGE_UNAVAILABLE = "SUBMISSION_STORAGE_UNAVAILABLE" PG_REQUIRED = "SUBMISSION_PG_REQUIRED" INVALID_RESPONSE = "SUBMISSION_RESPONSE_INVALID" TRANSPORT_UNCERTAIN = "SUBMISSION_TRANSPORT_UNCERTAIN" RESPONSE_UNCONFIRMED = "SUBMISSION_RESPONSE_UNCONFIRMED"str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
Ancestors
- builtins.str
- enum.Enum
Class variables
var HISTORY_CORRUPTvar INVALID_PLANvar INVALID_RESPONSEvar INVALID_SCOPEvar NOT_FOUNDvar PG_REQUIREDvar RESPONSE_UNCONFIRMEDvar STORAGE_UNAVAILABLEvar TRANSPORT_UNCERTAINvar UNAUTHORIZED
class ReportingSubmissionError (code: ReportingSubmissionCode)-
Expand source code
class ReportingSubmissionError(RuntimeError): """Closed code only: no provider, authentication, SQL or wire body details.""" def __init__(self, code: ReportingSubmissionCode) -> None: self.code = ( code if isinstance(code, ReportingSubmissionCode) else ReportingSubmissionCode.INVALID_PLAN ) super().__init__(self.code.value)Closed code only: no provider, authentication, SQL or wire body details.
Ancestors
- builtins.RuntimeError
- builtins.Exception
- builtins.BaseException
class ReportingSubmissionResult (submission: ReportingReceiptSubmission,
proposal_deferred: bool = False,
diagnostic: ReportingSubmissionCode | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingSubmissionResult: """Submission outcomes only; completion does not establish reconciliation. ``proposal_deferred`` means an earlier pending scope reservation was resumed instead. Inspect its outcomes before planning any subsequent submission. """ submission: ReportingReceiptSubmission proposal_deferred: bool = False diagnostic: ReportingSubmissionCode | None = None @property def pending(self) -> bool: return self.submission.pending @property def outcomes(self) -> tuple[ReportingReceiptOutcome, ...]: return self.submission.outcomes @property def submitted_receipts(self) -> tuple[ReportingSubmissionReceipt, ...]: return tuple( receipt for outcome in self.outcomes if (receipt := outcome.receipt) is not None )Submission outcomes only; completion does not establish reconciliation.
proposal_deferredmeans an earlier pending scope reservation was resumed instead. Inspect its outcomes before planning any subsequent submission.Instance variables
var diagnostic : ReportingSubmissionCode | Noneprop outcomes : tuple[ReportingReceiptOutcome, ...]-
Expand source code
@property def outcomes(self) -> tuple[ReportingReceiptOutcome, ...]: return self.submission.outcomes prop pending : bool-
Expand source code
@property def pending(self) -> bool: return self.submission.pending var proposal_deferred : boolvar submission : ReportingReceiptSubmissionprop submitted_receipts : tuple[ReportingSubmissionReceipt, ...]-
Expand source code
@property def submitted_receipts(self) -> tuple[ReportingSubmissionReceipt, ...]: return tuple( receipt for outcome in self.outcomes if (receipt := outcome.receipt) is not None )
class ReportingSubmissionScope (seller_id: str, account_id: str, consumer_id: str)-
Expand source code
@dataclass(frozen=True) class ReportingSubmissionScope: """Identity resolved by a trusted authorization adapter, never from a request. ``seller_id`` identifies the configured seller, ``account_id`` is the seller-resolved account and ``consumer_id`` is the canonical authenticated principal. Syntax checks cannot establish provenance: the required authorizer must bind all three to the exact client and its current access. No credential or natural-key account assertion belongs in these fields. """ seller_id: str = field(repr=False) account_id: str = field(repr=False) consumer_id: str = field(repr=False) def __post_init__(self) -> None: valid = False try: consumer_reference(self.seller_id) principal_reference(self.account_id) canonical_consumer(self.consumer_id) valid = all(value == value.strip() for value in (self.seller_id, self.account_id)) except (ValueError, TypeError, RuntimeError): # Map provider validation details to the closed INVALID_SCOPE error below. pass if not valid: raise ReportingSubmissionError(ReportingSubmissionCode.INVALID_SCOPE) @property def canonical_identity(self) -> bytes: return canonical_json_utf8_v1([self.seller_id, self.account_id, self.consumer_id]) @property def storage_key(self) -> str: # Index a fixed-size digest, not a possibly 2048-character principal. # Stores also compare the full identity to fail closed on collisions. return hashlib.sha256(self.canonical_identity).hexdigest()Identity resolved by a trusted authorization adapter, never from a request.
seller_ididentifies the configured seller,account_idis the seller-resolved account andconsumer_idis the canonical authenticated principal. Syntax checks cannot establish provenance: the required authorizer must bind all three to the exact client and its current access. No credential or natural-key account assertion belongs in these fields.Instance variables
var account_id : strprop canonical_identity : bytes-
Expand source code
@property def canonical_identity(self) -> bytes: return canonical_json_utf8_v1([self.seller_id, self.account_id, self.consumer_id]) var consumer_id : strvar seller_id : strprop storage_key : str-
Expand source code
@property def storage_key(self) -> str: # Index a fixed-size digest, not a possibly 2048-character principal. # Stores also compare the full identity to fail closed on collisions. return hashlib.sha256(self.canonical_identity).hexdigest()