Module adcp.reporting.ledger
Reliable Reporting reporting.core, seller side.
Core makes one question machine-answerable: do I have definitive reporting for this period – and if not, whose problem is it? Four ideas do the work.
- Obligations exist before reports. At each period close the seller freezes the scope and commits an obligation whether or not source data exists, so a missing first report is detectable rather than silent.
- A zero-row report differs from no report. An empty period commits a revision like any other; absence means something is wrong.
- Revisions are immutable. A provisional restatement is a new snapshot superseding the old one. An official revision is terminal; later corrections are explicit accounting adjustments.
get_reporting_statusanswers "where am I?" – one authoritative read over the ledger, summarized by five health states.
What You Assemble
::
from psycopg_pool import AsyncConnectionPool
from adcp.reporting.ledger import (
PgReportingLedgerStore, ProducerOfferings, ReportingProducer,
ReportingStatusCaller, ReportingStatusHandler,
)
store = PgReportingLedgerStore(pool=pool)
await store.create_schema()
producer = ReportingProducer(
source=my_executor, # adcp.reporting.source
offerings=ProducerOfferings(official_offering_id="DAILY_OFFICIAL_V1"),
store=store,
)
await producer.run_worker() # from cron, a loop, a supervisor
status = ReportingStatusHandler(store)
payload = await status.handle(
{"view": "summary"},
caller=ReportingStatusCaller(account_id=..., consumer_id=...),
)
No Temporal, no Celery, no Redis lock.
Durability lives in the store; the
worker is a stateless leased turn, and the status handler is a pure projection
you can mount from :mod:adcp.server or any framework.
Health is derived, never stored – a pure function of obligations, revisions, and the snapshot clock, so it cannot go stale and two readers of one snapshot cannot disagree.
Opt-in surface
:mod:adcp.reporting.ledger.consumer_status implements sync_reporting_status,
an additive opt-in extension in AdCP 3.2.0-rc.2 that is off by default.
See that module for what turning it on commits you to.
Sub-modules
adcp.reporting.ledger.consumer_status-
sync_reporting_statusingest and the consumer-mismatch projection … adcp.reporting.ledger.delivery-
Optional durable seller contracts for future destination writers and receipt handlers …
adcp.reporting.ledger.delivery_changes-
Consumer-scoped incremental reconciliation reads, independent of Core cursors.
adcp.reporting.ledger.delivery_models-
Retained seller delivery/reconciliation facts. No provider clients or wire blobs …
adcp.reporting.ledger.delivery_pg-
PostgreSQL reconciliation evidence with a separate, principal-qualified feed.
adcp.reporting.ledger.health-
Deriving reporting health from immutable evidence and the clock …
adcp.reporting.ledger.models-
The records a seller's reporting ledger retains …
adcp.reporting.ledger.notification_events-
Pure event mappings, invoked only by an opted-in store's transaction.
adcp.reporting.ledger.notification_models-
Closed logical notifications and typed, consumer-qualified status identities.
adcp.reporting.ledger.pg-
PostgreSQL-backed :class:
~adcp.reporting.ledger.store.ReportingLedgerStore… adcp.reporting.ledger.producer-
The seller-side loop: close a period, acquire it, commit it, restate it …
adcp.reporting.ledger.producer_progress-
Optional bounded producer progress; the original ledger protocol stays intact.
adcp.reporting.ledger.provisional-
Durable scheduling identities and immutable provisional observation metadata …
adcp.reporting.ledger.reconciliation_projection-
Pure tier evidence over one captured private record history …
adcp.reporting.ledger.schedule-
One captured-generation clock for producer obligations and status forecasts …
adcp.reporting.ledger.status-
get_reporting_status: one authoritative read over the obligation ledger … adcp.reporting.ledger.status_projection-
Pure status projection shared by polling, source transactions and clock sweeps …
adcp.reporting.ledger.status_server-
A real optional reporting status task for the SDK MCP/A2A handler surface.
adcp.reporting.ledger.status_snapshot-
Optional status participants and connection-bound snapshot/lifecycle helpers.
adcp.reporting.ledger.store-
The durable seam under the reporting producer and the status handler …
Functions
def adjustment_to_wire(adjustment: ReportingAdjustmentRecord) ‑> dict[str, typing.Any]-
Expand source code
def adjustment_to_wire(adjustment: ReportingAdjustmentRecord) -> dict[str, Any]: """Complete immutable adjustment evidence, including reason_detail in its digest.""" return { **adjustment_payload(adjustment), "canonical_adjustment_sha256": adjustment_sha256(adjustment), }Complete immutable adjustment evidence, including reason_detail in its digest.
def aggregate_reporting_health(obligation_health: Iterable[ReportingHealth],
*,
scope_closed: bool,
coverage_complete: bool) ‑> Literal['healthy', 'waiting', 'delayed', 'action_required', 'complete']-
Expand source code
def aggregate_reporting_health( obligation_health: Iterable[ReportingHealth], *, scope_closed: bool, coverage_complete: bool, ) -> ReportingHealth: """Roll obligation health up to a scope. Worst-case wins, with one addition: incomplete coverage is itself ``action_required`` regardless of the obligations underneath it. A scope whose denominator is not fully known cannot honestly be called healthy -- the obligations that *are* present may simply be the ones that happened to resolve. """ values = list(obligation_health) if not coverage_complete: return "action_required" if "action_required" in values: return "action_required" if "delayed" in values: return "delayed" if scope_closed and all(value == "complete" for value in values): return "complete" if any(value in {"healthy", "complete"} for value in values): return "healthy" return "waiting"Roll obligation health up to a scope.
Worst-case wins, with one addition: incomplete coverage is itself
action_requiredregardless of the obligations underneath it. A scope whose denominator is not fully known cannot honestly be called healthy – the obligations that are present may simply be the ones that happened to resolve. def check_issue_state_transition(current: str, requested: str) ‑> None-
Expand source code
def check_issue_state_transition(current: str, requested: str) -> None: """Refuse any transition the spec's forward-only lifecycle forbids. Raises :class:`LedgerConflictError` rather than returning a bool so neither store can forget to act on the answer. """ if requested == current: # Idempotent: re-acknowledging an acknowledged issue is a no-op, which # is what makes an operator retry safe. return if requested not in _ISSUE_STATE_SUCCESSORS.get(current, frozenset()): raise LedgerConflictError( "ISSUE_STATE_TRANSITION_INVALID", "issue_state moves only forward through open, acknowledged, then resolved or " f"waived; {current!r} -> {requested!r} is not permitted. A recurrence gets a new " "occurrence rather than reviving a retired one", )Refuse any transition the spec's forward-only lifecycle forbids.
Raises :class:
LedgerConflictErrorrather than returning a bool so neither store can forget to act on the answer. def consumer_mismatch_issue_key(*,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
report_definition_id: str,
period_start: datetime,
period_end: datetime) ‑> str-
Expand source code
def consumer_mismatch_issue_key( *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, report_definition_id: str, period_start: datetime, period_end: datetime, ) -> str: """Identity of the *condition*, not of the statement that evidences it. Keyed on the logical status chain, deliberately not on ``reporting_status_id``. A buyer that supersedes one conflicting statement with another conflicting statement has not resolved anything, and rc.3 anchors the escalation clock to the issue's ``opened_at``. Keying on the statement id would mint a fresh issue -- and a fresh clock -- on every re-file, letting an unresolved disagreement dodge escalation forever. Also deliberately not keyed on the obligation id: a chain that began as ``obligation_missing`` attaches to the repaired obligation later, and the spec requires that chain never be lost, forked, or reset. """ generation = ReportingConfigurationGenerationKey( account_id=account_id, consumer_id=consumer_id, delivery_config_id=delivery_config_id, delivery_config_version=delivery_config_version, ) payload = canonical_json_utf8_v1( [ "core-consumer-status-mismatch-v1", generation.account_id, consumer_id, generation.delivery_config_id, generation.delivery_config_version, report_definition_id, _utc(period_start).isoformat(), _utc(period_end).isoformat(), ] ) return "rpik_" + hashlib.sha256(payload).hexdigest()[:40]Identity of the condition, not of the statement that evidences it.
Keyed on the logical status chain, deliberately not on
reporting_status_id. A buyer that supersedes one conflicting statement with another conflicting statement has not resolved anything, and rc.3 anchors the escalation clock to the issue'sopened_at. Keying on the statement id would mint a fresh issue – and a fresh clock – on every re-file, letting an unresolved disagreement dodge escalation forever.Also deliberately not keyed on the obligation id: a chain that began as
obligation_missingattaches to the repaired obligation later, and the spec requires that chain never be lost, forked, or reset. def consumer_statement_conflicts(*,
current: ConsumerStatusRecord,
current_revision: ReportingRevisionRecord | None,
revisions: Sequence[ReportingRevisionRecord] = ()) ‑> bool-
Expand source code
def consumer_statement_conflicts( *, current: ConsumerStatusRecord, current_revision: ReportingRevisionRecord | None, revisions: Sequence[ReportingRevisionRecord] = (), ) -> bool: """Whether this statement contradicts the seller's *record* of the period. Deliberately independent of the seller's health *label*. The two questions are different and conflating them is how an escalation clock gets reset: health answers "should this caller's view be degraded right now", while this answers "does the disagreement still stand" -- which is what decides whether the issue may be retired. The spec allows resolution two ways: the consumer supersedes with a statement that agrees, or the seller's own projection changes so the two no longer conflict. Both are record comparisons: * ``obligation_missing`` -- the obligation is in front of us, so the buyer's claim that the seller omitted the period is contradicted. Always conflicts. * ``revision_missing`` -- conflicts only while the seller actually has a current required revision. If the seller has none either, they agree. * ``unreadable`` -- conflicts while the named revision is still readable in the seller's record. A seller that has since marked it unreadable agrees. * ``content_mismatch`` -- conflicts while the named revision is still the one the seller requires, because that is the seller standing behind the content the buyer disputes. * ``received`` -- conflicts only once something superseded the revision the buyer named. """ status = current.consumer_status if status == "obligation_missing": return True if status == "revision_missing": return current_revision is not None if status == "unreadable": named = _named_revision(current, revisions) return named is None or named.readable if status == "content_mismatch": return ( current_revision is not None and current.reporting_revision_id == current_revision.reporting_revision_id ) if status == "received": if current.reporting_revision_id is None: return False if ( current_revision is not None and current.reporting_revision_id == current_revision.reporting_revision_id ): return False # Stale only if a restatement actually superseded it. With no # supersession and no current required revision there is nothing to # re-read, so nothing to forgive and nothing to page about. return _superseder(current.reporting_revision_id, revisions) is not None or ( current_revision is not None ) # No fallback return: every ``ConsumerStatusValue`` is handled above, so a # sixth status added to the schema fails mypy with "missing return # statement" rather than silently classifying itself as "no conflict".Whether this statement contradicts the seller's record of the period.
Deliberately independent of the seller's health label. The two questions are different and conflating them is how an escalation clock gets reset: health answers "should this caller's view be degraded right now", while this answers "does the disagreement still stand" – which is what decides whether the issue may be retired.
The spec allows resolution two ways: the consumer supersedes with a statement that agrees, or the seller's own projection changes so the two no longer conflict. Both are record comparisons:
obligation_missing– the obligation is in front of us, so the buyer's claim that the seller omitted the period is contradicted. Always conflicts.revision_missing– conflicts only while the seller actually has a current required revision. If the seller has none either, they agree.unreadable– conflicts while the named revision is still readable in the seller's record. A seller that has since marked it unreadable agrees.content_mismatch– conflicts while the named revision is still the one the seller requires, because that is the seller standing behind the content the buyer disputes.received– conflicts only once something superseded the revision the buyer named.
def current_consumer_statement(statuses: Sequence[ConsumerStatusRecord]) ‑> ConsumerStatusRecord | None-
Expand source code
def current_consumer_statement( statuses: Sequence[ConsumerStatusRecord], ) -> ConsumerStatusRecord | None: """This caller's one unsuperseded leaf, or ``None`` for an empty chain.""" return next((item for item in statuses if not item.superseded), None)This caller's one unsuperseded leaf, or
Nonefor an empty chain. def current_required_revision(obligation: ReportingObligationRecord,
revisions: Sequence[ReportingRevisionRecord]) ‑> ReportingRevisionRecord | None-
Expand source code
def current_required_revision( obligation: ReportingObligationRecord, revisions: Sequence[ReportingRevisionRecord], ) -> ReportingRevisionRecord | None: """Source-compatible wrapper. Corrupt and not-ready histories both return None. New callers should consume ``select_reporting_revision``'s discriminated result so corruption can be parked for repair instead of retried as absence. """ result = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) return result.revision if result.kind == "selected" else NoneSource-compatible wrapper. Corrupt and not-ready histories both return None.
New callers should consume
select_reporting_revision()'s discriminated result so corruption can be parked for repair instead of retried as absence. def derive_period(schedule: ReportingScheduleSpec,
*,
account_timezone: str,
ordinal: int,
activated_at: datetime | None = None) ‑> ReportingPeriodBoundary-
Expand source code
def derive_period( schedule: ReportingScheduleSpec, *, account_timezone: str, ordinal: int, activated_at: datetime | None = None, ) -> ReportingPeriodBoundary: """Derive period ``ordinal`` from a schedule, the way both sides must. Both the buyer and the seller run this calculation independently, and a buyer treats a missing obligation as a protocol failure rather than evidence that nothing happened. That only works if the derivation is identical, so it lives here rather than in each side's handler. Periods are anchored at the Unix epoch in the schedule's timezone (or at an explicit ``period_anchor`` for billing-cycle alignment), which is what makes "the next full boundary" unambiguous. A configuration activated mid-period begins at the next boundary -- a partial first period would be reported as complete and understate delivery. """ zone_name = schedule.timezone_name(account_timezone) sla = iso_duration_to_timedelta(schedule.delivery_sla) # Walk whole periods from the anchor in local wall-clock terms so a DST # transition shifts the instant without changing which period it is. start, end = _period_instants(schedule, account_timezone, ordinal) if activated_at is not None and _utc(activated_at) > start: raise ValueError( "a configuration activated mid-period owes its first obligation at the next " "full boundary; a partial first period would understate delivery" ) return ReportingPeriodBoundary( # No "/": a period key travels into the source slice request, whose # identifier pattern is [A-Za-z0-9_.:-]. period_key=f"{start.strftime('%Y-%m-%dT%H:%M:%SZ')}_{schedule.period_duration}", start=start, end=end, source_timezone=zone_name, expected_at=end + sla, )Derive period
ordinalfrom a schedule, the way both sides must.Both the buyer and the seller run this calculation independently, and a buyer treats a missing obligation as a protocol failure rather than evidence that nothing happened. That only works if the derivation is identical, so it lives here rather than in each side's handler.
Periods are anchored at the Unix epoch in the schedule's timezone (or at an explicit
period_anchorfor billing-cycle alignment), which is what makes "the next full boundary" unambiguous. A configuration activated mid-period begins at the next boundary – a partial first period would be reported as complete and understate delivery. def iso_duration_to_timedelta(value: str) ‑> datetime.timedelta-
Expand source code
def iso_duration_to_timedelta(value: str) -> timedelta: """Parse the restricted ISO-8601 durations a reporting schedule may use. Days, hours, minutes, seconds only. Months and years are deliberately unsupported: their length depends on a calendar, and a schedule whose period length depends on which month it is cannot be derived identically by both sides -- which is the whole point of publishing the schedule. """ import re match = re.fullmatch( r"P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?", value, ) if match is None or value in {"P", "PT"}: raise ValueError( f"{value!r} is not a supported reporting duration; use day/hour/minute/second " "components only (calendar months and years are not derivable identically " "by both parties)" ) days, hours, minutes, seconds = match.groups() return timedelta( days=int(days or 0), hours=int(hours or 0), minutes=int(minutes or 0), seconds=float(seconds or 0), )Parse the restricted ISO-8601 durations a reporting schedule may use.
Days, hours, minutes, seconds only. Months and years are deliberately unsupported: their length depends on a calendar, and a schedule whose period length depends on which month it is cannot be derived identically by both sides – which is the whole point of publishing the schedule.
def issue_id_for(kind: str, *parts: object) ‑> str-
Expand source code
def issue_id_for(kind: str, *parts: object) -> str: """A stable, derived issue identity. Derived rather than stored because these conditions are monotone for one immutable obligation: once a qualifying revision is associated, ``REPORT_OVERDUE`` cannot recur. That makes a stateless identity correct without claiming a general issue-lifecycle ledger -- and it means a consumer polling twice deduplicates on the same id both times. """ digest = hashlib.sha256(canonical_json_utf8_v1([kind, *[str(part) for part in parts]])).digest() return "rpti_" + base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")[:32]A stable, derived issue identity.
Derived rather than stored because these conditions are monotone for one immutable obligation: once a qualifying revision is associated,
REPORT_OVERDUEcannot recur. That makes a stateless identity correct without claiming a general issue-lifecycle ledger – and it means a consumer polling twice deduplicates on the same id both times. def issue_id_for_occurrence(issue_key: str, generation: int) ‑> str-
Expand source code
def issue_id_for_occurrence(issue_key: str, generation: int) -> str: """The id for one *occurrence* of a stored, non-monotone condition. :func:`issue_id_for` is enough for conditions that cannot recur once satisfied. A consumer mismatch can: the buyer supersedes, the seller restates, the disagreement clears and comes back. AdCP 3.2.0-rc.3 requires a recurrence after retirement to get a *new* ``issue_id``, so identity has to include which occurrence this is -- the generation counter held by :class:`~adcp.reporting.ledger.models.ReportingIssueLifecycle`. Folding the generation into the digest rather than appending it keeps the id opaque, so nothing downstream can parse it back into "how many times has this buyer complained". """ digest = hashlib.sha256( canonical_json_utf8_v1(["core-issue-occurrence-v1", issue_key, generation]) ).digest() return "rpti_" + base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")[:32]The id for one occurrence of a stored, non-monotone condition.
:func:
issue_id_for()is enough for conditions that cannot recur once satisfied. A consumer mismatch can: the buyer supersedes, the seller restates, the disagreement clears and comes back. AdCP 3.2.0-rc.3 requires a recurrence after retirement to get a newissue_id, so identity has to include which occurrence this is – the generation counter held by :class:~adcp.reporting.ledger.models.ReportingIssueLifecycle.Folding the generation into the digest rather than appending it keeps the id opaque, so nothing downstream can parse it back into "how many times has this buyer complained".
def issue_is_retirable(current: str) ‑> bool-
Expand source code
def issue_is_retirable(current: str) -> bool: """Whether the projection may move this state to ``resolved``. ``waived`` is not retirable: it is already out of the projection by agreement, and overwriting that readable act with ``resolved`` is an edge the forward-only lifecycle forbids. Both stores consult this rather than each remembering the rule. """ return "resolved" in _ISSUE_STATE_SUCCESSORS.get(current, frozenset())Whether the projection may move this state to
resolved.waivedis not retirable: it is already out of the projection by agreement, and overwriting that readable act withresolvedis an edge the forward-only lifecycle forbids. Both stores consult this rather than each remembering the rule. def materialization_to_wire(view: ReportingMaterializationView,
*,
obligation: ReportingObligationRecord) ‑> dict[str, typing.Any]-
Expand source code
def materialization_to_wire( view: ReportingMaterializationView, *, obligation: ReportingObligationRecord ) -> dict[str, Any]: if view.attempt.scope.generation_key != obligation.generation_key or ( view.attempt.scope.reporting_obligation_id != obligation.reporting_obligation_id ): unavailable() result = view.to_wire() result["feed_purpose"] = obligation.feed_purpose return result def project_consumer_mismatch(*,
obligation: ReportingObligationRecord,
current_revision: ReportingRevisionRecord | None,
statuses: Sequence[ConsumerStatusRecord],
seller_health: ReportingHealth,
revisions: Sequence[ReportingRevisionRecord] = (),
ledger_as_of: datetime | None = None,
delivery_sla: timedelta = datetime.timedelta(0),
automated_recovery_window: timedelta = datetime.timedelta(seconds=21600),
escalation: ReportingDeliveryEscalation | None = None,
lifecycle: ReportingIssueLifecycle | None = None) ‑> ConsumerMismatch | None-
Expand source code
def project_consumer_mismatch( *, obligation: ReportingObligationRecord, current_revision: ReportingRevisionRecord | None, statuses: Sequence[ConsumerStatusRecord], seller_health: ReportingHealth, revisions: Sequence[ReportingRevisionRecord] = (), ledger_as_of: datetime | None = None, delivery_sla: timedelta = timedelta(0), automated_recovery_window: timedelta = timedelta(hours=6), escalation: ReportingDeliveryEscalation | None = None, lifecycle: ReportingIssueLifecycle | None = None, ) -> ConsumerMismatch | None: """Compare the seller's projection with this caller's current statement. Returns a mismatch only when they genuinely conflict. Five conflict kinds, four of them immediate: * ``obligation_missing``, ``revision_missing``, ``unreadable``, and ``content_mismatch`` against an otherwise ``healthy``/``complete`` seller projection are ``action_required`` at once; and * ``received`` naming a revision the seller has since superseded is ``delayed`` until its grace deadline, then ``action_required``. The carve-out exists because that buyer consumed exactly what the seller required at the time and has not yet had a bounded chance to re-read. Paging a human the instant a seller restates a provisional revision would train buyers to ignore the signal. An advertised ``consumer_mismatch_escalation`` boundary takes precedence when the two windows overlap: past ``opened_at`` plus that window the issue is ``action_required`` with a ``contact_*`` action regardless of the grace deadline. ``wait_for_retry`` must not survive that boundary -- an unattended mismatch is an escalation, not a retry. Missing consumer status stays *unknown*. It never excuses seller reporting and never by itself degrades seller health; silence is surfaced only through ``obligation_counts.consumer_status_pending``. """ current = current_consumer_statement(statuses) if current is None: return None if lifecycle is not None and lifecycle.issue_state == "waived": if waiver_covers_mismatch(lifecycle, current, obligation, current_revision): return None # A caller must settle the new occurrence before publishing. Never # treat a legacy/unrelated waiver as permission to clear this health. lifecycle = None if not consumer_statement_conflicts( current=current, current_revision=current_revision, revisions=revisions ): return None if seller_health not in {"healthy", "complete"}: # The seller already knows something is wrong and has said so. Adding a # second issue for the same condition would double-count it. # # Note this is *only* a suppression of the emission. The condition is # still open, so a caller must not read ``None`` here as "resolved" -- # see :func:`consumer_statement_conflicts`. return None boundary = _utc(ledger_as_of) if ledger_as_of is not None else None severity: Literal["delayed", "action_required"] = "action_required" grace_deadline: datetime | None = None if current.consumer_status == "received": assert current.reporting_revision_id is not None # the conflict test proved it grace_deadline = stale_received_grace_deadline( received_revision_id=current.reporting_revision_id, revisions=revisions, delivery_sla=delivery_sla, automated_recovery_window=automated_recovery_window, ) if grace_deadline is not None and boundary is not None and boundary < grace_deadline: severity = "delayed" responsible: ResponsibleParty = "buyer" message = ( "the authenticated consumer received revision " f"{current.reporting_revision_id} but the current required revision is " f"{current_revision.reporting_revision_id if current_revision else 'unknown'}; " "the consumer is working from a superseded restatement" ) else: responsible = _responsible_for(current) message = _negative_message(current) opened_at = _utc(lifecycle.opened_at) if lifecycle is not None else boundary escalated = False window = escalation.consumer_mismatch_escalation if escalation is not None else None if window is not None and opened_at is not None and boundary is not None: # Precedence: the escalation boundary wins when it overlaps the grace # window. Measured from the issue's opened_at, never from this poll, so # re-emission cannot reset it. if boundary >= opened_at + window: severity = "action_required" escalated = True issue = _mismatch_issue( obligation, current, responsible_party=responsible, message=message, severity=severity, escalated=escalated, lifecycle=lifecycle, opened_at=opened_at, ) published = lifecycle is None or lifecycle.published return ConsumerMismatch(issue=issue, severity=severity, published=published)Compare the seller's projection with this caller's current statement.
Returns a mismatch only when they genuinely conflict. Five conflict kinds, four of them immediate:
obligation_missing,revision_missing,unreadable, andcontent_mismatchagainst an otherwisehealthy/completeseller projection areaction_requiredat once; andreceivednaming a revision the seller has since superseded isdelayeduntil its grace deadline, thenaction_required.
The carve-out exists because that buyer consumed exactly what the seller required at the time and has not yet had a bounded chance to re-read. Paging a human the instant a seller restates a provisional revision would train buyers to ignore the signal.
An advertised
consumer_mismatch_escalationboundary takes precedence when the two windows overlap: pastopened_atplus that window the issue isaction_requiredwith acontact_*action regardless of the grace deadline.wait_for_retrymust not survive that boundary – an unattended mismatch is an escalation, not a retry.Missing consumer status stays unknown. It never excuses seller reporting and never by itself degrades seller health; silence is surfaced only through
obligation_counts.consumer_status_pending. def project_obligation_health(obligation: ReportingObligationRecord,
revisions: Sequence[ReportingRevisionRecord],
*,
ledger_as_of: datetime,
scope_closed: bool) ‑> ObligationProjection-
Expand source code
def project_obligation_health( obligation: ReportingObligationRecord, revisions: Sequence[ReportingRevisionRecord], *, ledger_as_of: datetime, scope_closed: bool, ) -> ObligationProjection: """Classify one obligation's immutable evidence at the snapshot's clock.""" boundary = _utc(ledger_as_of) selection = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) current = selection.revision if selection.kind == "selected" else None if selection.kind == "corrupt": return ObligationProjection( health="action_required", production_status="published" if revisions else "pending", issues=( ReportingIssue( issue_id=issue_id_for( "core-revision-history-corrupt-v2", obligation.account_id, obligation.reporting_obligation_id, ), code="HISTORY_UNAVAILABLE", severity="action_required", responsible_party="seller", recommended_action="contact_seller", reporting_obligation_id=obligation.reporting_obligation_id, delivery_config_id=obligation.delivery_config_id, delivery_config_version=obligation.delivery_config_version, feed_purpose=obligation.feed_purpose, media_buy_ids=obligation.media_buy_ids, period_start=obligation.period.start, period_end=obligation.period.end, message=( "The retained revision history is inconsistent; seller repair is required." ), ), ), satisfied=False, current_revision=None, ) if obligation.currency is None: return ObligationProjection( health="action_required", production_status="published" if revisions else "pending", issues=( ReportingIssue( issue_id=issue_id_for( "core-currency-unresolved-v1", obligation.reporting_obligation_id ), code="HISTORY_UNAVAILABLE", severity="action_required", responsible_party="seller", recommended_action="contact_seller", reporting_obligation_id=obligation.reporting_obligation_id, delivery_config_id=obligation.delivery_config_id, delivery_config_version=obligation.delivery_config_version, feed_purpose=obligation.feed_purpose, media_buy_ids=obligation.media_buy_ids, period_start=obligation.period.start, period_end=obligation.period.end, message=( "Currency was not retained for this legacy obligation; " "verified historical evidence is required." ), ), ), satisfied=False, current_revision=current, ) if current is not None and current.readable: return ObligationProjection( health="complete" if scope_closed else "healthy", production_status="published", issues=(), satisfied=True, current_revision=current, ) if current is not None: # A qualifying revision exists but nothing is readable. Core's promise # is retained readability, so this is a seller repair, not a wait. return ObligationProjection( health="action_required", production_status="published", issues=(_unreadable_issue(obligation, current.reporting_revision_id),), satisfied=False, current_revision=current, ) if revisions: # Revisions exist but none meet the required finality: the seller has # published something, so production is not pending -- it is simply not # yet official. production_status: ReportingProductionStatus = "published" elif boundary < _utc(obligation.period.expected_at): production_status = "not_due" else: production_status = "pending" if boundary < _utc(obligation.period.expected_at): return ObligationProjection( health="waiting", production_status=production_status, issues=(), satisfied=False, current_revision=None, ) health: ReportingHealth = ( "delayed" if boundary < _utc(obligation.automated_recovery_deadline_at) else "action_required" ) return ObligationProjection( health=health, production_status=production_status, issues=(_overdue_issue(obligation, health),), satisfied=False, current_revision=None, )Classify one obligation's immutable evidence at the snapshot's clock.
def project_status_scope(value: StatusProjectionInput) ‑> StatusProjectionResult-
Expand source code
def project_status_scope(value: StatusProjectionInput) -> StatusProjectionResult: """Project exact semantics and only deadlines that can change those semantics.""" result, candidates = _project(value) if result.intents: return result for at in sorted(candidates): if at <= value.snapshot.as_of: continue future, _ = _project(replace(value, snapshot=replace(value.snapshot, as_of=at))) if future.intents or future.fingerprint != result.fingerprint: return replace(result, next_due_at=at) return resultProject exact semantics and only deadlines that can change those semantics.
def receipt_to_wire(record: ReportingReceiptRecord) ‑> dict[str, typing.Any]-
Expand source code
def receipt_to_wire(record: ReportingReceiptRecord) -> dict[str, Any]: result = payload(record) result.pop("scope") result.pop("kind") if isinstance(record, ReportingRevisionReceiptRecord): result["reporting_obligation_id"] = record.scope.reporting_obligation_id result["observed_control_totals"] = totals_to_wire(record.observed_control_totals) if record.observed_canonical_content_digest is not None: result["observed_canonical_content_digest"] = ( record.observed_canonical_content_digest.to_wire() ) return { key: value for key, value in result.items() if value is not None and not (key == "rejection_codes" and not value) } -
Expand source code
def reject_reserved_authoritative_party(configuration: ReportingConfiguration) -> None: """Refuse ``authoritative_party: consumer`` before the generation is stored. AdCP 3.2.0-rc.3 reserves the value for a buyer-deposited billing revision task scoped to a later minor. No released minor defines that task, so a 3.2 seller must reject it with ``UNSUPPORTED_FEATURE`` *before* the generation becomes ready and before any obligation exists -- and must not silently coerce it to ``seller``. Coercing would be the dangerous option: the buyer asked to be the authoritative counter for a billing feed and would get a seller-authoritative one, with every obligation, revision, and receipt in this ledger quietly attributed the wrong way round. """ if configuration.authoritative_party == "consumer": raise LedgerConflictError( "UNSUPPORTED_FEATURE", "authoritative_party 'consumer' is reserved for the buyer-deposited billing " "revision task, which no released AdCP minor defines. This seller produces " "every revision; omit the field or set it to 'seller'. See " "https://github.com/adcontextprotocol/adcp/issues/7440", )Refuse
authoritative_party: consumerbefore the generation is stored.AdCP 3.2.0-rc.3 reserves the value for a buyer-deposited billing revision task scoped to a later minor. No released minor defines that task, so a 3.2 seller must reject it with
UNSUPPORTED_FEATUREbefore the generation becomes ready and before any obligation exists – and must not silently coerce it toseller.Coercing would be the dangerous option: the buyer asked to be the authoritative counter for a billing feed and would get a seller-authoritative one, with every obligation, revision, and receipt in this ledger quietly attributed the wrong way round.
def require_single_currency(currencies: Iterable[str]) ‑> str-
Expand source code
def require_single_currency(currencies: Iterable[str]) -> str: """Resolve trusted constituent currencies, rejecting an empty or mixed scope. Call this inside a currency resolver with historical account/media-buy values, before an obligation is committed. It does not convert money. """ resolved = {validate_currency(value) for value in currencies} if not resolved: return require_frozen_currency(None) if len(resolved) != 1: raise ReportingCurrencyError( "MIXED_CURRENCY_SCOPE", "one reporting obligation cannot aggregate multiple currencies" ) return next(iter(resolved))Resolve trusted constituent currencies, rejecting an empty or mixed scope.
Call this inside a currency resolver with historical account/media-buy values, before an obligation is committed. It does not convert money.
def revision_content_sha256(*,
reporting_revision_id: str,
row_count: int,
control_totals: Sequence[tuple[str, str]],
reporting_rows: Sequence[dict[str, Any]],
control_total_evidence: Sequence[ReportingControlTotalRecord] | None = None) ‑> str-
Expand source code
def revision_content_sha256( *, reporting_revision_id: str, row_count: int, control_totals: Sequence[tuple[str, str]], reporting_rows: Sequence[dict[str, Any]], control_total_evidence: Sequence[ReportingControlTotalRecord] | None = None, ) -> str: """The Core revision binding: JCS over the four bound fields, SHA-256. This is the digest ``get_media_buy_delivery`` echoes in ``reporting_revision_binding`` and the one a consumer independently recomputes from what it actually read. It binds exactly ``{reporting_revision_id, row_count, control_totals, reporting_rows}`` -- nothing about storage, materialization, or delivery, which is what keeps Core's digest distinct from the Managed Delivery canonicalization contract. New managed publishers supply ``control_total_evidence`` to bind the exact type/unit-bearing totals exposed by status and exact reads. Omitting it keeps the existing Core pair projection and all previously retained hashes intact. """ totals = ( [ item.to_wire() for item in freeze_control_totals(tuple(control_total_evidence), tuple(control_totals)) ] if control_total_evidence is not None else [{"name": name, "value": value} for name, value in control_totals] ) return hashlib.sha256( canonical_json_utf8_v1( { "reporting_revision_id": reporting_revision_id, "row_count": row_count, "control_totals": totals, "reporting_rows": [dict(row) for row in reporting_rows], } ) ).hexdigest()The Core revision binding: JCS over the four bound fields, SHA-256.
This is the digest
get_media_buy_deliveryechoes inreporting_revision_bindingand the one a consumer independently recomputes from what it actually read. It binds exactly{reporting_revision_id, row_count, control_totals, reporting_rows}– nothing about storage, materialization, or delivery, which is what keeps Core's digest distinct from the Managed Delivery canonicalization contract.New managed publishers supply
control_total_evidenceto bind the exact type/unit-bearing totals exposed by status and exact reads. Omitting it keeps the existing Core pair projection and all previously retained hashes intact. def revision_to_wire(revision: ReportingRevisionRecord,
*,
obligation: ReportingObligationRecord) ‑> dict[str, typing.Any]-
Expand source code
def revision_to_wire( revision: ReportingRevisionRecord, *, obligation: ReportingObligationRecord ) -> dict[str, Any]: """Opt-in evidence projection; Core's mounted handler remains unchanged.""" from adcp.reporting.ledger.status import _revision_to_wire if ( revision.account_id != obligation.account_id or revision.reporting_obligation_id != obligation.reporting_obligation_id ): unavailable() result = _revision_to_wire(revision, obligation) result["control_totals"] = totals_to_wire(revision_control_totals(revision, obligation)) if revision.canonical_content_digest is not None: result["canonical_content_digest"] = revision.canonical_content_digest.to_wire() return resultOpt-in evidence projection; Core's mounted handler remains unchanged.
def select_reporting_revision(revisions: Sequence[R],
*,
account_id: str,
reporting_obligation_id: str,
required_finality: str) ‑> ReportingRevisionSelected[~R] | ReportingRevisionNotReady | ReportingRevisionCorrupt-
Expand source code
def select_reporting_revision( revisions: Sequence[R], *, account_id: str, reporting_obligation_id: str, required_finality: str, ) -> ReportingRevisionSelection[R]: """Linear-time, order-independent selection, including disconnected cycles. Only empty history or a missing required official is ordinary not-ready. The caller supplies every retained revision belonging to the obligation, without filtering by finality, readability, or existing materializations. A selected unreadable revision remains selected: repair it, never fall back. """ if ( type(account_id) is not str or type(reporting_obligation_id) is not str or any( type(r.account_id) is not str or type(r.reporting_obligation_id) is not str or r.account_id != account_id or r.reporting_obligation_id != reporting_obligation_id for r in revisions ) ): return ReportingRevisionCorrupt("ownership_mismatch") if any( type(r.reporting_revision_id) is not str or not r.reporting_revision_id or ( r.supersedes_reporting_revision_id is not None and type(r.supersedes_reporting_revision_id) is not str ) for r in revisions ): return ReportingRevisionCorrupt("invalid_revision_identity") by_id = {r.reporting_revision_id: r for r in revisions} if len(by_id) != len(revisions): return ReportingRevisionCorrupt("duplicate_revision_id") if ( type(required_finality) is not str or required_finality not in {"snapshot", "official"} or any( type(r.finality) is not str or r.finality not in {"snapshot", "official"} for r in revisions ) ): return ReportingRevisionCorrupt("invalid_finality") if any( r.supersedes_reporting_revision_id is not None and r.supersedes_reporting_revision_id not in by_id for r in revisions ): return ReportingRevisionCorrupt("missing_predecessor") if any( r.supersedes_reporting_revision_id is not None and by_id[r.supersedes_reporting_revision_id].finality != r.finality for r in revisions ): return ReportingRevisionCorrupt("cross_finality_edge") if any( r.finality == "official" and r.supersedes_reporting_revision_id is not None for r in revisions ): return ReportingRevisionCorrupt("official_predecessor") officials = [r for r in revisions if r.finality == "official"] if len(officials) > 1: return ReportingRevisionCorrupt("multiple_officials") snapshots = [r for r in revisions if r.finality == "snapshot"] successors: dict[str, str] = {} for revision in snapshots: predecessor = revision.supersedes_reporting_revision_id if predecessor is not None: if predecessor in successors: return ReportingRevisionCorrupt("forked_snapshot_history") successors[predecessor] = revision.reporting_revision_id # Walk each component once. A unique-looking leaf cannot hide a cycle. visited: set[str] = set() for revision in snapshots: path: set[str] = set() node: str | None = revision.reporting_revision_id while node is not None and node not in visited: if node in path: return ReportingRevisionCorrupt("revision_cycle") path.add(node) node = by_id[node].supersedes_reporting_revision_id visited.update(path) roots = [r for r in snapshots if r.supersedes_reporting_revision_id is None] if snapshots and len(roots) != 1: return ReportingRevisionCorrupt("disconnected_snapshot_history") if not revisions: return ReportingRevisionNotReady("empty_history") if officials: return ReportingRevisionSelected(officials[0]) if required_finality == "official": return ReportingRevisionNotReady("official_required") leaf = next(r for r in snapshots if r.reporting_revision_id not in successors) return ReportingRevisionSelected(leaf)Linear-time, order-independent selection, including disconnected cycles.
Only empty history or a missing required official is ordinary not-ready. The caller supplies every retained revision belonging to the obligation, without filtering by finality, readability, or existing materializations. A selected unreadable revision remains selected: repair it, never fall back.
def stale_received_grace_deadline(*,
received_revision_id: str,
revisions: Sequence[ReportingRevisionRecord],
delivery_sla: timedelta,
automated_recovery_window: timedelta) ‑> datetime.datetime | None-
Expand source code
def stale_received_grace_deadline( *, received_revision_id: str, revisions: Sequence[ReportingRevisionRecord], delivery_sla: timedelta, automated_recovery_window: timedelta, ) -> datetime | None: """When a stale ``received`` stops being excusable. The anchor is the ``created_at`` of the **first** revision that superseded the one the consumer named, plus the configuration generation's ``delivery_sla`` as exact elapsed UTC time. Anchoring to the first supersession rather than the newest one is what stops the window from becoming a hiding place: otherwise a seller could hold a genuinely unresolved mismatch below ``action_required`` by restating on a timer. When ``delivery_sla`` resolves to zero in any of its legal spellings, the seller uses ``automated_recovery_window`` instead, so a zero-SLA feed still yields a bounded re-read window rather than an instant escalation. Returns ``None`` when nothing superseded the named revision -- there is no staleness to forgive. """ superseder = _superseder(received_revision_id, revisions) if superseder is None: return None window = delivery_sla if delivery_sla > timedelta(0) else automated_recovery_window return _utc(superseder.created_at) + windowWhen a stale
receivedstops being excusable.The anchor is the
created_atof the first revision that superseded the one the consumer named, plus the configuration generation'sdelivery_slaas exact elapsed UTC time. Anchoring to the first supersession rather than the newest one is what stops the window from becoming a hiding place: otherwise a seller could hold a genuinely unresolved mismatch belowaction_requiredby restating on a timer.When
delivery_slaresolves to zero in any of its legal spellings, the seller usesautomated_recovery_windowinstead, so a zero-SLA feed still yields a bounded re-read window rather than an instant escalation.Returns
Nonewhen nothing superseded the named revision – there is no staleness to forgive. def validate_currency(value: object) ‑> str-
Expand source code
def validate_currency(value: object) -> str: """Require an ISO 4217-shaped code, without coercion or a registry lookup.""" if not isinstance(value, str) or re.fullmatch(r"[A-Z]{3}", value) is None: raise ReportingCurrencyError( "INVALID_CURRENCY", "reporting currency must be three uppercase ASCII letters" ) return valueRequire an ISO 4217-shaped code, without coercion or a registry lookup.
Classes
class ConsumerMismatch (issue: ReportingIssue,
severity: "Literal['delayed', 'action_required']",
published: bool = True)-
Expand source code
@dataclass(frozen=True) class ConsumerMismatch: """A consumer mismatch, with the severity it degrades the caller's view to. Severity is carried alongside the issue rather than read back off it so the caller cannot accidentally project ``delayed`` health from an ``action_required`` issue, or the reverse. """ issue: ReportingIssue severity: Literal["delayed", "action_required"] #: Only published occurrences contribute health. A waived occurrence is #: omitted only while its exact statement and diagnosed conflict match #: the recorded bilateral waiver. Later disagreements are independent. published: bool = TrueA consumer mismatch, with the severity it degrades the caller's view to.
Severity is carried alongside the issue rather than read back off it so the caller cannot accidentally project
delayedhealth from anaction_requiredissue, or the reverse.Instance variables
var issue : ReportingIssuevar published : bool-
Only published occurrences contribute health. A waived occurrence is omitted only while its exact statement and diagnosed conflict match the recorded bilateral waiver. Later disagreements are independent.
var severity : Literal['delayed', 'action_required']
class ConsumerStatusDisabledError (*args, **kwargs)-
Expand source code
class ConsumerStatusDisabledError(RuntimeError): """The consumer-status ingest was called without being enabled."""The consumer-status ingest was called without being enabled.
Ancestors
- builtins.RuntimeError
- builtins.Exception
- builtins.BaseException
class ConsumerStatusIngest (store: ReportingLedgerStore,
enabled: bool | None = None,
clock: Callable[[], datetime] | None = None)-
Expand source code
@dataclass(frozen=True) class ConsumerStatusIngest: """Records authenticated consumer statements into the ledger. Framework-agnostic like :class:`~adcp.reporting.ledger.status.ReportingStatusHandler`: a request mapping plus an authenticated caller in, a response mapping out. """ store: ReportingLedgerStore enabled: bool | None = None #: Supplies ``recorded_at`` -- when the seller durably recorded the #: statement -- and therefore the issue's ``opened_at``. Defaults to wall #: clock. A deployment whose ledger boundary comes from the database should #: pass the same source here, or an issue's escalation clock and the #: snapshot that reads it will disagree about what time it is. clock: Callable[[], datetime] | None = None def _require_enabled(self) -> None: active = CONSUMER_STATUS_ENABLED if self.enabled is None else self.enabled if not active: raise ConsumerStatusDisabledError( "sync_reporting_status is an additive opt-in extension. Set " "adcp.reporting.ledger.consumer_status.CONSUMER_STATUS_ENABLED = True, or " "pass enabled=True, and advertise consumer_status_task before accepting " "live statements." ) async def handle( self, request: dict[str, Any], *, account_id: str, consumer_id: str ) -> dict[str, Any]: """Serve one ``sync_reporting_status`` batch. Returns one ``recorded`` / ``unchanged`` / ``failed`` result per submitted statement. A failure in one statement does not fail the batch: a buyer reporting five periods should not lose four good statements to one stale supersession pointer. """ self._require_enabled() statements = request.get("statuses") or [] if not statements: raise LedgerConflictError( "EMPTY_BATCH", "a sync_reporting_status batch must carry at least one statement" ) seen_chains: set[tuple[Any, ...]] = set() results: list[dict[str, Any]] = [] for statement in statements: status_id = statement.get("reporting_status_id", "") problems = _validate_consumer_status_wire(statement) if problems: results.append(_failed(status_id, "INVALID_CONSUMER_STATUS", problems[0])) continue try: record = self._to_record( statement, account_id=account_id, consumer_id=consumer_id, recorded_at=self._now(), ) except ValueError as error: results.append(_failed(status_id, "INVALID_CONSUMER_STATUS", str(error))) continue if record.chain_key in seen_chains: # "A batch contains at most one update for each logical status # chain" -- two updates in one batch have no defined order, so # the second would silently win or silently lose. results.append( _failed( status_id, "DUPLICATE_CHAIN_IN_BATCH", "a batch may carry at most one statement per logical status chain", ) ) continue seen_chains.add(record.chain_key) try: # Answer "exact retry?" before validating anything, because # some of that validation is time-dependent. "content_mismatch # must name the revision the seller currently requires" has an # answer that changes when the seller restates, so a retry of a # statement this ledger already accepted would otherwise be # rejected for a reason that did not apply when it was first # recorded. The spec's result_mapping requires `unchanged`, and # a buyer retrying after a transport failure has no way to tell # a genuine rejection from that. replay = await self.store.resolve_consumer_status_replay(record) if replay is not None: results.append({"result": "unchanged", "consumer_status": _to_wire(replay)}) continue await self._validate_against_configuration(record) from adcp.reporting.ledger.status_snapshot import ReportingStatusParticipant atomic = isinstance(self.store, ReportingStatusParticipant) if atomic and isinstance(self.store, ReportingStatusParticipant): stored, recorded = await self.store.record_consumer_status_with_lifecycle( record ) else: stored, recorded = await self.store.record_consumer_status(record) except LedgerConflictError as error: results.append(_failed(status_id, error.code, str(error))) continue if recorded and not atomic: await self._open_issue_on_first_observation(stored) results.append( { "result": "recorded" if recorded else "unchanged", "consumer_status": _to_wire(stored), } ) return {"status": "completed", "results": results} async def _open_issue_on_first_observation(self, stored: ConsumerStatusRecord) -> None: """Start the escalation clock when the statement arrives, not when polled. ``opened_at`` is "when the seller first observed this logical condition", and the seller observes it here -- a statement landing on this task *is* the observation. Deferring to the first ``get_reporting_status`` would tie the escalation clock to whether anyone happened to poll: a mismatch filed and never read would sit at ``opened_at = None`` indefinitely and could never cross ``consumer_mismatch_escalation_seconds``, which is precisely the unattended case the boundary exists for. Only opens; never retires and never changes an existing occurrence. ``ensure_issue_opened`` is idempotent, so a supersession that keeps the same condition open keeps the same ``opened_at``. """ obligation: ReportingObligationRecord | None = None if stored.reporting_obligation_id is not None: obligation = await self.store.get_obligation( account_id=stored.account_id, reporting_obligation_id=stored.reporting_obligation_id, ) if obligation is None: obligation = await self.store.find_obligation( account_id=stored.account_id, consumer_id=stored.consumer_id, delivery_config_id=stored.delivery_config_id, delivery_config_version=stored.delivery_config_version, period_start=stored.period_start, period_end=stored.period_end, ) revisions = ( await self.store.list_revisions( account_id=stored.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if obligation is not None else () ) current = None if obligation is not None: selection = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) if selection.kind == "corrupt": return # Corruption cannot establish or resolve consumer disagreement. if selection.kind == "selected": current = selection.revision if not consumer_statement_conflicts( current=stored, current_revision=current, revisions=revisions ): return key = consumer_mismatch_issue_key( account_id=stored.account_id, consumer_id=stored.consumer_id, delivery_config_id=stored.delivery_config_id, delivery_config_version=stored.delivery_config_version, report_definition_id=stored.report_definition_id, period_start=stored.period_start, period_end=stored.period_end, ) seen: set[str] = set() while ( issue := await self.store.get_issue(account_id=stored.account_id, issue_key=key) ) is not None and issue.issue_state == "waived": if issue.issue_id in seen or issue.issue_key != key: raise LedgerConflictError("STATUS_PROJECTION_UNAVAILABLE", "invalid waiver chain") seen.add(issue.issue_id) if waiver_covers_mismatch(issue, stored, obligation, current): return key = condition_after_waiver(issue) await self.store.ensure_issue_opened( issue_key=key, account_id=stored.account_id, consumer_id=stored.consumer_id, observed_at=stored.recorded_at, ) async def _validate_against_configuration(self, record: ConsumerStatusRecord) -> None: """Resolve every identifier the statement names, within caller + account. Deliberately does *not* require an obligation to exist *when none is named*: the whole point of ``obligation_missing`` is that it is filed when the seller's ledger omitted the period, and the configuration generation is what makes that period legitimate. But an obligation or revision id the caller *does* supply must resolve. The spec's ``batch_identity`` rule is explicit that every optional obligation, revision, and superseded status MUST resolve within the authenticated caller and account or fail with the same unavailable result. Skipping that lets a statement name a foreign, nonexistent, or long-superseded revision and still be recorded -- and then projected as ``action_required`` against a seller who never published the thing being disputed. """ configurations = await self.store.list_configurations( caller=ReportingDeliveryPrincipal(record.account_id, record.consumer_id), delivery_config_ids=[record.delivery_config_id], ) generation = next( (item for item in configurations if item.generation_key == record.generation_key), None, ) if generation is None: raise LedgerConflictError( "UNKNOWN_CONFIGURATION_GENERATION", f"{record.delivery_config_id}@{record.delivery_config_version} is not an " "accepted configuration generation for this account", ) if generation.report_definition_id != record.report_definition_id: raise LedgerConflictError( "REPORT_DEFINITION_MISMATCH", "the statement names a different report definition than the configuration " "generation accepted; unlike reporting promises must not share a status chain", ) if (record.seller_ledger_snapshot_id is None) != (record.seller_ledger_as_of is None): raise LedgerConflictError( "SELLER_SNAPSHOT_EVIDENCE_INCOMPLETE", "seller_ledger_snapshot_id and seller_ledger_as_of are present together or " "not at all", ) await self._resolve_named_records(record) validate_consumer_status_timing(record, generation, as_of=self._now()) async def _resolve_named_records(self, record: ConsumerStatusRecord) -> None: """Resolve the optional obligation and revision ids the caller supplied. Unknown, unauthorized, cross-account, and cross-caller identifiers all produce the same result, so this surface cannot be used as an oracle to discover which ids exist on another tenant. """ obligation: ReportingObligationRecord | None = None if record.reporting_obligation_id is not None: obligation = await self.store.get_obligation( account_id=record.account_id, reporting_obligation_id=record.reporting_obligation_id, ) if obligation is None: raise LedgerConflictError( "LOOKUP_UNAVAILABLE", f"reporting_obligation_id {record.reporting_obligation_id} does not " "resolve for this caller and account", ) if ( obligation.generation_key != record.generation_key or obligation.report_definition_id != record.report_definition_id or _utc(obligation.period.start) != _utc(record.period_start) or _utc(obligation.period.end) != _utc(record.period_end) ): # The id resolves but describes a different period or # generation. Recording it would attach this chain to the wrong # obligation, which the immutability rule forbids. raise LedgerConflictError( "OBLIGATION_IDENTITY_MISMATCH", f"reporting_obligation_id {record.reporting_obligation_id} names a " "different configuration generation, report definition, or period than " "this statement", ) if obligation is None: obligation = await self.store.find_obligation( account_id=record.account_id, consumer_id=record.consumer_id, delivery_config_id=record.delivery_config_id, delivery_config_version=record.delivery_config_version, period_start=record.period_start, period_end=record.period_end, ) required = None if obligation is not None: revisions = await self.store.list_revisions( account_id=record.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) selection = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) if selection.kind == "corrupt": raise LedgerConflictError( "HISTORY_UNAVAILABLE", "the revision history requires repair" ) if selection.kind == "selected": required = selection.revision if record.reporting_revision_id is None: return revision = await self.store.get_revision( account_id=record.account_id, reporting_revision_id=record.reporting_revision_id ) if revision is None or ( obligation is not None and revision.reporting_obligation_id != obligation.reporting_obligation_id ): raise LedgerConflictError( "LOOKUP_UNAVAILABLE", f"reporting_revision_id {record.reporting_revision_id} does not resolve for " "this caller, account, and period", ) if record.consumer_status != "content_mismatch" or obligation is None: return # ``content_mismatch`` is valid only against a revision the seller # *currently requires* for the period. Against a superseded revision the # dispute is already moot -- the seller has replaced the content -- and # recording it would degrade the caller's own view over bytes neither # party stands behind any more. The buyer's move there is to re-read and # either accept or dispute the current revision. if required is None or required.reporting_revision_id != record.reporting_revision_id: raise LedgerConflictError( "REVISION_NOT_CURRENTLY_REQUIRED", f"content_mismatch names revision {record.reporting_revision_id}, which is " "not the revision this seller currently requires for the period; re-read the " "current revision and file against that", ) def _now(self) -> datetime: return _utc(self.clock() if self.clock is not None else datetime.now(timezone.utc)) @staticmethod def _to_record( statement: dict[str, Any], *, account_id: str, consumer_id: str, recorded_at: datetime, ) -> ConsumerStatusRecord: from adcp.types import ReportingConsumerStatus parsed = ReportingConsumerStatus.model_validate(statement) period = parsed.period return ConsumerStatusRecord( reporting_status_id=parsed.reporting_status_id, # Identity comes from authenticated transport. A body that asserts # a buyer or consumer principal is ignored, not trusted. account_id=account_id, consumer_id=consumer_id, delivery_config_id=parsed.delivery_config_id, delivery_config_version=parsed.delivery_config_version, report_definition_id=parsed.report_definition_id, period_start=_utc(period.start), period_end=_utc(period.end), period_source_timezone=period.source_timezone, consumer_status=_narrow_recorded_status(parsed.consumer_status.value), status_as_of=_utc(parsed.status_as_of), recorded_at=recorded_at, supersedes_reporting_status_id=parsed.supersedes_reporting_status_id, reporting_obligation_id=parsed.reporting_obligation_id, reporting_revision_id=parsed.reporting_revision_id, observed_revision_content_sha256=parsed.observed_revision_content_sha256, failure_code=parsed.failure_code.value if parsed.failure_code else None, mismatch_code=parsed.mismatch_code.value if parsed.mismatch_code else None, consumer_commit_ref=parsed.consumer_commit_ref, seller_ledger_snapshot_id=parsed.seller_ledger_snapshot_id, seller_ledger_as_of=( _utc(parsed.seller_ledger_as_of) if parsed.seller_ledger_as_of else None ), )Records authenticated consumer statements into the ledger.
Framework-agnostic like :class:
~adcp.reporting.ledger.status.ReportingStatusHandler: a request mapping plus an authenticated caller in, a response mapping out.Instance variables
var clock : collections.abc.Callable[[], datetime.datetime] | None-
Supplies
recorded_at– when the seller durably recorded the statement – and therefore the issue'sopened_at. Defaults to wall clock. A deployment whose ledger boundary comes from the database should pass the same source here, or an issue's escalation clock and the snapshot that reads it will disagree about what time it is. var enabled : bool | Nonevar store : ReportingLedgerStore
Methods
async def handle(self, request: dict[str, Any], *, account_id: str, consumer_id: str) ‑> dict[str, typing.Any]-
Expand source code
async def handle( self, request: dict[str, Any], *, account_id: str, consumer_id: str ) -> dict[str, Any]: """Serve one ``sync_reporting_status`` batch. Returns one ``recorded`` / ``unchanged`` / ``failed`` result per submitted statement. A failure in one statement does not fail the batch: a buyer reporting five periods should not lose four good statements to one stale supersession pointer. """ self._require_enabled() statements = request.get("statuses") or [] if not statements: raise LedgerConflictError( "EMPTY_BATCH", "a sync_reporting_status batch must carry at least one statement" ) seen_chains: set[tuple[Any, ...]] = set() results: list[dict[str, Any]] = [] for statement in statements: status_id = statement.get("reporting_status_id", "") problems = _validate_consumer_status_wire(statement) if problems: results.append(_failed(status_id, "INVALID_CONSUMER_STATUS", problems[0])) continue try: record = self._to_record( statement, account_id=account_id, consumer_id=consumer_id, recorded_at=self._now(), ) except ValueError as error: results.append(_failed(status_id, "INVALID_CONSUMER_STATUS", str(error))) continue if record.chain_key in seen_chains: # "A batch contains at most one update for each logical status # chain" -- two updates in one batch have no defined order, so # the second would silently win or silently lose. results.append( _failed( status_id, "DUPLICATE_CHAIN_IN_BATCH", "a batch may carry at most one statement per logical status chain", ) ) continue seen_chains.add(record.chain_key) try: # Answer "exact retry?" before validating anything, because # some of that validation is time-dependent. "content_mismatch # must name the revision the seller currently requires" has an # answer that changes when the seller restates, so a retry of a # statement this ledger already accepted would otherwise be # rejected for a reason that did not apply when it was first # recorded. The spec's result_mapping requires `unchanged`, and # a buyer retrying after a transport failure has no way to tell # a genuine rejection from that. replay = await self.store.resolve_consumer_status_replay(record) if replay is not None: results.append({"result": "unchanged", "consumer_status": _to_wire(replay)}) continue await self._validate_against_configuration(record) from adcp.reporting.ledger.status_snapshot import ReportingStatusParticipant atomic = isinstance(self.store, ReportingStatusParticipant) if atomic and isinstance(self.store, ReportingStatusParticipant): stored, recorded = await self.store.record_consumer_status_with_lifecycle( record ) else: stored, recorded = await self.store.record_consumer_status(record) except LedgerConflictError as error: results.append(_failed(status_id, error.code, str(error))) continue if recorded and not atomic: await self._open_issue_on_first_observation(stored) results.append( { "result": "recorded" if recorded else "unchanged", "consumer_status": _to_wire(stored), } ) return {"status": "completed", "results": results}Serve one
sync_reporting_statusbatch.Returns one
recorded/unchanged/failedresult per submitted statement. A failure in one statement does not fail the batch: a buyer reporting five periods should not lose four good statements to one stale supersession pointer.
class ConsumerStatusRecord (reporting_status_id: str,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
report_definition_id: str,
period_start: datetime,
period_end: datetime,
period_source_timezone: str,
consumer_status: ConsumerStatusValue,
status_as_of: datetime,
recorded_at: datetime,
supersedes_reporting_status_id: str | None = None,
reporting_obligation_id: str | None = None,
reporting_revision_id: str | None = None,
observed_revision_content_sha256: str | None = None,
failure_code: str | None = None,
mismatch_code: ReportingMismatchCode | None = None,
consumer_commit_ref: str | None = None,
seller_ledger_snapshot_id: str | None = None,
seller_ledger_as_of: datetime | None = None,
superseded: bool = False)-
Expand source code
@dataclass(frozen=True) class ConsumerStatusRecord: """One authenticated consumer statement about what it could consume. Separately attributed, append-only, and never seller-authored evidence: ``received`` does not satisfy the seller's production health, and a conflicting statement degrades **only** the submitting caller's view. See :mod:`adcp.reporting.ledger.consumer_status`. """ reporting_status_id: str account_id: str consumer_id: str delivery_config_id: str delivery_config_version: int report_definition_id: str period_start: datetime period_end: datetime period_source_timezone: str consumer_status: ConsumerStatusValue status_as_of: datetime recorded_at: datetime supersedes_reporting_status_id: str | None = None reporting_obligation_id: str | None = None reporting_revision_id: str | None = None observed_revision_content_sha256: str | None = None failure_code: str | None = None # AdCP 3.2.0-rc.3. Present exactly when consumer_status is # ``content_mismatch``: the closed reason the consumed revision contradicts # a fact the accepted configuration generation already fixed. Agents # dispatch on this value, never on ``message`` prose. mismatch_code: ReportingMismatchCode | None = None consumer_commit_ref: str | None = None seller_ledger_snapshot_id: str | None = None seller_ledger_as_of: datetime | None = None superseded: bool = False def __post_init__(self) -> None: principal_reference(self.account_id) consumer_reference(self.consumer_id) @property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) @property def chain_key(self) -> tuple[str, str, str, int, str, str, str]: """The logical chain this statement belongs to. Deliberately keyed *without* the seller's obligation id. Requiring it would make the first missing report invisible again, which is the exact failure this loop exists to surface. The flat shape is retained for compatibility with persisted statement digests. Use ``generation_key`` when joining configuration generations. """ return ( self.account_id, self.consumer_id, self.delivery_config_id, self.delivery_config_version, self.report_definition_id, _utc(self.period_start).isoformat(), _utc(self.period_end).isoformat(), )One authenticated consumer statement about what it could consume.
Separately attributed, append-only, and never seller-authored evidence:
receiveddoes not satisfy the seller's production health, and a conflicting statement degrades only the submitting caller's view. See :mod:adcp.reporting.ledger.consumer_status.Instance variables
var account_id : strprop chain_key : tuple[str, str, str, int, str, str, str]-
Expand source code
@property def chain_key(self) -> tuple[str, str, str, int, str, str, str]: """The logical chain this statement belongs to. Deliberately keyed *without* the seller's obligation id. Requiring it would make the first missing report invisible again, which is the exact failure this loop exists to surface. The flat shape is retained for compatibility with persisted statement digests. Use ``generation_key`` when joining configuration generations. """ return ( self.account_id, self.consumer_id, self.delivery_config_id, self.delivery_config_version, self.report_definition_id, _utc(self.period_start).isoformat(), _utc(self.period_end).isoformat(), )The logical chain this statement belongs to.
Deliberately keyed without the seller's obligation id. Requiring it would make the first missing report invisible again, which is the exact failure this loop exists to surface.
The flat shape is retained for compatibility with persisted statement digests. Use
generation_keywhen joining configuration generations. var consumer_commit_ref : str | Nonevar consumer_id : strvar consumer_status : Literal['received', 'obligation_missing', 'revision_missing', 'unreadable', 'content_mismatch']var delivery_config_id : strvar delivery_config_version : intvar failure_code : str | Noneprop generation_key : ReportingConfigurationGenerationKey-
Expand source code
@property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) var mismatch_code : Literal['scope_media_buy_missing', 'coverage_short', 'metric_missing', 'schema_nonconformant', 'currency_mismatch', 'period_mismatch'] | Nonevar observed_revision_content_sha256 : str | Nonevar period_end : datetime.datetimevar period_source_timezone : strvar period_start : datetime.datetimevar recorded_at : datetime.datetimevar report_definition_id : strvar reporting_obligation_id : str | Nonevar reporting_revision_id : str | Nonevar reporting_status_id : strvar seller_ledger_as_of : datetime.datetime | Nonevar seller_ledger_snapshot_id : str | Nonevar status_as_of : datetime.datetimevar superseded : boolvar supersedes_reporting_status_id : str | None
class FixedCurrencyResolver (currency: str = 'USD')-
Expand source code
@dataclass(frozen=True) class FixedCurrencyResolver: """The backward-compatible single-currency default, also usable explicitly.""" currency: str = "USD" def __post_init__(self) -> None: validate_currency(self.currency) def __call__( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord ) -> str: return self.currencyThe backward-compatible single-currency default, also usable explicitly.
Instance variables
var currency : str
class InMemoryReportingLedgerStore (*, clock: Callable[[], datetime] | None = None, notifications: bool = False)-
Expand source code
class InMemoryReportingLedgerStore: """Process-local reference store. Correct, ordered, and not durable. ``clock`` supplies change timestamps and the snapshot observation boundary, standing in for the database clock a durable store reads. Override it to place a test's ledger boundary at a deliberate instant. """ def __init__( self, *, clock: Callable[[], datetime] | None = None, notifications: bool = False ) -> None: self._clock = clock or (lambda: datetime.now(timezone.utc)) self._lock = asyncio.Lock() self._configurations: dict[ReportingConfigurationGenerationKey, ReportingConfiguration] = {} self._obligations: dict[str, ReportingObligationRecord] = {} self._obligation_by_period: dict[ tuple[ReportingConfigurationGenerationKey, str, str], str ] = {} self._revisions: dict[str, ReportingRevisionRecord] = {} self._revision_identity: dict[str, str] = {} self._rows: dict[str, tuple[dict[str, Any], ...]] = {} self._restatement_checkpoints: dict[str, RestatementCheckpoint] = {} self._retry_schedules: dict[str, RetryScheduleEntry] = {} self._provisional_acquisitions: dict[tuple[str, str, int], ProvisionalAcquisition] = {} self._provisional_observations: dict[tuple[str, str, int], ProvisionalObservation] = {} self._adjustments: dict[str, ReportingAdjustmentRecord] = {} self._statuses: dict[tuple[str, str, str], ConsumerStatusRecord] = {} self._status_identity: dict[tuple[str, str, str], str] = {} self._changes: list[tuple[int, str, LedgerRecordKind, str, datetime]] = [] self._change_owners: dict[int, str] = {} self._sequence = 0 self._leases: dict[ReportingConfigurationGenerationKey, tuple[str, datetime]] = {} # When each generation was last handed to a worker, so releasing a # lease sends that generation to the back of the queue instead of # letting it win every turn. self._lease_turns: dict[ReportingConfigurationGenerationKey, int] = {} self._lease_turn = 0 # Live occurrence per (account, issue_key), plus the retired generation # high-water mark so a recurrence never reuses an id. self._issues: dict[tuple[str, str], ReportingIssueLifecycle] = {} self._issue_generations: dict[tuple[str, str], int] = {} self._issue_status_scopes: dict[tuple[str, str], ReportingStatusScope] = {} self._notification_state: NotificationState | None = None self._status_notification_state: Any = None if notifications: from adcp.reporting.outbox.memory import NotificationState self._notification_state = NotificationState() self._notification_state.issue_scopes = self._issue_status_scopes @asynccontextmanager async def _mutation(self) -> AsyncIterator[None]: """Publish domain changes and notifications under one rollback boundary. Rollback also applies with notifications disabled. Newly initialized collections and sequence heads belong to the transaction too. """ owner = (id(self), asyncio.current_task()) if _MEMORY_TRANSACTION.get() == owner: yield return async with self._lock: before = deepcopy( {key: value for key, value in vars(self).items() if key not in {"_lock", "_clock"}} ) token = _MEMORY_TRANSACTION.set(owner) try: dirty_start = len(self._notification_state.dirty) if self._notification_state else 0 yield if self._notification_state is not None: from adcp.reporting.ledger.status_snapshot import settle_memory_snapshot from adcp.reporting.outbox.status import StatusBoundary dirty = tuple(self._notification_state.dirty[dirty_start:]) transaction_id = str(uuid4()) for account_id in sorted({d.scope.account_id for d in dirty}): snapshot = settle_memory_snapshot(self, account_id) account_dirty = tuple(d for d in dirty if d.scope.account_id == account_id) self._notification_state.boundaries.append( StatusBoundary( transaction_id, max(d.sequence for d in account_dirty), account_dirty, snapshot, ) ) except BaseException: for key in set(vars(self)) - {"_lock", "_clock"} - set(before): del vars(self)[key] vars(self).update(before) raise finally: _MEMORY_TRANSACTION.reset(token) @asynccontextmanager async def transaction(self) -> AsyncIterator[InMemoryReportingLedgerStore]: """Group several source mutations into one atomic, replayable boundary.""" async with self._mutation(): yield self @asynccontextmanager async def _source_publication( self, account_id: str, *, seals: ReportingSealStore | None = None ) -> AsyncIterator[None]: # The memory mutation lock serializes all accounts, including seals. async with self._mutation(): yield def _record_notification(self, event: ReportingDomainEvent) -> None: if self._notification_state is not None: self._notification_state.enqueue(event) def _dirty_status( self, scope: ReportingStatusScope, reason: DirtyReason, before: ReportingStatusEvidence | None = None, after: ReportingStatusEvidence | None = None, ) -> None: if self._notification_state is not None: self._notification_state.mark_dirty(scope, reason, self._clock(), before, after) def _resolve_issue_scope( self, *, account_id: str, consumer_id: str | None, issue_id: str, status_scope: ReportingStatusScope | None, ) -> ReportingStatusScope: """Validate a requested scope refinement without touching retained state. Default-off stores deliberately keep no rollback copy, so every caller that moves an issue record must clear this check *before* mutating: PostgreSQL rolls the statement back, the reference store cannot. """ existing = self._issue_status_scopes.get((account_id, issue_id)) scope = ( status_scope or existing or ReportingStatusScope(account_id, consumer_id=consumer_id) ) if scope.account_id != account_id or scope.consumer_id != consumer_id: raise ReportingNotificationError("invalid_status_scope") validate_scope_refinement(existing, scope) if scope.generation_key is not None: configuration = self._configurations.get(scope.generation_key) if configuration is None or ( scope.feed_purpose is not None and configuration.feed_purpose != scope.feed_purpose ): raise ReportingNotificationError("invalid_status_scope") if scope.reporting_obligation_id is not None: obligation = self._obligations.get(scope.reporting_obligation_id) if ( obligation is None or obligation.account_id != scope.account_id or ( scope.generation_key is not None and obligation.generation_key != scope.generation_key ) or ( scope.feed_purpose is not None and obligation.feed_purpose != scope.feed_purpose ) ): raise ReportingNotificationError("invalid_status_scope") return scope def _dirty_issue( self, issue: ReportingIssueLifecycle, status_scope: ReportingStatusScope | None, before: ReportingIssueLifecycle | None = None, *, enqueue: bool = True, ) -> None: key = (issue.account_id, issue.issue_id) existing = self._issue_status_scopes.get(key) scope = self._resolve_issue_scope( account_id=issue.account_id, consumer_id=issue.consumer_id, issue_id=issue.issue_id, status_scope=status_scope, ) self._issue_status_scopes[key] = scope if enqueue and (before != issue or existing != scope): self._dirty_status( scope, "issue", issue_evidence(before) if before else None, issue_evidence(issue) ) async def create_schema(self) -> None: return None def _append( self, account_id: str, kind: LedgerRecordKind, record_id: str, *, consumer_id: str ) -> None: self._sequence += 1 self._changes.append((self._sequence, account_id, kind, record_id, self._clock())) self._change_owners[self._sequence] = consumer_id # -- configurations -------------------------------------------------- async def put_configuration(self, configuration: ReportingConfiguration) -> None: reject_reserved_authoritative_party(configuration) async with self._mutation(): key = configuration.generation_key existing = self._configurations.get(key) if existing is not None and existing.quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "operator reconciliation is required" ) if existing is not None and _fingerprint(_config_payload(existing)) != _fingerprint( _config_payload(configuration) ): raise LedgerConflictError( "CONFIGURATION_GENERATION_IMMUTABLE", f"configuration {key.delivery_config_id}@{key.delivery_config_version} " "already exists with different content for this account; " "publish a new version instead of editing a retained generation", ) self._configurations[key] = configuration changed = existing is None or configuration_lifecycle( existing ) != configuration_lifecycle(configuration) if changed and self._notification_state is not None: self._dirty_status( ReportingStatusScope(configuration.account_id, configuration.generation_key), "configuration", before=configuration_evidence(existing) if existing is not None else None, after=configuration_evidence(configuration), ) async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: wanted = set(delivery_config_ids) if delivery_config_ids else None return tuple( configuration for configuration in self._configurations.values() if configuration.account_id == caller.account_id and configuration.consumer_id == caller.consumer_id and (wanted is None or configuration.delivery_config_id in wanted) ) async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: return tuple( sorted( (c for c in self._configurations.values() if not c.quarantined), key=lambda item: ( item.account_id, item.consumer_id, item.delivery_config_id, item.delivery_config_version, ), ) ) # -- obligations ----------------------------------------------------- async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: async with self._mutation(): key = ( obligation.generation_key, _utc(obligation.period.start).isoformat(), _utc(obligation.period.end).isoformat(), ) existing_id = self._obligation_by_period.get(key) if existing_id is not None: return self._obligations[existing_id] if obligation.reporting_obligation_id in self._obligations: raise LedgerConflictError( "OBLIGATION_IDENTITY_CONFLICT", "the obligation identifier already belongs to a different logical period", ) if obligation.generation_key not in self._configurations: raise LedgerConflictError( "UNKNOWN_CONFIGURATION_GENERATION", "generation is unavailable" ) if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "establish a new owned generation after operator reconciliation", ) require_frozen_currency(obligation.currency) self._obligations[obligation.reporting_obligation_id] = obligation self._obligation_by_period[key] = obligation.reporting_obligation_id self._append( obligation.account_id, "obligation", obligation.reporting_obligation_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: self._dirty_status( ReportingStatusScope.for_obligation(obligation), "obligation", after=ReportingStatusEvidence("obligation", obligation.reporting_obligation_id), ) return obligation async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: found = self._obligations.get(reporting_obligation_id) return found if found and found.account_id == account_id else None async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: key = ( ReportingConfigurationGenerationKey( account_id=account_id, consumer_id=consumer_id, delivery_config_id=delivery_config_id, delivery_config_version=delivery_config_version, ), _utc(period_start).isoformat(), _utc(period_end).isoformat(), ) found = self._obligation_by_period.get(key) return ( await self.get_obligation(account_id=account_id, reporting_obligation_id=found) if found else None ) # -- revisions ------------------------------------------------------- async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]] ) -> ReportingRevisionRecord: rows = tuple(deepcopy(row) for row in rows) # Checked first, exactly as PgReportingLedgerStore does: a caller whose # rows do not match its own declared count must get the same code from # both stores, not whichever invariant that store happens to reach. if revision.row_count != len(rows): raise LedgerConflictError( "ROW_COUNT_MISMATCH", f"revision declares {revision.row_count} rows but {len(rows)} were supplied", ) validate_managed_revision_rows(revision, rows) async with self._mutation(): identity = _revision_identity(revision) existing = self._revisions.get(revision.reporting_revision_id) if existing is not None: if existing.account_id != revision.account_id: raise LedgerConflictError( "REVISION_NOT_FOUND", "no such revision for this account" ) if self._revision_identity[revision.reporting_revision_id] != identity: raise LedgerConflictError( "REVISION_IMMUTABLE", f"revision {revision.reporting_revision_id} already exists with " "different content; a restatement is a new revision", ) return existing obligation = self._obligations.get(revision.reporting_obligation_id) if obligation is None or obligation.account_id != revision.account_id: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a revision must attach to an obligation committed at the period close", ) if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) validate_revision_currency(obligation, revision, rows) siblings = [ item for item in self._revisions.values() if item.reporting_obligation_id == revision.reporting_obligation_id ] if revision.finality == "official" and any( item.finality == "official" for item in siblings ): raise LedgerConflictError( "OFFICIAL_REVISION_TERMINAL", "an official revision already exists for this obligation; publish a later " "source correction as an adjustment", ) if revision.supersedes_reporting_revision_id: self._require_current_leaf(revision, siblings) self._revisions[revision.reporting_revision_id] = revision self._revision_identity[revision.reporting_revision_id] = identity self._rows[revision.reporting_revision_id] = tuple(dict(row) for row in rows) self._append( revision.account_id, "revision", revision.reporting_revision_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: from adcp.reporting.ledger.notification_events import revision_event self._record_notification( revision_event(revision, self._clock(), consumer_id=obligation.consumer_id) ) self._dirty_status( ReportingStatusScope.for_obligation(obligation), "revision", after=ReportingStatusEvidence( "revision", revision.reporting_revision_id, readable=revision.readable, supersedes_id=revision.supersedes_reporting_revision_id, ), ) return revision def _require_current_leaf( self, revision: ReportingRevisionRecord, siblings: Sequence[ReportingRevisionRecord] ) -> None: target = revision.supersedes_reporting_revision_id known = {item.reporting_revision_id for item in siblings} if target not in known: raise LedgerConflictError( "SUPERSEDES_UNKNOWN", f"revision {target} is not part of this obligation's chain", ) already = { item.supersedes_reporting_revision_id for item in siblings if item.supersedes_reporting_revision_id } if target in already: raise LedgerConflictError( "SUPERSEDES_STALE", f"revision {target} has already been superseded; a stale pointer would fork " "the chain and let a successful retry erase a recorded restatement", ) async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: return tuple( item for item in self._revisions.values() if item.account_id == account_id and item.reporting_obligation_id == reporting_obligation_id ) async def get_provisional_acquisition( self, *, account_id: str, reporting_obligation_id: str, ordinal: int ) -> ProvisionalAcquisition | None: return self._provisional_acquisitions.get((account_id, reporting_obligation_id, ordinal)) async def reserve_provisional_acquisition( self, acquisition: ProvisionalAcquisition ) -> ProvisionalAcquisition: # Canonical round-trip also detaches all request collections. acquisition = ProvisionalAcquisition.from_wire(acquisition.to_wire()) key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) async with self._mutation(): existing = self._provisional_acquisitions.get(key) if existing is not None: return existing obligation = self._obligations.get(acquisition.obligation_id) if obligation is None or obligation.account_id != acquisition.account_id: raise LedgerConflictError("OBLIGATION_NOT_FOUND", "unknown observation obligation") if not acquisition.binds(obligation): raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition generation differs") if any( item.account_id == acquisition.account_id and item.execution_key == acquisition.execution_key for item in self._provisional_acquisitions.values() ): raise LedgerConflictError( "OBSERVATION_CONFLICT", "execution key is already reserved" ) checkpoint = self._restatement_checkpoints.get(acquisition.obligation_id) expected = ( checkpoint.next_observation if checkpoint else len( await self.list_revisions( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) ) ) if acquisition.ordinal != expected: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") self._provisional_acquisitions[key] = acquisition return acquisition async def get_provisional_observation( self, *, account_id: str, reporting_obligation_id: str ) -> ProvisionalObservation | None: observations = [ value for (account, obligation, _), value in self._provisional_observations.items() if account == account_id and obligation == reporting_obligation_id ] return max(observations, key=lambda item: item.acquisition.ordinal, default=None) async def commit_provisional_observation( self, observation: ProvisionalObservation, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: acquisition = observation.acquisition key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) if ( revision.account_id != acquisition.account_id or revision.reporting_obligation_id != acquisition.obligation_id or revision.reporting_revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation identity differs") async with self._mutation(): existing = self._provisional_observations.get(key) if existing is not None: if ( existing.acquisition != acquisition or existing.revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation replay differs") if existing.revision_id not in self._revisions: raise LedgerConflictError( "HISTORY_UNAVAILABLE", "observation revision is missing" ) # A matching observation identity does not make conflicting # publication content an idempotent replay. return await self.commit_revision(revision, rows) retained = self._provisional_acquisitions.get(key) if retained != acquisition: raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition was not reserved") checkpoint = self._restatement_checkpoints.get(acquisition.obligation_id) if checkpoint is not None and checkpoint.next_observation != acquisition.ordinal: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") committed = await self.commit_revision(revision, rows) await self.record_restatement_checkpoint( RestatementCheckpoint( acquisition.account_id, acquisition.obligation_id, observation.checked_at, acquisition.ordinal + 1, observation.provisional_until, ) ) self._provisional_observations[key] = observation return committed async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: checkpoint = self._restatement_checkpoints.get(reporting_obligation_id) return checkpoint if checkpoint and checkpoint.account_id == account_id else None async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: return self._retry_schedules.get(scope_key) async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: async with self._mutation(): existing = self._retry_schedules.get(entry.scope_key) if existing is not None: assert entry.recorded_at is not None and existing.recorded_at is not None if _utc(entry.recorded_at) < _utc(existing.recorded_at): return existing if entry.attempt > 0 and not entry.blocked: if existing.blocked and not entry.replayed: return existing if entry.attempt < existing.attempt and not entry.replayed: return existing if not entry.replayed and not existing.blocked: entry = replace( entry, retry_not_before=max( entry.retry_not_before, existing.retry_not_before, key=_utc ), ) stored = replace(entry, replayed=False) self._retry_schedules[entry.scope_key] = stored return stored async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: async with self._mutation(): obligation = self._obligations.get(checkpoint.reporting_obligation_id) if obligation is None or obligation.account_id != checkpoint.account_id: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) existing = self._restatement_checkpoints.get(checkpoint.reporting_obligation_id) if existing is not None: if checkpoint.next_observation < existing.next_observation: return existing if checkpoint.next_observation == existing.next_observation and _utc( checkpoint.checked_at ) <= _utc(existing.checked_at): return existing self._restatement_checkpoints[checkpoint.reporting_obligation_id] = checkpoint return checkpoint async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: found = self._revisions.get(reporting_revision_id) return found if found and found.account_id == account_id else None async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: revision = await self.get_revision( account_id=account_id, reporting_revision_id=reporting_revision_id ) if revision is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") rows = self._rows.get(reporting_revision_id, ()) offset = revision_row_offset(cursor, reporting_revision_id, limit) window = rows[offset : offset + limit] has_more = offset + limit < len(rows) return ReportingRowPage( reporting_revision_id=reporting_revision_id, rows=tuple(deepcopy(row) for row in window), total_count=len(rows), has_more=has_more, cursor=( encode_cursor( {"ownership": 2, "revision": reporting_revision_id, "offset": offset + limit} ) if has_more else None ), ) async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: async with self._mutation(): existing = self._revisions.get(reporting_revision_id) if existing is None or existing.account_id != account_id: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") if existing.readable == readable: return self._revisions[reporting_revision_id] = replace(existing, readable=readable) if self._notification_state is not None: obligation = self._obligations[existing.reporting_obligation_id] self._dirty_status( ReportingStatusScope.for_obligation(obligation), "readability", ReportingStatusEvidence( "revision", reporting_revision_id, readable=existing.readable ), ReportingStatusEvidence("revision", reporting_revision_id, readable=readable), ) # -- adjustments ----------------------------------------------------- async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: async with self._mutation(): existing = self._adjustments.get(adjustment.reporting_adjustment_id) if existing is not None: if existing.account_id != adjustment.account_id: raise LedgerConflictError("ADJUSTMENT_UNAVAILABLE", "adjustment is unavailable") if existing != adjustment: raise LedgerConflictError( "ADJUSTMENT_IMMUTABLE", "adjustment content is immutable" ) return existing revision = self._revisions.get(adjustment.adjusts_reporting_revision_id) if revision is None or revision.account_id != adjustment.account_id: raise LedgerConflictError( "REVISION_NOT_FOUND", "an adjustment must name a committed revision" ) if revision.finality != "official": raise LedgerConflictError( "ADJUSTMENT_REQUIRES_OFFICIAL", "adjustments correct an official revision; restate a snapshot with a " "superseding snapshot revision instead", ) obligation = self._obligations[revision.reporting_obligation_id] if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) validate_adjustment_currency( self._obligations[revision.reporting_obligation_id], adjustment ) self._adjustments[adjustment.reporting_adjustment_id] = adjustment self._append( adjustment.account_id, "adjustment", adjustment.reporting_adjustment_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: from adcp.reporting.ledger.notification_events import adjustment_event self._record_notification( adjustment_event( adjustment, self._clock(), consumer_id=self._obligations[revision.reporting_obligation_id].consumer_id, ) ) self._dirty_status( ReportingStatusScope.for_obligation( self._obligations[revision.reporting_obligation_id] ), "adjustment", after=ReportingStatusEvidence("adjustment", adjustment.reporting_adjustment_id), ) return adjustment async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: wanted = set(reporting_revision_ids) return tuple( item for item in self._adjustments.values() if item.account_id == account_id and item.adjusts_reporting_revision_id in wanted ) # -- consumer status ------------------------------------------------- async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: async with self._lock: return self._replay(status) def _replay(self, status: ConsumerStatusRecord) -> ConsumerStatusRecord | None: key = (status.account_id, status.consumer_id, status.reporting_status_id) existing = self._statuses.get(key) if existing is None: return None if self._status_identity[key] != _consumer_status_identity(status): raise LedgerConflictError( "STATUS_IDENTITY_CONFLICT", f"reporting_status_id {status.reporting_status_id} was already " "recorded with different content", ) return existing async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: from adcp.reporting.evidence import consumer_reference consumer_reference(status.consumer_id) async with self._mutation(): identity = _consumer_status_identity(status) existing = self._replay(status) if existing is not None: return existing, False from adcp.reporting.ledger.status_snapshot import ( memory_snapshot, validate_status_evidence, ) validate_status_evidence(status, memory_snapshot(self, status.account_id)) leaf = self._current_status_leaf(status.chain_key) if status.supersedes_reporting_status_id: if ( leaf is None or leaf.reporting_status_id != status.supersedes_reporting_status_id ): raise LedgerConflictError( "STATUS_SUPERSEDES_STALE", "supersedes_reporting_status_id must name this chain's current leaf; " "a stale pointer would let a successful retry erase a recorded outage", ) self._statuses[(leaf.account_id, leaf.consumer_id, leaf.reporting_status_id)] = ( replace(leaf, superseded=True) ) elif leaf is not None: raise LedgerConflictError( "STATUS_SUPERSEDES_REQUIRED", "this chain already has a current statement; a new statement must " "explicitly supersede it", ) key = (status.account_id, status.consumer_id, status.reporting_status_id) self._statuses[key] = status self._status_identity[key] = identity self._append( status.account_id, "consumer_status", status.reporting_status_id, consumer_id=status.consumer_id, ) from adcp.reporting.ledger.status_snapshot import settle_memory_snapshot settle_memory_snapshot(self, status.account_id) if self._notification_state is not None: self._dirty_status( ReportingStatusScope( status.account_id, status.generation_key, status.reporting_obligation_id, status.consumer_id, ), "consumer_status", ( ReportingStatusEvidence("consumer_status", leaf.reporting_status_id) if leaf is not None else None ), ReportingStatusEvidence( "consumer_status", status.reporting_status_id, supersedes_id=status.supersedes_reporting_status_id, ), ) return status, True async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: return await self.record_consumer_status(status) async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.status_snapshot import settle_memory_snapshot async with self._mutation(): from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.materializer.capture import private_snapshot return private_snapshot( settle_memory_snapshot(self, caller.account_id), ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id), ) def _current_status_leaf(self, chain_key: tuple[Any, ...]) -> ConsumerStatusRecord | None: for item in self._statuses.values(): if item.chain_key == chain_key and not item.superseded: return item return None async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: wanted = set(reporting_obligation_ids) if reporting_obligation_ids is not None else None result = [] for item in self._statuses.values(): if item.account_id != account_id or item.consumer_id != consumer_id: continue if wanted is not None and not self._matches_obligations(item, wanted): continue result.append(item) return tuple(result) def _matches_obligations(self, status: ConsumerStatusRecord, wanted: set[str]) -> bool: """Attach a statement to an obligation by id *or* by logical period key. The logical-key path is what makes the loop repairable: a statement filed as ``obligation_missing`` predates the obligation it is about, and when the seller later creates that obligation the existing chain must attach to it rather than being lost, forked, or reset. """ for obligation_id in wanted: obligation = self._obligations.get(obligation_id) if obligation is None or obligation.account_id != status.account_id: continue if status.reporting_obligation_id == obligation_id: return True if ( obligation.generation_key == status.generation_key and obligation.report_definition_id == status.report_definition_id and _utc(obligation.period.start) == _utc(status.period_start) and _utc(obligation.period.end) == _utc(status.period_end) ): return True return False # -- issue lifecycle ------------------------------------------------- async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._mutation(): key = (account_id, issue_key) live = self._issues.get(key) if live is not None: if live.consumer_id != consumer_id: raise ReportingNotificationError("invalid_status_scope") previous_scope = self._issue_status_scopes.get((account_id, live.issue_id)) if status_scope is not None: validate_scope_refinement(previous_scope, status_scope) else: status_scope = previous_scope if live is not None and live.live: self._dirty_issue(live, status_scope, live) return live generation = self._issue_generations.get(key, 0) + 1 record = ReportingIssueLifecycle( issue_key=issue_key, issue_id=issue_id_for_occurrence(issue_key, generation), account_id=account_id, consumer_id=consumer_id, opened_at=_utc(observed_at), issue_state="open", generation=generation, ) self._resolve_issue_scope( account_id=account_id, consumer_id=consumer_id, issue_id=record.issue_id, status_scope=status_scope, ) self._issue_generations[key] = generation self._issues[key] = record self._dirty_issue(record, status_scope) return record async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._mutation(): if state not in {"acknowledged", "waived"}: raise LedgerConflictError( "ISSUE_STATE_NOT_OPERATOR_SETTABLE", f"issue_state {state!r} is not settable by an operator. 'resolved' is " "reachable only when the condition actually clears -- the projection " "retires it -- because a seller must not retire a mismatch out of a " "degraded projection while the statement that caused it is still the " "consumer's current leaf", ) key = (account_id, issue_key) live = self._issues.get(key) if live is None or not live.live: raise LedgerConflictError( "ISSUE_NOT_OPEN", f"no open issue {issue_key!r} for this account; a retired issue cannot be " "reopened, and a recurrence gets a new occurrence", ) check_issue_state_transition(live.issue_state, state) if state == "waived" and live.issue_state != "waived": from adcp.reporting.ledger.status_projection import bind_mismatch_waiver from adcp.reporting.ledger.status_snapshot import memory_snapshot live = bind_mismatch_waiver(memory_snapshot(self, account_id), live) updated = replace( live, issue_state=state, external_ref=external_ref or live.external_ref, # Set on the way into a retired state and never cleared. retired_at=( ( live.retired_at if live.waived_reporting_status_id is not None and live.retired_at is not None else _utc(at) ) if state == "waived" else live.retired_at ), ) self._resolve_issue_scope( account_id=account_id, consumer_id=live.consumer_id, issue_id=live.issue_id, status_scope=status_scope, ) self._issues[key] = updated # Derive the no-op from the resulting record rather than predicting # it: an idempotent re-acknowledge changes nothing and enqueues # nothing, while anything that does move retained evidence stays # reconstructable for the projector. self._dirty_issue(updated, status_scope, live) return updated async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle | None: async with self._mutation(): key = (account_id, issue_key) live = self._issues.get(key) if live is None or not live.live or not issue_is_retirable(live.issue_state): # Convergent: nothing live, or already waived. A waived issue # is retired from the projection by agreement, and overwriting # that readable act with `resolved` is an edge the forward-only # lifecycle forbids -- enforced here rather than left to each # caller to remember. return None check_issue_state_transition(live.issue_state, "resolved") retired = replace(live, issue_state="resolved", retired_at=_utc(at)) self._resolve_issue_scope( account_id=account_id, consumer_id=live.consumer_id, issue_id=live.issue_id, status_scope=status_scope, ) self._issues[key] = retired self._dirty_issue(retired, status_scope, live) return retired async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: async with self._lock: # Live occurrences only, matching the Protocol docstring and the # Postgres store. Returning a retired row here would let the # projection re-emit a resolved issue's opened_at. live = self._issues.get((account_id, issue_key)) return live if live is not None and live.live else None # -- snapshots ------------------------------------------------------- async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: account_id = caller.account_id async with self._lock: as_of = _utc(self._clock()) max_sequence = max( ( sequence for sequence, account, kind, record_id, _ in self._changes if account == account_id and self._change_owners[sequence] == caller.consumer_id ), default=0, ) return LedgerSnapshot( snapshot_id="rpls_" + _fingerprint([account_id, caller.consumer_id, filters_fingerprint, max_sequence])[ :32 ], account_id=account_id, consumer_id=caller.consumer_id, ledger_as_of=as_of, max_sequence=max_sequence, ) async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> LedgerPage: if consumer_id not in {None, snapshot.consumer_id}: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "snapshot belongs to another caller" ) consumer_id = snapshot.consumer_id lower = changes_after_sequence or 0 selected = [ item for item in self._changes if item[1] == snapshot.account_id and self._change_owners[item[0]] == consumer_id and lower < item[0] <= snapshot.max_sequence ] config_filter = set(delivery_config_ids) if delivery_config_ids else None media_buy_filter = set(media_buy_ids) if media_buy_ids else None records: list[tuple[int, LedgerRecordKind, Any]] = [] for sequence, _account, kind, record_id, _committed in sorted(selected): record = self._resolve(kind, record_id, snapshot.account_id, consumer_id) if record is None or record.account_id != snapshot.account_id: continue if not self._in_scope( kind, record, config_filter, media_buy_filter, consumer_id, feed_purposes, period_start, period_end, ): continue records.append((sequence, kind, record)) window = records[offset : offset + limit] has_more = offset + limit < len(records) return LedgerPage( obligations=tuple(item[2] for item in window if item[1] == "obligation"), revisions=tuple(item[2] for item in window if item[1] == "revision"), adjustments=tuple(item[2] for item in window if item[1] == "adjustment"), consumer_statuses=tuple(item[2] for item in window if item[1] == "consumer_status"), total_count=len(records), has_more=has_more, cursor=( encode_cursor({"snapshot": snapshot.snapshot_id, "offset": offset + limit}) if has_more else None ), ) def _resolve( self, kind: str, record_id: str, account_id: str = "", consumer_id: str | None = None ) -> Any: if kind == "obligation": return self._obligations.get(record_id) if kind == "revision": return self._revisions.get(record_id) if kind == "adjustment": return self._adjustments.get(record_id) if kind == "consumer_status": return self._statuses.get((account_id, consumer_id or "", record_id)) # Optional delivery records have their own retained snapshot reader. # Core status must not count or expose them, even in a shared ledger. return None def _in_scope( self, kind: LedgerRecordKind, record: Any, config_filter: set[str] | None, media_buy_filter: set[str] | None, consumer_id: str | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> bool: from adcp.reporting.ledger.status_projection import configuration_selected, period_selected if kind == "consumer_status": # A caller sees only its own statements; another consumer's # operational status is not disclosed. if consumer_id is None or record.consumer_id != consumer_id: return False configuration = self._configurations.get(record.generation_key) return ( configuration is not None and configuration_selected( configuration, delivery_config_ids=tuple(config_filter or ()), media_buy_ids=tuple(media_buy_filter or ()), feed_purposes=feed_purposes or (), ) and period_selected( record.period_start, record.period_end, period_start, period_end ) ) obligation = self._obligation_for(kind, record) if obligation is None or obligation.consumer_id != consumer_id: return False configuration = self._configurations.get(obligation.generation_key) if ( configuration is None or not configuration_selected( configuration, delivery_config_ids=tuple(config_filter or ()), media_buy_ids=tuple(media_buy_filter or ()), feed_purposes=feed_purposes or (), ) or not period_selected( obligation.period.start, obligation.period.end, period_start, period_end ) ): return False if config_filter is not None and obligation.delivery_config_id not in config_filter: return False if media_buy_filter is not None and not media_buy_filter.intersection( obligation.media_buy_ids ): return False return True def _obligation_for( self, kind: LedgerRecordKind, record: Any ) -> ReportingObligationRecord | None: if kind == "obligation": found: ReportingObligationRecord = record return found if kind == "revision": return self._obligations.get(record.reporting_obligation_id) revision = self._revisions.get(record.adjusts_reporting_revision_id) return self._obligations.get(revision.reporting_obligation_id) if revision else None # -- leasing --------------------------------------------------------- def _configuration_lease_eligible(self, configuration: ReportingConfiguration) -> bool: return not configuration.quarantined async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: from datetime import timedelta async with self._lock: moment = _utc(now) # Rank leasable generations exactly the way the SQL store's # `ORDER BY lease_turn, lease_expires_at NULLS FIRST, ...` does: # whichever generation went longest without a turn goes first. # # The turn has to be the *primary* term. The caller's `now` has # already excluded every live lease, so among the survivors the # expiry carries no fairness information -- and preferring unheld # over expired ahead of the turn starves a crashed generation # forever: a peer that is leased and released every turn is always # unheld, so it wins every comparison while the generation whose # worker died stays expired and never closes another period. # Expiry and the generation key only break exact turn ties, so the # order stays total and never depends on physical layout. ranked: list[ tuple[ tuple[int, int, float, str, str, str, int], ReportingConfigurationGenerationKey, ] ] = [] for key in self._configurations: if not self._configuration_lease_eligible(self._configurations[key]): continue turn = self._lease_turns.get(key, 0) held = self._leases.get(key) tail = ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ) if held is None: ranked.append(((turn, 0, 0.0, *tail), key)) elif _utc(held[1]) <= moment: ranked.append(((turn, 1, _utc(held[1]).timestamp(), *tail), key)) if not ranked: return None # The generation key is part of the rank, so the order is total and # both stores make the same choice. Ranking by `min` alone would # fall back to whichever generation this process happened to accept # first, which the SQL store cannot reproduce and no adopter can # observe consistently across a restart or a second worker. key = min(ranked, key=lambda item: item[0])[1] configuration = self._configurations[key] expires = moment + timedelta(seconds=lease_seconds) self._lease_turn += 1 self._lease_turns[key] = self._lease_turn self._leases[key] = (worker_id, expires) return LeasedConfiguration( account_id=configuration.account_id, consumer_id=configuration.consumer_id, delivery_config_id=configuration.delivery_config_id, delivery_config_version=configuration.delivery_config_version, lease_expires_at=expires, ) async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: async with self._lock: key = lease.generation_key held = self._leases.get(key) if held == (worker_id, _utc(lease.lease_expires_at)): del self._leases[key]Process-local reference store. Correct, ordered, and not durable.
clocksupplies change timestamps and the snapshot observation boundary, standing in for the database clock a durable store reads. Override it to place a test's ledger boundary at a deliberate instant.Subclasses
Methods
async def commit_adjustment(self,
adjustment: ReportingAdjustmentRecord) ‑> ReportingAdjustmentRecord-
Expand source code
async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: async with self._mutation(): existing = self._adjustments.get(adjustment.reporting_adjustment_id) if existing is not None: if existing.account_id != adjustment.account_id: raise LedgerConflictError("ADJUSTMENT_UNAVAILABLE", "adjustment is unavailable") if existing != adjustment: raise LedgerConflictError( "ADJUSTMENT_IMMUTABLE", "adjustment content is immutable" ) return existing revision = self._revisions.get(adjustment.adjusts_reporting_revision_id) if revision is None or revision.account_id != adjustment.account_id: raise LedgerConflictError( "REVISION_NOT_FOUND", "an adjustment must name a committed revision" ) if revision.finality != "official": raise LedgerConflictError( "ADJUSTMENT_REQUIRES_OFFICIAL", "adjustments correct an official revision; restate a snapshot with a " "superseding snapshot revision instead", ) obligation = self._obligations[revision.reporting_obligation_id] if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) validate_adjustment_currency( self._obligations[revision.reporting_obligation_id], adjustment ) self._adjustments[adjustment.reporting_adjustment_id] = adjustment self._append( adjustment.account_id, "adjustment", adjustment.reporting_adjustment_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: from adcp.reporting.ledger.notification_events import adjustment_event self._record_notification( adjustment_event( adjustment, self._clock(), consumer_id=self._obligations[revision.reporting_obligation_id].consumer_id, ) ) self._dirty_status( ReportingStatusScope.for_obligation( self._obligations[revision.reporting_obligation_id] ), "adjustment", after=ReportingStatusEvidence("adjustment", adjustment.reporting_adjustment_id), ) return adjustment async def commit_obligation(self,
obligation: ReportingObligationRecord) ‑> ReportingObligationRecord-
Expand source code
async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: async with self._mutation(): key = ( obligation.generation_key, _utc(obligation.period.start).isoformat(), _utc(obligation.period.end).isoformat(), ) existing_id = self._obligation_by_period.get(key) if existing_id is not None: return self._obligations[existing_id] if obligation.reporting_obligation_id in self._obligations: raise LedgerConflictError( "OBLIGATION_IDENTITY_CONFLICT", "the obligation identifier already belongs to a different logical period", ) if obligation.generation_key not in self._configurations: raise LedgerConflictError( "UNKNOWN_CONFIGURATION_GENERATION", "generation is unavailable" ) if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "establish a new owned generation after operator reconciliation", ) require_frozen_currency(obligation.currency) self._obligations[obligation.reporting_obligation_id] = obligation self._obligation_by_period[key] = obligation.reporting_obligation_id self._append( obligation.account_id, "obligation", obligation.reporting_obligation_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: self._dirty_status( ReportingStatusScope.for_obligation(obligation), "obligation", after=ReportingStatusEvidence("obligation", obligation.reporting_obligation_id), ) return obligation async def commit_provisional_observation(self,
observation: ProvisionalObservation,
revision: ReportingRevisionRecord,
rows: Sequence[dict[str, Any]]) ‑> ReportingRevisionRecord-
Expand source code
async def commit_provisional_observation( self, observation: ProvisionalObservation, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: acquisition = observation.acquisition key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) if ( revision.account_id != acquisition.account_id or revision.reporting_obligation_id != acquisition.obligation_id or revision.reporting_revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation identity differs") async with self._mutation(): existing = self._provisional_observations.get(key) if existing is not None: if ( existing.acquisition != acquisition or existing.revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation replay differs") if existing.revision_id not in self._revisions: raise LedgerConflictError( "HISTORY_UNAVAILABLE", "observation revision is missing" ) # A matching observation identity does not make conflicting # publication content an idempotent replay. return await self.commit_revision(revision, rows) retained = self._provisional_acquisitions.get(key) if retained != acquisition: raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition was not reserved") checkpoint = self._restatement_checkpoints.get(acquisition.obligation_id) if checkpoint is not None and checkpoint.next_observation != acquisition.ordinal: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") committed = await self.commit_revision(revision, rows) await self.record_restatement_checkpoint( RestatementCheckpoint( acquisition.account_id, acquisition.obligation_id, observation.checked_at, acquisition.ordinal + 1, observation.provisional_until, ) ) self._provisional_observations[key] = observation return committed async def commit_revision(self,
revision: ReportingRevisionRecord,
rows: Sequence[dict[str, Any]]) ‑> ReportingRevisionRecord-
Expand source code
async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]] ) -> ReportingRevisionRecord: rows = tuple(deepcopy(row) for row in rows) # Checked first, exactly as PgReportingLedgerStore does: a caller whose # rows do not match its own declared count must get the same code from # both stores, not whichever invariant that store happens to reach. if revision.row_count != len(rows): raise LedgerConflictError( "ROW_COUNT_MISMATCH", f"revision declares {revision.row_count} rows but {len(rows)} were supplied", ) validate_managed_revision_rows(revision, rows) async with self._mutation(): identity = _revision_identity(revision) existing = self._revisions.get(revision.reporting_revision_id) if existing is not None: if existing.account_id != revision.account_id: raise LedgerConflictError( "REVISION_NOT_FOUND", "no such revision for this account" ) if self._revision_identity[revision.reporting_revision_id] != identity: raise LedgerConflictError( "REVISION_IMMUTABLE", f"revision {revision.reporting_revision_id} already exists with " "different content; a restatement is a new revision", ) return existing obligation = self._obligations.get(revision.reporting_obligation_id) if obligation is None or obligation.account_id != revision.account_id: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a revision must attach to an obligation committed at the period close", ) if self._configurations[obligation.generation_key].quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) validate_revision_currency(obligation, revision, rows) siblings = [ item for item in self._revisions.values() if item.reporting_obligation_id == revision.reporting_obligation_id ] if revision.finality == "official" and any( item.finality == "official" for item in siblings ): raise LedgerConflictError( "OFFICIAL_REVISION_TERMINAL", "an official revision already exists for this obligation; publish a later " "source correction as an adjustment", ) if revision.supersedes_reporting_revision_id: self._require_current_leaf(revision, siblings) self._revisions[revision.reporting_revision_id] = revision self._revision_identity[revision.reporting_revision_id] = identity self._rows[revision.reporting_revision_id] = tuple(dict(row) for row in rows) self._append( revision.account_id, "revision", revision.reporting_revision_id, consumer_id=obligation.consumer_id, ) if self._notification_state is not None: from adcp.reporting.ledger.notification_events import revision_event self._record_notification( revision_event(revision, self._clock(), consumer_id=obligation.consumer_id) ) self._dirty_status( ReportingStatusScope.for_obligation(obligation), "revision", after=ReportingStatusEvidence( "revision", revision.reporting_revision_id, readable=revision.readable, supersedes_id=revision.supersedes_reporting_revision_id, ), ) return revision async def create_schema(self) ‑> None-
Expand source code
async def create_schema(self) -> None: return None async def ensure_issue_opened(self,
*,
issue_key: str,
account_id: str,
consumer_id: str | None,
observed_at: datetime,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle-
Expand source code
async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._mutation(): key = (account_id, issue_key) live = self._issues.get(key) if live is not None: if live.consumer_id != consumer_id: raise ReportingNotificationError("invalid_status_scope") previous_scope = self._issue_status_scopes.get((account_id, live.issue_id)) if status_scope is not None: validate_scope_refinement(previous_scope, status_scope) else: status_scope = previous_scope if live is not None and live.live: self._dirty_issue(live, status_scope, live) return live generation = self._issue_generations.get(key, 0) + 1 record = ReportingIssueLifecycle( issue_key=issue_key, issue_id=issue_id_for_occurrence(issue_key, generation), account_id=account_id, consumer_id=consumer_id, opened_at=_utc(observed_at), issue_state="open", generation=generation, ) self._resolve_issue_scope( account_id=account_id, consumer_id=consumer_id, issue_id=record.issue_id, status_scope=status_scope, ) self._issue_generations[key] = generation self._issues[key] = record self._dirty_issue(record, status_scope) return record async def find_obligation(self,
*,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
period_start: datetime,
period_end: datetime) ‑> ReportingObligationRecord | None-
Expand source code
async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: key = ( ReportingConfigurationGenerationKey( account_id=account_id, consumer_id=consumer_id, delivery_config_id=delivery_config_id, delivery_config_version=delivery_config_version, ), _utc(period_start).isoformat(), _utc(period_end).isoformat(), ) found = self._obligation_by_period.get(key) return ( await self.get_obligation(account_id=account_id, reporting_obligation_id=found) if found else None ) async def get_issue(self, *, issue_key: str, account_id: str) ‑> ReportingIssueLifecycle | None-
Expand source code
async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: async with self._lock: # Live occurrences only, matching the Protocol docstring and the # Postgres store. Returning a retired row here would let the # projection re-emit a resolved issue's opened_at. live = self._issues.get((account_id, issue_key)) return live if live is not None and live.live else None async def get_obligation(self, *, account_id: str, reporting_obligation_id: str) ‑> ReportingObligationRecord | None-
Expand source code
async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: found = self._obligations.get(reporting_obligation_id) return found if found and found.account_id == account_id else None async def get_provisional_acquisition(self, *, account_id: str, reporting_obligation_id: str, ordinal: int) ‑> ProvisionalAcquisition | None-
Expand source code
async def get_provisional_acquisition( self, *, account_id: str, reporting_obligation_id: str, ordinal: int ) -> ProvisionalAcquisition | None: return self._provisional_acquisitions.get((account_id, reporting_obligation_id, ordinal)) async def get_provisional_observation(self, *, account_id: str, reporting_obligation_id: str) ‑> ProvisionalObservation | None-
Expand source code
async def get_provisional_observation( self, *, account_id: str, reporting_obligation_id: str ) -> ProvisionalObservation | None: observations = [ value for (account, obligation, _), value in self._provisional_observations.items() if account == account_id and obligation == reporting_obligation_id ] return max(observations, key=lambda item: item.acquisition.ordinal, default=None) async def get_restatement_checkpoint(self, *, account_id: str, reporting_obligation_id: str) ‑> RestatementCheckpoint | None-
Expand source code
async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: checkpoint = self._restatement_checkpoints.get(reporting_obligation_id) return checkpoint if checkpoint and checkpoint.account_id == account_id else None async def get_retry_schedule(self, *, scope_key: str) ‑> RetryScheduleEntry | None-
Expand source code
async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: return self._retry_schedules.get(scope_key) async def get_revision(self, *, account_id: str, reporting_revision_id: str) ‑> ReportingRevisionRecord | None-
Expand source code
async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: found = self._revisions.get(reporting_revision_id) return found if found and found.account_id == account_id else None async def lease_period_close(self, *, worker_id: str, now: datetime, lease_seconds: float) ‑> LeasedConfiguration | None-
Expand source code
async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: from datetime import timedelta async with self._lock: moment = _utc(now) # Rank leasable generations exactly the way the SQL store's # `ORDER BY lease_turn, lease_expires_at NULLS FIRST, ...` does: # whichever generation went longest without a turn goes first. # # The turn has to be the *primary* term. The caller's `now` has # already excluded every live lease, so among the survivors the # expiry carries no fairness information -- and preferring unheld # over expired ahead of the turn starves a crashed generation # forever: a peer that is leased and released every turn is always # unheld, so it wins every comparison while the generation whose # worker died stays expired and never closes another period. # Expiry and the generation key only break exact turn ties, so the # order stays total and never depends on physical layout. ranked: list[ tuple[ tuple[int, int, float, str, str, str, int], ReportingConfigurationGenerationKey, ] ] = [] for key in self._configurations: if not self._configuration_lease_eligible(self._configurations[key]): continue turn = self._lease_turns.get(key, 0) held = self._leases.get(key) tail = ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ) if held is None: ranked.append(((turn, 0, 0.0, *tail), key)) elif _utc(held[1]) <= moment: ranked.append(((turn, 1, _utc(held[1]).timestamp(), *tail), key)) if not ranked: return None # The generation key is part of the rank, so the order is total and # both stores make the same choice. Ranking by `min` alone would # fall back to whichever generation this process happened to accept # first, which the SQL store cannot reproduce and no adopter can # observe consistently across a restart or a second worker. key = min(ranked, key=lambda item: item[0])[1] configuration = self._configurations[key] expires = moment + timedelta(seconds=lease_seconds) self._lease_turn += 1 self._lease_turns[key] = self._lease_turn self._leases[key] = (worker_id, expires) return LeasedConfiguration( account_id=configuration.account_id, consumer_id=configuration.consumer_id, delivery_config_id=configuration.delivery_config_id, delivery_config_version=configuration.delivery_config_version, lease_expires_at=expires, ) async def list_adjustments(self, *, account_id: str, reporting_revision_ids: Sequence[str]) ‑> tuple[ReportingAdjustmentRecord, ...]-
Expand source code
async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: wanted = set(reporting_revision_ids) return tuple( item for item in self._adjustments.values() if item.account_id == account_id and item.adjusts_reporting_revision_id in wanted ) async def list_all_configurations(self) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: return tuple( sorted( (c for c in self._configurations.values() if not c.quarantined), key=lambda item: ( item.account_id, item.consumer_id, item.delivery_config_id, item.delivery_config_version, ), ) ) async def list_configurations(self,
*,
caller: ReportingCaller,
delivery_config_ids: Sequence[str] | None = None) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: wanted = set(delivery_config_ids) if delivery_config_ids else None return tuple( configuration for configuration in self._configurations.values() if configuration.account_id == caller.account_id and configuration.consumer_id == caller.consumer_id and (wanted is None or configuration.delivery_config_id in wanted) ) async def list_consumer_statuses(self,
*,
account_id: str,
consumer_id: str,
reporting_obligation_ids: Sequence[str] | None = None) ‑> tuple[ConsumerStatusRecord, ...]-
Expand source code
async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: wanted = set(reporting_obligation_ids) if reporting_obligation_ids is not None else None result = [] for item in self._statuses.values(): if item.account_id != account_id or item.consumer_id != consumer_id: continue if wanted is not None and not self._matches_obligations(item, wanted): continue result.append(item) return tuple(result) async def list_revisions(self, *, account_id: str, reporting_obligation_id: str) ‑> tuple[ReportingRevisionRecord, ...]-
Expand source code
async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: return tuple( item for item in self._revisions.values() if item.account_id == account_id and item.reporting_obligation_id == reporting_obligation_id ) async def open_snapshot(self,
*,
caller: ReportingCaller,
filters_fingerprint: str) ‑> LedgerSnapshot-
Expand source code
async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: account_id = caller.account_id async with self._lock: as_of = _utc(self._clock()) max_sequence = max( ( sequence for sequence, account, kind, record_id, _ in self._changes if account == account_id and self._change_owners[sequence] == caller.consumer_id ), default=0, ) return LedgerSnapshot( snapshot_id="rpls_" + _fingerprint([account_id, caller.consumer_id, filters_fingerprint, max_sequence])[ :32 ], account_id=account_id, consumer_id=caller.consumer_id, ledger_as_of=as_of, max_sequence=max_sequence, ) async def put_configuration(self,
configuration: ReportingConfiguration) ‑> None-
Expand source code
async def put_configuration(self, configuration: ReportingConfiguration) -> None: reject_reserved_authoritative_party(configuration) async with self._mutation(): key = configuration.generation_key existing = self._configurations.get(key) if existing is not None and existing.quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "operator reconciliation is required" ) if existing is not None and _fingerprint(_config_payload(existing)) != _fingerprint( _config_payload(configuration) ): raise LedgerConflictError( "CONFIGURATION_GENERATION_IMMUTABLE", f"configuration {key.delivery_config_id}@{key.delivery_config_version} " "already exists with different content for this account; " "publish a new version instead of editing a retained generation", ) self._configurations[key] = configuration changed = existing is None or configuration_lifecycle( existing ) != configuration_lifecycle(configuration) if changed and self._notification_state is not None: self._dirty_status( ReportingStatusScope(configuration.account_id, configuration.generation_key), "configuration", before=configuration_evidence(existing) if existing is not None else None, after=configuration_evidence(configuration), ) async def read_page(self,
*,
snapshot: LedgerSnapshot,
consumer_id: str | None,
delivery_config_ids: Sequence[str] | None,
media_buy_ids: Sequence[str] | None,
offset: int,
limit: int,
changes_after_sequence: int | None,
feed_purposes: Sequence[str] | None = None,
period_start: datetime | None = None,
period_end: datetime | None = None) ‑> LedgerPage-
Expand source code
async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> LedgerPage: if consumer_id not in {None, snapshot.consumer_id}: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "snapshot belongs to another caller" ) consumer_id = snapshot.consumer_id lower = changes_after_sequence or 0 selected = [ item for item in self._changes if item[1] == snapshot.account_id and self._change_owners[item[0]] == consumer_id and lower < item[0] <= snapshot.max_sequence ] config_filter = set(delivery_config_ids) if delivery_config_ids else None media_buy_filter = set(media_buy_ids) if media_buy_ids else None records: list[tuple[int, LedgerRecordKind, Any]] = [] for sequence, _account, kind, record_id, _committed in sorted(selected): record = self._resolve(kind, record_id, snapshot.account_id, consumer_id) if record is None or record.account_id != snapshot.account_id: continue if not self._in_scope( kind, record, config_filter, media_buy_filter, consumer_id, feed_purposes, period_start, period_end, ): continue records.append((sequence, kind, record)) window = records[offset : offset + limit] has_more = offset + limit < len(records) return LedgerPage( obligations=tuple(item[2] for item in window if item[1] == "obligation"), revisions=tuple(item[2] for item in window if item[1] == "revision"), adjustments=tuple(item[2] for item in window if item[1] == "adjustment"), consumer_statuses=tuple(item[2] for item in window if item[1] == "consumer_status"), total_count=len(records), has_more=has_more, cursor=( encode_cursor({"snapshot": snapshot.snapshot_id, "offset": offset + limit}) if has_more else None ), ) async def read_revision_rows(self,
*,
account_id: str,
reporting_revision_id: str,
cursor: str | None = None,
limit: int = 500) ‑> ReportingRowPage-
Expand source code
async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: revision = await self.get_revision( account_id=account_id, reporting_revision_id=reporting_revision_id ) if revision is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") rows = self._rows.get(reporting_revision_id, ()) offset = revision_row_offset(cursor, reporting_revision_id, limit) window = rows[offset : offset + limit] has_more = offset + limit < len(rows) return ReportingRowPage( reporting_revision_id=reporting_revision_id, rows=tuple(deepcopy(row) for row in window), total_count=len(rows), has_more=has_more, cursor=( encode_cursor( {"ownership": 2, "revision": reporting_revision_id, "offset": offset + limit} ) if has_more else None ), ) async def read_status_snapshot(self,
*,
caller: ReportingCaller) ‑> ReportingStatusSnapshot-
Expand source code
async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.status_snapshot import settle_memory_snapshot async with self._mutation(): from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.materializer.capture import private_snapshot return private_snapshot( settle_memory_snapshot(self, caller.account_id), ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id), ) async def record_consumer_status(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: from adcp.reporting.evidence import consumer_reference consumer_reference(status.consumer_id) async with self._mutation(): identity = _consumer_status_identity(status) existing = self._replay(status) if existing is not None: return existing, False from adcp.reporting.ledger.status_snapshot import ( memory_snapshot, validate_status_evidence, ) validate_status_evidence(status, memory_snapshot(self, status.account_id)) leaf = self._current_status_leaf(status.chain_key) if status.supersedes_reporting_status_id: if ( leaf is None or leaf.reporting_status_id != status.supersedes_reporting_status_id ): raise LedgerConflictError( "STATUS_SUPERSEDES_STALE", "supersedes_reporting_status_id must name this chain's current leaf; " "a stale pointer would let a successful retry erase a recorded outage", ) self._statuses[(leaf.account_id, leaf.consumer_id, leaf.reporting_status_id)] = ( replace(leaf, superseded=True) ) elif leaf is not None: raise LedgerConflictError( "STATUS_SUPERSEDES_REQUIRED", "this chain already has a current statement; a new statement must " "explicitly supersede it", ) key = (status.account_id, status.consumer_id, status.reporting_status_id) self._statuses[key] = status self._status_identity[key] = identity self._append( status.account_id, "consumer_status", status.reporting_status_id, consumer_id=status.consumer_id, ) from adcp.reporting.ledger.status_snapshot import settle_memory_snapshot settle_memory_snapshot(self, status.account_id) if self._notification_state is not None: self._dirty_status( ReportingStatusScope( status.account_id, status.generation_key, status.reporting_obligation_id, status.consumer_id, ), "consumer_status", ( ReportingStatusEvidence("consumer_status", leaf.reporting_status_id) if leaf is not None else None ), ReportingStatusEvidence( "consumer_status", status.reporting_status_id, supersedes_id=status.supersedes_reporting_status_id, ), ) return status, True async def record_consumer_status_with_lifecycle(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: return await self.record_consumer_status(status) async def record_restatement_checkpoint(self,
checkpoint: RestatementCheckpoint) ‑> RestatementCheckpoint-
Expand source code
async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: async with self._mutation(): obligation = self._obligations.get(checkpoint.reporting_obligation_id) if obligation is None or obligation.account_id != checkpoint.account_id: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) existing = self._restatement_checkpoints.get(checkpoint.reporting_obligation_id) if existing is not None: if checkpoint.next_observation < existing.next_observation: return existing if checkpoint.next_observation == existing.next_observation and _utc( checkpoint.checked_at ) <= _utc(existing.checked_at): return existing self._restatement_checkpoints[checkpoint.reporting_obligation_id] = checkpoint return checkpoint async def record_retry_schedule(self,
entry: RetryScheduleEntry) ‑> RetryScheduleEntry-
Expand source code
async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: async with self._mutation(): existing = self._retry_schedules.get(entry.scope_key) if existing is not None: assert entry.recorded_at is not None and existing.recorded_at is not None if _utc(entry.recorded_at) < _utc(existing.recorded_at): return existing if entry.attempt > 0 and not entry.blocked: if existing.blocked and not entry.replayed: return existing if entry.attempt < existing.attempt and not entry.replayed: return existing if not entry.replayed and not existing.blocked: entry = replace( entry, retry_not_before=max( entry.retry_not_before, existing.retry_not_before, key=_utc ), ) stored = replace(entry, replayed=False) self._retry_schedules[entry.scope_key] = stored return stored async def release_period_close(self,
lease: LeasedConfiguration,
*,
worker_id: str) ‑> None-
Expand source code
async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: async with self._lock: key = lease.generation_key held = self._leases.get(key) if held == (worker_id, _utc(lease.lease_expires_at)): del self._leases[key] async def reserve_provisional_acquisition(self, acquisition: ProvisionalAcquisition) ‑> ProvisionalAcquisition-
Expand source code
async def reserve_provisional_acquisition( self, acquisition: ProvisionalAcquisition ) -> ProvisionalAcquisition: # Canonical round-trip also detaches all request collections. acquisition = ProvisionalAcquisition.from_wire(acquisition.to_wire()) key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) async with self._mutation(): existing = self._provisional_acquisitions.get(key) if existing is not None: return existing obligation = self._obligations.get(acquisition.obligation_id) if obligation is None or obligation.account_id != acquisition.account_id: raise LedgerConflictError("OBLIGATION_NOT_FOUND", "unknown observation obligation") if not acquisition.binds(obligation): raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition generation differs") if any( item.account_id == acquisition.account_id and item.execution_key == acquisition.execution_key for item in self._provisional_acquisitions.values() ): raise LedgerConflictError( "OBSERVATION_CONFLICT", "execution key is already reserved" ) checkpoint = self._restatement_checkpoints.get(acquisition.obligation_id) expected = ( checkpoint.next_observation if checkpoint else len( await self.list_revisions( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) ) ) if acquisition.ordinal != expected: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") self._provisional_acquisitions[key] = acquisition return acquisition async def resolve_consumer_status_replay(self,
status: ConsumerStatusRecord) ‑> ConsumerStatusRecord | None-
Expand source code
async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: async with self._lock: return self._replay(status) async def retire_issue(self,
*,
issue_key: str,
account_id: str,
at: datetime,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle | None-
Expand source code
async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle | None: async with self._mutation(): key = (account_id, issue_key) live = self._issues.get(key) if live is None or not live.live or not issue_is_retirable(live.issue_state): # Convergent: nothing live, or already waived. A waived issue # is retired from the projection by agreement, and overwriting # that readable act with `resolved` is an edge the forward-only # lifecycle forbids -- enforced here rather than left to each # caller to remember. return None check_issue_state_transition(live.issue_state, "resolved") retired = replace(live, issue_state="resolved", retired_at=_utc(at)) self._resolve_issue_scope( account_id=account_id, consumer_id=live.consumer_id, issue_id=live.issue_id, status_scope=status_scope, ) self._issues[key] = retired self._dirty_issue(retired, status_scope, live) return retired async def set_issue_state(self,
*,
issue_key: str,
account_id: str,
state: "Literal['acknowledged', 'waived']",
at: datetime,
external_ref: str | None = None,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle-
Expand source code
async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._mutation(): if state not in {"acknowledged", "waived"}: raise LedgerConflictError( "ISSUE_STATE_NOT_OPERATOR_SETTABLE", f"issue_state {state!r} is not settable by an operator. 'resolved' is " "reachable only when the condition actually clears -- the projection " "retires it -- because a seller must not retire a mismatch out of a " "degraded projection while the statement that caused it is still the " "consumer's current leaf", ) key = (account_id, issue_key) live = self._issues.get(key) if live is None or not live.live: raise LedgerConflictError( "ISSUE_NOT_OPEN", f"no open issue {issue_key!r} for this account; a retired issue cannot be " "reopened, and a recurrence gets a new occurrence", ) check_issue_state_transition(live.issue_state, state) if state == "waived" and live.issue_state != "waived": from adcp.reporting.ledger.status_projection import bind_mismatch_waiver from adcp.reporting.ledger.status_snapshot import memory_snapshot live = bind_mismatch_waiver(memory_snapshot(self, account_id), live) updated = replace( live, issue_state=state, external_ref=external_ref or live.external_ref, # Set on the way into a retired state and never cleared. retired_at=( ( live.retired_at if live.waived_reporting_status_id is not None and live.retired_at is not None else _utc(at) ) if state == "waived" else live.retired_at ), ) self._resolve_issue_scope( account_id=account_id, consumer_id=live.consumer_id, issue_id=live.issue_id, status_scope=status_scope, ) self._issues[key] = updated # Derive the no-op from the resulting record rather than predicting # it: an idempotent re-acknowledge changes nothing and enqueues # nothing, while anything that does move retained evidence stays # reconstructable for the projector. self._dirty_issue(updated, status_scope, live) return updated async def set_revision_readable(self, *, account_id: str, reporting_revision_id: str, readable: bool) ‑> None-
Expand source code
async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: async with self._mutation(): existing = self._revisions.get(reporting_revision_id) if existing is None or existing.account_id != account_id: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") if existing.readable == readable: return self._revisions[reporting_revision_id] = replace(existing, readable=readable) if self._notification_state is not None: obligation = self._obligations[existing.reporting_obligation_id] self._dirty_status( ReportingStatusScope.for_obligation(obligation), "readability", ReportingStatusEvidence( "revision", reporting_revision_id, readable=existing.readable ), ReportingStatusEvidence("revision", reporting_revision_id, readable=readable), ) async def transaction(self) ‑> AsyncIterator[InMemoryReportingLedgerStore]-
Expand source code
@asynccontextmanager async def transaction(self) -> AsyncIterator[InMemoryReportingLedgerStore]: """Group several source mutations into one atomic, replayable boundary.""" async with self._mutation(): yield selfGroup several source mutations into one atomic, replayable boundary.
class InMemoryReportingReconciliationStore (*, clock: Callable[[], datetime] | None = None, notifications: bool = False)-
Expand source code
class InMemoryReportingReconciliationStore(InMemoryReportingLedgerStore, _ReconciliationOperations): """Optional reference extension. No destination/receipt services at construction.""" _delivery_records: list[tuple[int, ReportingDeliveryPrincipal, ReportingDeliveryRecord]] async def _commit(self, record: RecordT) -> tuple[RecordT, bool]: candidate = cast(RecordT, decode_record(payload(record))) async with self._mutation(): return self._commit_record_unlocked(candidate) def _commit_record_unlocked( self, record: RecordT, *, notify: bool = True, dirty: bool = True ) -> tuple[RecordT, bool]: """Caller owns the memory mutation; no lock or callback is acquired here. ``notify`` and ``dirty`` are independent: suppressing a readiness event must never also drop the projection work that an ordinary public write has always produced. """ candidate = decode_record(payload(record)) who = principal(candidate) records = tuple(item.record for item in self._caller_changes(who)) existing = replay(candidate, records) if existing is not None: return cast(RecordT, existing), False context = self._delivery_context(candidate) stored = validate_transition(candidate, records, context, self._clock()) self._append_reconciliation_change(stored) if (notify or dirty) and self._notification_state is not None: from adcp.reporting.ledger.notification_events import ( delivery_dirty, materialization_event, ) if notify: event = materialization_event( stored, records, context.obligation, context.revision, context.configuration, self._clock(), ) if event is not None: self._record_notification(event) if dirty: scope, reason, evidence = delivery_dirty(stored, context.obligation) self._dirty_status(scope, reason, after=evidence) return cast(RecordT, stored), True def _append_reconciliation_change(self, record: ReportingDeliveryRecord) -> None: who = principal(record) retained = self._retained_delivery_records() sequence = sum(owner == who for _, owner, _ in retained) + 1 # One assignment publishes the record, its feed row, and its local head. # Core's sequence and change list never participate in this transaction. self._delivery_records = [*retained, (sequence, who, record)] def _retained_delivery_records( self, ) -> list[tuple[int, ReportingDeliveryPrincipal, ReportingDeliveryRecord]]: # Lazily allocated so construction keeps Core's exact component surface. if not hasattr(self, "_delivery_records"): self._delivery_records = [] return self._delivery_records def _caller_changes( self, caller: ReportingDeliveryPrincipal ) -> tuple[ReportingReconciliationChange, ...]: retained = [ item for item in self._retained_delivery_records() if item[1] == caller or principal(item[2]) == caller ] if any( type(sequence) is not int or sequence != ordinal or owner != caller or principal(record) != caller for ordinal, (sequence, owner, record) in enumerate(retained, start=1) ): fail("REPORTING_HISTORY_CORRUPT") return tuple( ReportingReconciliationChange(sequence, record) for sequence, _, record in retained ) def _delivery_context(self, record: ReportingDeliveryRecord) -> DeliveryContext: if isinstance(record, ReportingDestinationBinding): return DeliveryContext(configuration=self._configurations.get(record.generation_key)) obligation = self._obligations.get(record.scope.reporting_obligation_id) revision_id = getattr(record, "reporting_revision_id", None) if isinstance(record, ReportingAdjustmentReceiptRecord): revision_id = record.adjusts_reporting_revision_id revision = self._revisions.get(revision_id) if revision_id is not None else None return DeliveryContext( configuration=self._configurations.get(record.scope.generation_key), obligation=obligation, revision=revision, adjustment=( self._adjustments.get(record.reporting_adjustment_id) if isinstance(record, ReportingAdjustmentReceiptRecord) else None ), ) async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: requested = boundary is not None async with self._lock: changes = self._caller_changes(caller) if boundary is None: boundary = change_boundary(caller, len(changes), self._clock()) if ( type(boundary) is not ReportingReconciliationSnapshotToken or boundary.caller != caller or boundary.min_sequence != 0 or boundary.filters != ReportingReconciliationFilter() ): unavailable() validate_boundary(caller, boundary, len(changes)) if boundary.total_count != boundary.max_sequence: # A caller-presented boundary that disagrees with the retained feed # is a stale or hand-built token, never evidence that storage is # corrupt. Only a boundary this store opened can accuse itself. _boundary_unavailable(requested) return ReportingReconciliationSnapshot( caller, boundary, tuple(item.record for item in changes if item.sequence <= boundary.max_sequence), ) async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: after, boundary, last_key = read_position(caller, changes_after, cursor, limit, filters) continued = boundary is not None async with self._lock: records = self._caller_changes(caller) if boundary is None: boundary = change_boundary( caller, len(records), self._clock(), after=after, total_count=sum( item.sequence > after and filters.matches(item.record) for item in records ), filters=filters, ) validate_boundary(caller, boundary, len(records)) if last_key is not None and not any( item.sequence == after and change_id(item.record) == last_key and filters.matches(item.record) for item in records ): raise LedgerConflictError("INVALID_CHECKPOINT", "reconciliation key is unavailable") changes = tuple( item for item in records if boundary.min_sequence < item.sequence <= boundary.max_sequence and filters.matches(item.record) ) if len(changes) != boundary.total_count: _boundary_unavailable(continued) return change_page( caller, boundary, after, tuple(item for item in changes if item.sequence > after), limit, )Optional reference extension. No destination/receipt services at construction.
Ancestors
- InMemoryReportingLedgerStore
- adcp.reporting.ledger.delivery._ReconciliationOperations
Subclasses
Methods
async def read_reconciliation_changes(self,
*,
caller: ReportingDeliveryPrincipal,
changes_after: ReportingReconciliationCheckpoint | None = None,
cursor: ReportingReconciliationCursor | None = None,
limit: int = 100,
filters: ReportingReconciliationFilter = ReportingReconciliationFilter(record_kinds=(), reporting_obligation_id=None)) ‑> ReportingReconciliationPage-
Expand source code
async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: after, boundary, last_key = read_position(caller, changes_after, cursor, limit, filters) continued = boundary is not None async with self._lock: records = self._caller_changes(caller) if boundary is None: boundary = change_boundary( caller, len(records), self._clock(), after=after, total_count=sum( item.sequence > after and filters.matches(item.record) for item in records ), filters=filters, ) validate_boundary(caller, boundary, len(records)) if last_key is not None and not any( item.sequence == after and change_id(item.record) == last_key and filters.matches(item.record) for item in records ): raise LedgerConflictError("INVALID_CHECKPOINT", "reconciliation key is unavailable") changes = tuple( item for item in records if boundary.min_sequence < item.sequence <= boundary.max_sequence and filters.matches(item.record) ) if len(changes) != boundary.total_count: _boundary_unavailable(continued) return change_page( caller, boundary, after, tuple(item for item in changes if item.sequence > after), limit, ) async def read_reconciliation_snapshot(self,
*,
caller: ReportingDeliveryPrincipal,
boundary: ReportingReconciliationSnapshotToken | None = None) ‑> ReportingReconciliationSnapshot-
Expand source code
async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: requested = boundary is not None async with self._lock: changes = self._caller_changes(caller) if boundary is None: boundary = change_boundary(caller, len(changes), self._clock()) if ( type(boundary) is not ReportingReconciliationSnapshotToken or boundary.caller != caller or boundary.min_sequence != 0 or boundary.filters != ReportingReconciliationFilter() ): unavailable() validate_boundary(caller, boundary, len(changes)) if boundary.total_count != boundary.max_sequence: # A caller-presented boundary that disagrees with the retained feed # is a stale or hand-built token, never evidence that storage is # corrupt. Only a boundary this store opened can accuse itself. _boundary_unavailable(requested) return ReportingReconciliationSnapshot( caller, boundary, tuple(item.record for item in changes if item.sequence <= boundary.max_sequence), )
Inherited members
class LeasedConfiguration (account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
lease_expires_at: datetime)-
Expand source code
@dataclass(frozen=True) class LeasedConfiguration: """One configuration generation leased for period-close work. Carries its account so the producer never has to invert the lease back into a tenant -- an inversion that is easy to get subtly wrong when two accounts reuse a ``delivery_config_id``. """ account_id: str consumer_id: str delivery_config_id: str delivery_config_version: int lease_expires_at: datetime @property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, )One configuration generation leased for period-close work.
Carries its account so the producer never has to invert the lease back into a tenant – an inversion that is easy to get subtly wrong when two accounts reuse a
delivery_config_id.Instance variables
var account_id : strvar consumer_id : strvar delivery_config_id : strvar delivery_config_version : intprop generation_key : ReportingConfigurationGenerationKey-
Expand source code
@property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) var lease_expires_at : datetime.datetime
class LedgerChange (sequence: int,
account_id: str,
record_kind: LedgerRecordKind,
record_id: str,
committed_at: datetime)-
Expand source code
@dataclass(frozen=True) class LedgerChange: """One append to the per-account change feed. The feed is what makes ``changes_after`` exact. Every immutable record -- obligation, revision, adjustment, consumer status -- appends here in the same transaction that writes it, so a consumer that persists a checkpoint and replays from it cannot miss a record or see one twice under a different identity. """ sequence: int account_id: str record_kind: LedgerRecordKind record_id: str committed_at: datetimeOne append to the per-account change feed.
The feed is what makes
changes_afterexact. Every immutable record – obligation, revision, adjustment, consumer status – appends here in the same transaction that writes it, so a consumer that persists a checkpoint and replays from it cannot miss a record or see one twice under a different identity.Instance variables
var account_id : strvar committed_at : datetime.datetimevar record_id : strvar record_kind : Literal['obligation', 'revision', 'adjustment', 'adcp.reporting.ledger.consumer_status']var sequence : int
class LedgerConflictError (code: str, message: str)-
Expand source code
class LedgerConflictError(RuntimeError): """A write would violate an invariant that makes the ledger evidence. Carries a stable ``code`` so a handler can map it to a wire error without matching on prose. """ def __init__(self, code: str, message: str) -> None: super().__init__(message) self.code = codeA write would violate an invariant that makes the ledger evidence.
Carries a stable
codeso a handler can map it to a wire error without matching on prose.Ancestors
- builtins.RuntimeError
- builtins.Exception
- builtins.BaseException
class LedgerPage (obligations: tuple[ReportingObligationRecord, ...],
revisions: tuple[ReportingRevisionRecord, ...],
adjustments: tuple[ReportingAdjustmentRecord, ...],
consumer_statuses: tuple[ConsumerStatusRecord, ...],
total_count: int,
has_more: bool,
cursor: str | None)-
Expand source code
@dataclass(frozen=True) class LedgerPage: """One page of the flat union of ledger records. Pagination is over the *flat union* of obligations, revisions, adjustments and consumer statuses rather than nesting history under each obligation. Nesting looks friendlier and is unbounded: one obligation with ten thousand snapshot restatements would make a single "page" unpageable. """ obligations: tuple[ReportingObligationRecord, ...] revisions: tuple[ReportingRevisionRecord, ...] adjustments: tuple[ReportingAdjustmentRecord, ...] consumer_statuses: tuple[ConsumerStatusRecord, ...] total_count: int has_more: bool cursor: str | NoneOne page of the flat union of ledger records.
Pagination is over the flat union of obligations, revisions, adjustments and consumer statuses rather than nesting history under each obligation. Nesting looks friendlier and is unbounded: one obligation with ten thousand snapshot restatements would make a single "page" unpageable.
Instance variables
var adjustments : tuple[ReportingAdjustmentRecord, ...]var consumer_statuses : tuple[ConsumerStatusRecord, ...]var cursor : str | Nonevar has_more : boolvar obligations : tuple[ReportingObligationRecord, ...]var revisions : tuple[ReportingRevisionRecord, ...]var total_count : int
class LedgerSnapshot (snapshot_id: str,
account_id: str,
consumer_id: str,
ledger_as_of: datetime,
max_sequence: int)-
Expand source code
@dataclass(frozen=True) class LedgerSnapshot: """A consistent read boundary over one account's ledger. Every page reached from one cursor returns the same ``snapshot_id`` and ``ledger_as_of``. Records committed *during* pagination appear after the returned ``changes_checkpoint`` on the next read, never inside the current snapshot -- otherwise a consumer's record count would not match what it received, and it could not tell a lost record from a late one. """ snapshot_id: str account_id: str consumer_id: str ledger_as_of: datetime max_sequence: intA consistent read boundary over one account's ledger.
Every page reached from one cursor returns the same
snapshot_idandledger_as_of. Records committed during pagination appear after the returnedchanges_checkpointon the next read, never inside the current snapshot – otherwise a consumer's record count would not match what it received, and it could not tell a lost record from a late one.Instance variables
var account_id : strvar consumer_id : strvar ledger_as_of : datetime.datetimevar max_sequence : intvar snapshot_id : str
class ObligationProjection (health: ReportingHealth,
production_status: ReportingProductionStatus,
issues: tuple[ReportingIssue, ...],
satisfied: bool,
current_revision: ReportingRevisionRecord | None)-
Expand source code
@dataclass(frozen=True) class ObligationProjection: """One obligation's derived status at a snapshot boundary.""" health: ReportingHealth production_status: ReportingProductionStatus issues: tuple[ReportingIssue, ...] satisfied: bool current_revision: ReportingRevisionRecord | NoneOne obligation's derived status at a snapshot boundary.
Instance variables
var current_revision : ReportingRevisionRecord | Nonevar health : Literal['healthy', 'waiting', 'delayed', 'action_required', 'complete']var issues : tuple[ReportingIssue, ...]var production_status : Literal['not_due', 'pending', 'published', 'failed']var satisfied : bool
class PgReportingLedgerStore (*,
pool: AsyncConnectionPool,
clock: Callable[[], datetime] | None = None,
notifications: bool = False)-
Expand source code
class PgReportingLedgerStore: """Durable reporting ledger over a caller-supplied connection pool.""" is_durable: ClassVar[bool] = True def __init__( self, *, pool: AsyncConnectionPool, clock: Callable[[], datetime] | None = None, notifications: bool = False, ) -> None: if not PG_AVAILABLE: raise ImportError(_INSTALL_HINT) self._pool = pool # Change timestamps and the snapshot boundary default to the *database* clock, # which is what makes two readers of one snapshot agree even across # application hosts with drifting clocks -- do not override it in # production for that reason. Overriding is for replay, backfill, and # tests that need to stand at a specific instant relative to seeded # evidence rather than wherever wall-clock time happens to fall. self._clock = clock self._notifications_enabled = notifications self._period_close_sample: tuple[int, datetime | None, str, str, str, int] | None = None @asynccontextmanager async def _connection(self) -> AsyncIterator[Any]: bound = _BOUND_CONNECTION.get() if bound is not None and bound[:2] == (self._pool, asyncio.current_task()): yield bound[2] else: async with self._pool.connection() as connection: yield connection @asynccontextmanager async def transaction(self) -> AsyncIterator[PgReportingLedgerStore]: """Group source operations into one committed status boundary. Pool ownership remains with the adopter. Nested turns are savepoints; callers acquire accounts in canonical order when touching several. """ async with self._connection() as connection, connection.transaction(): token = _BOUND_CONNECTION.set((self._pool, asyncio.current_task(), connection)) try: yield self finally: _BOUND_CONNECTION.reset(token) @asynccontextmanager async def _source_publication( self, account_id: str, *, seals: ReportingSealStore | None = None ) -> AsyncIterator[InlineSealPublisher | None]: from psycopg import Error from adcp.reporting.inline_storage import InlineStorageError, PgReportingSealStore if isinstance(seals, PgReportingSealStore) and seals._pool is self._pool: # The backend's audited READ COMMITTED transaction owns both the # account lock and seal write. A second pool checkout would deadlock # with a size-one pool, and would separate the publication boundary. failure = None try: async with seals._transaction() as connection: await self._lock_account(connection, account_id) owner = asyncio.current_task() async def publish(key: str, sealed: SealedSlice) -> SealedSlice: if asyncio.current_task() is not owner: raise InlineStorageError("INVALID_INPUT") prepared = seals._prepare_seal( account_id=account_id, source_execution_key=key, sealed=sealed ) return await seals._put_on(connection, prepared) yield publish except InlineStorageError as error: failure = error.code except Error: failure = "RESOURCE_UNAVAILABLE" if failure is not None: raise InlineStorageError(failure) return # Keep the live authorization check and ledger commit on the same # account transaction, including commits replayed after a restart. async with self.transaction(), self._connection() as connection: await self._lock_account(connection, account_id) yield None async def create_schema(self) -> None: """Create or upgrade the ledger atomically, serializing concurrent boots. The bootstrap takes a transaction-scoped schema lock before any DDL. Keep the migration in that transaction, including with an autocommit pool, so a second process cannot observe a partially upgraded schema. """ async with self._connection() as connection: async with connection.transaction(): await self._create_schema_on(connection) async def _create_schema_on(self, connection: Any) -> None: from adcp.reporting.migration import require_owned_schema_or_empty await require_owned_schema_or_empty(connection) for path in ( _DDL_PATH, _ACCOUNT_GENERATIONS_DDL_PATH, _CURRENCY_DDL_PATH, _RECONCILIATION_DDL_PATH, _NOTIFICATIONS_DDL_PATH, _ACTIVITY_DDL_PATH, Path(__file__).with_name("reporting_provisional_observations.sql"), Path(__file__).with_name("reporting_caller_ownership.sql"), ): await connection.execute(path.read_text()) await self._require_provisional_schema(connection) if self._notifications_enabled: from adcp.reporting.outbox._schema import validate_schema await validate_schema(connection) async def _require_provisional_schema(self, connection: Any) -> None: from adcp.reporting.outbox._schema import validate_provisional_schema await validate_provisional_schema(connection) async def _notification_now(self, connection: Any) -> datetime: from adcp.reporting.outbox.pg import database_now if self._clock is not None: # Preserve the explicit conformance clock across deferred boundary # capture. Production leaves this unset and captures database time # after the complete source transaction's writes, before commit. await connection.execute("SELECT set_config('adcp.status_clock_override', 'on', true)") return await database_now(connection, self._clock) async def _record_notification(self, connection: Any, event: ReportingDomainEvent) -> None: if self._notifications_enabled: from adcp.reporting.outbox.pg import enqueue_event await enqueue_event(connection, event) async def _dirty_status( self, connection: Any, scope: ReportingStatusScope, reason: DirtyReason, before: ReportingStatusEvidence | None = None, after: ReportingStatusEvidence | None = None, ) -> None: if self._notifications_enabled: from adcp.reporting.ledger.status_snapshot import settle_snapshot_on from adcp.reporting.outbox.pg import mark_dirty await settle_snapshot_on(self, connection, account_id=scope.account_id) await mark_dirty( connection, scope, reason, await self._notification_now(connection), before, after ) async def _dirty_issue( self, connection: Any, issue: ReportingIssueLifecycle, status_scope: ReportingStatusScope | None, before: ReportingIssueLifecycle | None = None, *, enqueue: bool = True, ) -> None: from dataclasses import asdict has_scope_storage = await self._issue_scope_storage_on(connection) row = ( await ( await connection.execute( "SELECT scope FROM reporting_issue_status_scopes" " WHERE account_id = %s AND issue_id = %s", (issue.account_id, issue.issue_id), ) ).fetchone() if has_scope_storage else None ) existing = decode_status_scope(row[0]) if row else None scope = ( status_scope or existing or ReportingStatusScope(issue.account_id, consumer_id=issue.consumer_id) ) if ( scope.account_id != issue.account_id or issue.consumer_id is not None and scope.consumer_id != issue.consumer_id ): raise ReportingNotificationError("invalid_status_scope") validate_scope_refinement(existing, scope) if scope.generation_key is not None: key = scope.generation_key configuration = await ( await connection.execute( "SELECT feed_purpose FROM reporting_configurations WHERE account_id = %s" " AND consumer_id = %s AND delivery_config_id = %s AND delivery_co" "nfig_version = %s", ( scope.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) ).fetchone() if configuration is None or ( scope.feed_purpose is not None and configuration[0] != scope.feed_purpose ): raise ReportingNotificationError("invalid_status_scope") if scope.reporting_obligation_id is not None: obligation = await ( await connection.execute( "SELECT delivery_config_id, delivery_config_version, feed_purpose" " FROM reporting_obligations WHERE account_id = %s" " AND consumer_id = %s AND reporting_obligation_id = %s", (scope.account_id, scope.consumer_id, scope.reporting_obligation_id), ) ).fetchone() if ( obligation is None or ( scope.generation_key is not None and obligation[:2] != ( scope.generation_key.delivery_config_id, scope.generation_key.delivery_config_version, ) ) or (scope.feed_purpose is not None and obligation[2] != scope.feed_purpose) ): raise ReportingNotificationError("invalid_status_scope") if not has_scope_storage: # Default-off Core writers also support the pre-outbox schema. # Migration introduces scope persistence without rewriting history. return await connection.execute( "INSERT INTO reporting_issue_status_scopes (account_id, issue_id, scope)" " VALUES (%s,%s,%s::jsonb) ON CONFLICT (account_id, issue_id)" " DO UPDATE SET scope = EXCLUDED.scope", (issue.account_id, issue.issue_id, _json(asdict(scope))), ) if enqueue and (before != issue or existing != scope): await self._dirty_status( connection, scope, "issue", issue_evidence(before) if before else None, issue_evidence(issue), ) async def _issue_scope_storage_on(self, connection: Any) -> bool: row = await ( await connection.execute( "SELECT to_regclass(quote_ident(current_schema())" " || '.reporting_issue_status_scopes')" ) ).fetchone() present = row is not None and row[0] is not None if not present and self._notifications_enabled: raise ReportingNotificationError( "notification_schema_unready:missing:reporting_issue_status_scopes" ) return present # -- change feed ------------------------------------------------------ async def _append_change( self, connection: Any, account_id: str, kind: LedgerRecordKind, record_id: str, *, consumer_id: str, ) -> None: await connection.execute( "INSERT INTO reporting_ledger_changes" " (account_id, consumer_id, record_kind, record_id, committed_at)" " VALUES (%s, %s, %s, %s, COALESCE(%s, now()))" " ON CONFLICT (account_id, consumer_id, record_kind, record_id) DO NOTHING", ( account_id, consumer_id, kind, record_id, self._clock() if self._clock is not None else None, ), ) @staticmethod async def _bound_lock_waits(connection: Any) -> None: # set_config(..., true) is SET LOCAL: the cap ends with this transaction # (or savepoint rollback) and never lengthens an adopter's shorter limit. # SKIP LOCKED does not skip relation, FK, trigger, or advisory-lock waits. await connection.execute( "SELECT set_config('lock_timeout'," " LEAST(COALESCE(NULLIF(setting::integer,0),5000),5000)::text,true)" " FROM pg_settings WHERE name='lock_timeout'" ) def _worker_lock_errors(self) -> tuple[type[Exception], ...]: from psycopg.errors import DeadlockDetected, LockNotAvailable bound = _BOUND_CONNECTION.get() if bound is not None and bound[:2] == (self._pool, asyncio.current_task()): # The adopter owns the outer rollback and any locks acquired before # our savepoint. Only standalone worker turns may reacquire a lease. return () return (DeadlockDetected, LockNotAvailable) @staticmethod async def _lock_account(connection: Any, account_id: str) -> None: """Serialize this account's feed appends so seq order == commit order.""" await PgReportingLedgerStore._bound_lock_waits(connection) await connection.execute( "SELECT pg_advisory_xact_lock(hashtext(%s))", (f"adcp.reporting:{account_id}",) ) # Optional C lock order: account, boundary cursor, canonical typed scopes, # then source evidence. A/B schemas and custom stores need no C methods. present = await ( await connection.execute( "SELECT to_regclass(quote_ident(current_schema()) || '.reporting_status_accounts')" ) ).fetchone() if present is not None and present[0] is not None: await ( await connection.execute( "SELECT 1 FROM reporting_status_accounts WHERE account_id=%s FOR UPDATE", (account_id,), ) ).fetchall() await ( await connection.execute( "SELECT 1 FROM reporting_status_scope_checkpoints WHERE account_id=%s" " ORDER BY account_id, consumer_namespace, delivery_config_id, version," " scope_kind, obligation_namespace FOR UPDATE", (account_id,), ) ).fetchall() # -- configurations --------------------------------------------------- async def put_configuration(self, configuration: ReportingConfiguration) -> None: reject_reserved_authoritative_party(configuration) key = configuration.generation_key payload = _configuration_payload(configuration) digest = _fingerprint(payload) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) inserted_cursor = await connection.execute( "INSERT INTO reporting_configurations" " (delivery_config_id, delivery_config_version, account_id," " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, content_sha256, consumer_id, quarantined)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s, %s, %s, %s," " %s::jsonb, %s, %s, %s, %s)" " ON CONFLICT (account_id, consumer_id, delivery_config_id, delive" "ry_config_version) DO NOTHING" " RETURNING delivery_config_id", ( key.delivery_config_id, key.delivery_config_version, key.account_id, configuration.report_definition_id, configuration.reporting_profile, configuration.feed_purpose, configuration.required_finality, configuration.account_timezone, _json(payload["schedule"]), _json(sorted(configuration.media_buy_ids)), configuration.activated_at, configuration.deactivated_at, configuration.automated_recovery_window.total_seconds(), configuration.status_retention_days, _json(payload["definition"]) if payload["definition"] else None, configuration.authoritative_party, digest, configuration.consumer_id, configuration.quarantined, ), ) inserted = await inserted_cursor.fetchone() # Check *after* the insert. ON CONFLICT waits for a concurrent # winner; a fresh READ COMMITTED statement sees its retained # content. A pre-insert check followed by DO NOTHING could silently # accept a different immutable generation from a losing writer. row = await ( await connection.execute( "SELECT content_sha256, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, quarantined" " FROM reporting_configurations" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s FOR UPDATE", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) ).fetchone() if row is not None and row[5]: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) if row is None or row[0] != digest: raise LedgerConflictError( "CONFIGURATION_GENERATION_IMMUTABLE", f"configuration {key.delivery_config_id}@{key.delivery_config_version} " "already exists with different content for this account; publish a new " "version instead of editing a retained generation", ) scope = ReportingStatusScope(configuration.account_id, configuration.generation_key) if inserted is not None: if self._notifications_enabled: await self._dirty_status( connection, scope, "configuration", after=configuration_evidence(configuration), ) return # rc.3 carries activation/deactivation and the recovery/retention # windows as lifecycle state over one immutable generation, so a # re-put that changes only those must apply -- otherwise a # deactivated feed keeps minting obligations -- and must co-commit # its status-dirty generation on this exact connection. An # unchanged re-put stays a no-op and enqueues nothing. retained = replace( configuration, activated_at=_utc(row[1]) if row[1] else None, deactivated_at=_utc(row[2]) if row[2] else None, automated_recovery_window=timedelta(seconds=float(row[3])), status_retention_days=row[4], ) if configuration_lifecycle(retained) == configuration_lifecycle(configuration): return await connection.execute( "UPDATE reporting_configurations SET activated_at = %s, deactivated_at = %s," " automated_recovery_seconds = %s, status_retention_days = %s" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s", ( configuration.activated_at, configuration.deactivated_at, configuration.automated_recovery_window.total_seconds(), configuration.status_retention_days, key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) if self._notifications_enabled: await self._dirty_status( connection, scope, "configuration", before=configuration_evidence(retained), after=configuration_evidence(configuration), ) async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: async with self._connection() as connection: return await self._list_configurations_on( connection, account_id=caller.account_id, consumer_id=caller.consumer_id, delivery_config_ids=delivery_config_ids, ) async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: """One ordered scan for trusted service recovery, including retired generations.""" async with self._connection() as connection: rows = await ( await connection.execute( "SELECT delivery_config_id, delivery_config_version, account_id," " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, consumer_id, quarantined" " FROM reporting_configurations WHERE NOT quarantined" " ORDER BY account_id, consumer_id, delivery_config_id, delivery_config_version" ) ).fetchall() return tuple(_configuration_from_row(row) for row in rows) @staticmethod async def _list_configurations_on( connection: Any, *, account_id: str, consumer_id: str, delivery_config_ids: Sequence[str] | None = None, ) -> tuple[ReportingConfiguration, ...]: clause = " AND delivery_config_id = ANY(%s)" if delivery_config_ids else "" params: list[Any] = [account_id, consumer_id] if delivery_config_ids: params.append(list(delivery_config_ids)) rows = await ( await connection.execute( "SELECT delivery_config_id, delivery_config_version, account_id," # noqa: S608 # nosec B608 " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, consumer_id, quarantined" " FROM reporting_configurations" f" WHERE account_id = %s AND consumer_id = %s{clause}" # noqa: S608 — clause is a literal " ORDER BY delivery_config_id, delivery_config_version", tuple(params), ) ).fetchall() return tuple(_configuration_from_row(row) for row in rows) @staticmethod async def _require_writable_generation( connection: Any, key: ReportingConfigurationGenerationKey ) -> None: row = await ( await connection.execute( "SELECT quarantined FROM reporting_configurations WHERE account_id=%s" " AND consumer_id=%s AND delivery_config_id=%s AND delivery_config_version=%s", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) ).fetchone() if row is None: raise LedgerConflictError( "UNKNOWN_CONFIGURATION_GENERATION", "generation is unavailable" ) if row[0]: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "establish a new owned generation after operator reconciliation", ) # -- obligations ------------------------------------------------------ async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: key = obligation.generation_key async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) await self._require_writable_generation(connection, key) existing_row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND period_start = %s AND period_end = %s", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, obligation.period.start, obligation.period.end, ), ) ).fetchone() if existing_row is not None: return _obligation_from_row(existing_row) require_frozen_currency(obligation.currency) try: inserted = await ( await connection.execute( "INSERT INTO reporting_obligations" " (reporting_obligation_id, account_id, delivery_config_id," " delivery_config_version, report_definition_id, reporting_profile," " feed_purpose, period_key, period_start, period_end, source_timezone," " expected_at, scope_resolved_at, automated_recovery_deadline_at," " required_finality, coverage_status, media_buy_ids, package_ids," " schedule, definition, created_at, currency, consumer_id)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s," " %s::jsonb, %s::jsonb, %s::jsonb, %s::jsonb, %s, %s, %s)" " ON CONFLICT (account_id, consumer_id, delivery_config_id, delive" "ry_config_version," " period_start, period_end) DO NOTHING" " RETURNING reporting_obligation_id", ( obligation.reporting_obligation_id, key.account_id, key.delivery_config_id, key.delivery_config_version, obligation.report_definition_id, obligation.reporting_profile, obligation.feed_purpose, obligation.period.period_key, obligation.period.start, obligation.period.end, obligation.period.source_timezone, obligation.period.expected_at, obligation.scope_resolved_at, obligation.automated_recovery_deadline_at, obligation.required_finality, obligation.coverage_status, _json(sorted(obligation.media_buy_ids)), _json(sorted(obligation.package_ids)), _json(_schedule_payload(obligation.schedule)), ( _json(_definition_payload(obligation.definition)) if obligation.definition else None ), obligation.created_at, obligation.currency, obligation.consumer_id, ), ) ).fetchone() except Exception as error: raise _translate_integrity_error(error) from error if inserted is not None: await self._append_change( connection, obligation.account_id, "obligation", obligation.reporting_obligation_id, consumer_id=obligation.consumer_id, ) if self._notifications_enabled: await self._dirty_status( connection, ReportingStatusScope.for_obligation(obligation), "obligation", after=ReportingStatusEvidence( "obligation", obligation.reporting_obligation_id ), ) return obligation # Another worker won the period close; converge on its obligation. existing = await self.find_obligation( account_id=obligation.account_id, consumer_id=obligation.consumer_id, delivery_config_id=obligation.delivery_config_id, delivery_config_version=obligation.delivery_config_version, period_start=obligation.period.start, period_end=obligation.period.end, ) if existing is None: # pragma: no cover - only under concurrent deletion raise LedgerConflictError( "OBLIGATION_LOST", "the obligation vanished between insert and read" ) return existing async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, reporting_obligation_id), ) ).fetchone() return _obligation_from_row(row) if row else None async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: key = ReportingConfigurationGenerationKey( account_id=account_id, consumer_id=consumer_id, delivery_config_id=delivery_config_id, delivery_config_version=delivery_config_version, ) async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND period_start = %s AND period_end = %s", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, period_start, period_end, ), ) ).fetchone() return _obligation_from_row(row) if row else None # -- revisions -------------------------------------------------------- async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]] ) -> ReportingRevisionRecord: rows = tuple(deepcopy(row) for row in rows) if revision.row_count != len(rows): raise LedgerConflictError( "ROW_COUNT_MISMATCH", f"revision declares {revision.row_count} rows but {len(rows)} were supplied", ) validate_managed_revision_rows(revision, rows) digest = _fingerprint(_revision_payload(revision)) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, revision.account_id) existing = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS}, content_sha256 FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE reporting_revision_id = %s AND account_id = %s", (revision.reporting_revision_id, revision.account_id), ) ).fetchone() if existing is not None: if existing[-1] != digest: raise LedgerConflictError( "REVISION_IMMUTABLE", f"revision {revision.reporting_revision_id} already exists with " "different content; a restatement is a new revision", ) return _revision_from_row(existing[:-1]) obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE reporting_obligation_id = %s AND account_id = %s", (revision.reporting_obligation_id, revision.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a revision must attach to an obligation committed at the period close", ) await self._require_writable_generation( connection, _obligation_from_row(obligation).generation_key ) validate_revision_currency(_obligation_from_row(obligation), revision, rows) if revision.supersedes_reporting_revision_id: await self._require_current_leaf(connection, revision) write_error = None try: await connection.execute( "INSERT INTO reporting_revisions" " (reporting_revision_id, account_id, reporting_obligation_id, finality," " revision_content_sha256, row_count, control_totals, observed_at," " data_through, created_at, supersedes_reporting_revision_id," " finality_basis, finality_policy_id, finalized_at, readable," " readable_at_commit, source_publication_id, source_manifest_sha256," " content_sha256, canonical_content_digest, managed_control_totals)" " VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s, %s," " %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb)", ( revision.reporting_revision_id, revision.account_id, revision.reporting_obligation_id, revision.finality, revision.revision_content_sha256, revision.row_count, _json([[name, value] for name, value in revision.control_totals]), revision.observed_at, revision.data_through, revision.created_at, revision.supersedes_reporting_revision_id, revision.finality_basis, revision.finality_policy_id, revision.finalized_at, revision.readable, revision.readable_at_commit, revision.source_publication_id, revision.source_manifest_sha256, digest, ( _json(revision.canonical_content_digest.to_wire()) if revision.canonical_content_digest is not None else None ), ( _json([item.to_wire() for item in revision.managed_control_totals]) if revision.managed_control_totals is not None else None ), ), ) except Exception as error: # psycopg raises UniqueViolation subclasses write_error = _translate_integrity_error(error) if write_error is not None: raise write_error if rows: await connection.cursor().executemany( "INSERT INTO reporting_revision_rows" " (reporting_revision_id, ordinal, row_payload)" " SELECT reporting_revision_id, %s, %s::jsonb FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s", [ (ordinal, _json(row), revision.account_id, revision.reporting_revision_id) for ordinal, row in enumerate(rows) ], ) await self._append_change( connection, revision.account_id, "revision", revision.reporting_revision_id, consumer_id=_obligation_from_row(obligation).consumer_id, ) if self._notifications_enabled: from adcp.reporting.ledger.notification_events import revision_event await self._record_notification( connection, revision_event( revision, await self._notification_now(connection), consumer_id=_obligation_from_row(obligation).consumer_id, ), ) await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "revision", after=ReportingStatusEvidence( "revision", revision.reporting_revision_id, readable=revision.readable, supersedes_id=revision.supersedes_reporting_revision_id, ), ) return revision @staticmethod async def _require_current_leaf(connection: Any, revision: ReportingRevisionRecord) -> None: target = revision.supersedes_reporting_revision_id row = await ( await connection.execute( "SELECT r.reporting_revision_id," " EXISTS (SELECT 1 FROM reporting_revisions s" " WHERE s.account_id = r.account_id" " AND s.supersedes_reporting_revision_id = r.reporting_revision_id)" " FROM reporting_revisions r" " WHERE r.reporting_revision_id = %s AND r.account_id = %s" " AND r.reporting_obligation_id = %s", (target, revision.account_id, revision.reporting_obligation_id), ) ).fetchone() if row is None: raise LedgerConflictError( "SUPERSEDES_UNKNOWN", f"revision {target} is not part of this obligation's chain" ) if row[1]: raise LedgerConflictError( "SUPERSEDES_STALE", f"revision {target} has already been superseded; a stale pointer would fork " "the chain and let a successful retry erase a recorded restatement", ) async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS} FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s" " ORDER BY created_at, reporting_revision_id", (account_id, reporting_obligation_id), ) ).fetchall() return tuple(_revision_from_row(row) for row in rows) async def get_provisional_acquisition( self, *, account_id: str, reporting_obligation_id: str, ordinal: int ) -> ProvisionalAcquisition | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", (account_id, reporting_obligation_id, ordinal), ) ).fetchone() return ProvisionalAcquisition.from_wire(row[0]) if row is not None else None async def reserve_provisional_acquisition( self, acquisition: ProvisionalAcquisition ) -> ProvisionalAcquisition: async with self.transaction(), self._connection() as connection: await self._lock_account(connection, acquisition.account_id) await self._require_provisional_schema(connection) key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) existing = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if existing is not None: return ProvisionalAcquisition.from_wire(existing[0]) obligation = await self.get_obligation( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) if obligation is None: raise LedgerConflictError("OBLIGATION_NOT_FOUND", "unknown observation obligation") if not acquisition.binds(obligation): raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition generation differs") duplicate = await ( await connection.execute( "SELECT 1 FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND source_execution_key=%s", (acquisition.account_id, acquisition.execution_key), ) ).fetchone() if duplicate is not None: raise LedgerConflictError( "OBSERVATION_CONFLICT", "execution key is already reserved" ) checkpoint = await self.get_restatement_checkpoint( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) expected = ( checkpoint.next_observation if checkpoint else len( await self.list_revisions( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) ) ) if acquisition.ordinal != expected: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") await connection.execute( "INSERT INTO reporting_provisional_acquisitions" " (account_id,reporting_obligation_id,ordinal,source_execution_key,payload)" " VALUES (%s,%s,%s,%s,%s::jsonb)", (*key, acquisition.execution_key, _json(acquisition.to_wire())), ) return ProvisionalAcquisition.from_wire(acquisition.to_wire()) async def get_provisional_observation( self, *, account_id: str, reporting_obligation_id: str ) -> ProvisionalObservation | None: async with self._connection() as connection: await self._require_provisional_schema(connection) row = await ( await connection.execute( "SELECT payload FROM reporting_provisional_observations" " WHERE account_id=%s AND reporting_obligation_id=%s" " ORDER BY ordinal DESC LIMIT 1", (account_id, reporting_obligation_id), ) ).fetchone() return ProvisionalObservation.from_wire(row[0]) if row else None async def commit_provisional_observation( self, observation: ProvisionalObservation, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: acquisition = observation.acquisition key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) if ( revision.account_id != acquisition.account_id or revision.reporting_obligation_id != acquisition.obligation_id or revision.reporting_revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation identity differs") async with self.transaction(), self._connection() as connection: await self._lock_account(connection, acquisition.account_id) await self._require_provisional_schema(connection) existing = await ( await connection.execute( "SELECT reporting_revision_id,payload FROM reporting_provisional_observations" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if existing is not None: retained_observation = ProvisionalObservation.from_wire(existing[1]) if ( retained_observation.acquisition != acquisition or existing[0] != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation replay differs") retained_revision = await self.get_revision( account_id=acquisition.account_id, reporting_revision_id=existing[0] ) if retained_revision is None: raise LedgerConflictError( "HISTORY_UNAVAILABLE", "observation revision is missing" ) # Reuse the immutable publication replay checks while holding # the observation's account lock and transaction. return await self.commit_revision(revision, rows) reserved = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if reserved is None or ProvisionalAcquisition.from_wire(reserved[0]) != acquisition: raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition was not reserved") checkpoint = await self.get_restatement_checkpoint( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) if checkpoint is not None and checkpoint.next_observation != acquisition.ordinal: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") committed = await self.commit_revision(revision, rows) await self.record_restatement_checkpoint( RestatementCheckpoint( acquisition.account_id, acquisition.obligation_id, observation.checked_at, acquisition.ordinal + 1, observation.provisional_until, ) ) await connection.execute( "INSERT INTO reporting_provisional_observations" " (account_id,reporting_obligation_id,ordinal,reporting_revision_id,payload)" " VALUES (%s,%s,%s,%s,%s::jsonb)", (*key, revision.reporting_revision_id, _json(observation.to_wire())), ) return committed async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT retry_not_before, attempt, blocked, recorded_at" " FROM adcp_reporting_producer_retry_schedules" " WHERE scope_key = %s", (scope_key,), ) ).fetchone() return ( RetryScheduleEntry(scope_key, _utc(row[0]), int(row[1]), row[2], _utc(row[3])) if row else None ) async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: async with self._connection() as connection: row = await ( await connection.execute( "INSERT INTO adcp_reporting_producer_retry_schedules" " (scope_key, retry_not_before, attempt, blocked, recorded_at)" " VALUES (%s, %s, %s, %s, %s)" " ON CONFLICT (scope_key) DO UPDATE SET" " retry_not_before = CASE WHEN EXCLUDED.attempt = 0 OR EXCLUDED.blocked" " OR adcp_reporting_producer_retry_schedules.blocked OR %s" " THEN EXCLUDED.retry_not_before ELSE GREATEST(" " adcp_reporting_producer_retry_schedules.retry_not_before," " EXCLUDED.retry_not_before) END," " attempt = EXCLUDED.attempt, blocked = EXCLUDED.blocked," " recorded_at = EXCLUDED.recorded_at" " WHERE EXCLUDED.recorded_at >=" " adcp_reporting_producer_retry_schedules.recorded_at" " AND (EXCLUDED.attempt = 0 OR EXCLUDED.blocked OR %s" " OR (NOT adcp_reporting_producer_retry_schedules.blocked AND" " adcp_reporting_producer_retry_schedules.attempt <= EXCLUDED.attempt))" " RETURNING retry_not_before, attempt, blocked, recorded_at", ( entry.scope_key, entry.retry_not_before, entry.attempt, entry.blocked, entry.recorded_at, entry.replayed, entry.replayed, ), ) ).fetchone() if row is None: stored = await self.get_retry_schedule(scope_key=entry.scope_key) assert stored is not None return stored return RetryScheduleEntry(entry.scope_key, _utc(row[0]), int(row[1]), row[2], _utc(row[3])) async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT account_id, reporting_obligation_id, checked_at," " next_observation, provisional_until" " FROM reporting_restatement_checkpoints" " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, reporting_obligation_id), ) ).fetchone() if row is None: return None return RestatementCheckpoint( account_id=row[0], reporting_obligation_id=row[1], checked_at=_utc(row[2]), next_observation=int(row[3]), provisional_until=_utc(row[4]) if row[4] else None, ) async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: async with self._connection() as connection: obligation = await ( await connection.execute( "SELECT 1 FROM reporting_obligations" " WHERE reporting_obligation_id = %s AND account_id = %s", (checkpoint.reporting_obligation_id, checkpoint.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) row = await ( await connection.execute( "INSERT INTO reporting_restatement_checkpoints" " (account_id, reporting_obligation_id, checked_at, next_observation," " provisional_until)" " VALUES (%s, %s, %s, %s, %s)" " ON CONFLICT (reporting_obligation_id) DO UPDATE SET" " checked_at = EXCLUDED.checked_at," " next_observation = EXCLUDED.next_observation," " provisional_until = EXCLUDED.provisional_until" " WHERE reporting_restatement_checkpoints.account_id = EXCLUDED.account_id" " AND (reporting_restatement_checkpoints.next_observation" " < EXCLUDED.next_observation" " OR (reporting_restatement_checkpoints.next_observation" " = EXCLUDED.next_observation" " AND reporting_restatement_checkpoints.checked_at" " < EXCLUDED.checked_at))" " RETURNING account_id, reporting_obligation_id, checked_at," " next_observation, provisional_until", ( checkpoint.account_id, checkpoint.reporting_obligation_id, checkpoint.checked_at, checkpoint.next_observation, checkpoint.provisional_until, ), ) ).fetchone() if row is None: stored = await self.get_restatement_checkpoint( account_id=checkpoint.account_id, reporting_obligation_id=checkpoint.reporting_obligation_id, ) if stored is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) return stored return RestatementCheckpoint( account_id=row[0], reporting_obligation_id=row[1], checked_at=_utc(row[2]), next_observation=int(row[3]), provisional_until=_utc(row[4]) if row[4] else None, ) async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS} FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_revision_id = %s", (account_id, reporting_revision_id), ) ).fetchone() return _revision_from_row(row) if row else None async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: from adcp.reporting.ledger.store import revision_row_offset offset = revision_row_offset(cursor, reporting_revision_id, limit) async with self._connection() as connection: owned = await ( await connection.execute( "SELECT row_count FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s", (account_id, reporting_revision_id), ) ).fetchone() if owned is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") rows = await ( await connection.execute( "SELECT row_payload FROM reporting_revision_rows" " WHERE reporting_revision_id = %s" " ORDER BY ordinal OFFSET %s LIMIT %s", (reporting_revision_id, offset, limit), ) ).fetchall() total = int(owned[0]) has_more = offset + limit < total return ReportingRowPage( reporting_revision_id=reporting_revision_id, rows=tuple(row[0] for row in rows), total_count=total, has_more=has_more, cursor=( encode_cursor( {"ownership": 2, "revision": reporting_revision_id, "offset": offset + limit} ) if has_more else None ), ) async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) existing = await ( await connection.execute( "SELECT readable, reporting_obligation_id FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s FOR UPDATE", (account_id, reporting_revision_id), ) ).fetchone() if existing is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") if existing[0] == readable: return await connection.execute( "UPDATE reporting_revisions SET readable = %s" " WHERE account_id = %s AND reporting_revision_id = %s", (readable, account_id, reporting_revision_id), ) if self._notifications_enabled: obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, existing[1]), ) ).fetchone() assert obligation is not None await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "readability", ReportingStatusEvidence( "revision", reporting_revision_id, readable=existing[0] ), ReportingStatusEvidence("revision", reporting_revision_id, readable=readable), ) # -- adjustments ------------------------------------------------------ async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, adjustment.account_id) existing = await ( await connection.execute( f"SELECT {_ADJUSTMENT_COLUMNS} FROM reporting_adjustments" # noqa: S608 # nosec B608 " WHERE reporting_adjustment_id = %s AND account_id = %s", (adjustment.reporting_adjustment_id, adjustment.account_id), ) ).fetchone() if existing is not None: stored = _adjustment_from_row(existing) if stored != adjustment: raise LedgerConflictError( "ADJUSTMENT_IMMUTABLE", "adjustment content is immutable" ) return stored revision = await ( await connection.execute( "SELECT finality, reporting_obligation_id FROM reporting_revisions" " WHERE reporting_revision_id = %s AND account_id = %s", (adjustment.adjusts_reporting_revision_id, adjustment.account_id), ) ).fetchone() if revision is None: raise LedgerConflictError( "REVISION_NOT_FOUND", "an adjustment must name a committed revision" ) if revision[0] != "official": raise LedgerConflictError( "ADJUSTMENT_REQUIRES_OFFICIAL", "adjustments correct an official revision; restate a snapshot with a " "superseding snapshot revision instead", ) obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE reporting_obligation_id = %s AND account_id = %s", (revision[1], adjustment.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "the adjustment target has no obligation" ) await self._require_writable_generation( connection, _obligation_from_row(obligation).generation_key ) validate_adjustment_currency(_obligation_from_row(obligation), adjustment) inserted = await ( await connection.execute( "INSERT INTO reporting_adjustments" " (reporting_adjustment_id, account_id, adjusts_reporting_revision_id," " reason_code, reason_detail, accounting_period_start," " accounting_period_end, control_total_deltas, correction_observed_at," " created_at, managed_control_total_deltas)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s::jsonb)" " ON CONFLICT (reporting_adjustment_id) DO NOTHING" " RETURNING reporting_adjustment_id", ( adjustment.reporting_adjustment_id, adjustment.account_id, adjustment.adjusts_reporting_revision_id, adjustment.reason_code, adjustment.reason_detail, adjustment.accounting_period_start, adjustment.accounting_period_end, _json([[name, value] for name, value in adjustment.control_total_deltas]), adjustment.correction_observed_at, adjustment.created_at, ( _json( [item.to_wire() for item in adjustment.managed_control_total_deltas] ) if adjustment.managed_control_total_deltas is not None else None ), ), ) ).fetchone() if inserted is None: raise LedgerConflictError("ADJUSTMENT_UNAVAILABLE", "adjustment is unavailable") await self._append_change( connection, adjustment.account_id, "adjustment", adjustment.reporting_adjustment_id, consumer_id=_obligation_from_row(obligation).consumer_id, ) if self._notifications_enabled: from adcp.reporting.ledger.notification_events import adjustment_event await self._record_notification( connection, adjustment_event( adjustment, await self._notification_now(connection), consumer_id=_obligation_from_row(obligation).consumer_id, ), ) await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "adjustment", after=ReportingStatusEvidence("adjustment", adjustment.reporting_adjustment_id), ) return adjustment async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: if not reporting_revision_ids: return () async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_ADJUSTMENT_COLUMNS} FROM reporting_adjustments" # noqa: S608 # nosec B608 " WHERE account_id = %s AND adjusts_reporting_revision_id = ANY(%s)" " ORDER BY created_at, reporting_adjustment_id", (account_id, list(reporting_revision_ids)), ) ).fetchall() return tuple(_adjustment_from_row(row) for row in rows) # -- consumer status (preview) ---------------------------------------- async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: from adcp.reporting.evidence import consumer_reference consumer_reference(status.consumer_id) key = status.generation_key digest = _fingerprint(_consumer_status_payload(status)) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, status.account_id) replay = await self._replay(connection, status, digest) if replay is not None: return replay, False from adcp.reporting.ledger.status_snapshot import ( read_snapshot_on, validate_status_evidence, ) validate_status_evidence( status, await read_snapshot_on( connection, account_id=status.account_id, clock=self._clock, include_issue_scopes=await self._issue_scope_storage_on(connection), ), ) leaf = await ( await connection.execute( "SELECT reporting_status_id FROM reporting_consumer_statuses" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND report_definition_id = %s" " AND period_start = %s AND period_end = %s AND superseded = FALSE", ( key.account_id, status.consumer_id, key.delivery_config_id, key.delivery_config_version, status.report_definition_id, status.period_start, status.period_end, ), ) ).fetchone() if status.supersedes_reporting_status_id: if leaf is None or leaf[0] != status.supersedes_reporting_status_id: raise LedgerConflictError( "STATUS_SUPERSEDES_STALE", "supersedes_reporting_status_id must name this chain's current leaf; " "a stale pointer would let a successful retry erase a recorded outage", ) await connection.execute( "UPDATE reporting_consumer_statuses SET superseded = TRUE" " WHERE account_id = %s AND consumer_id = %s AND reporting_status_id = %s", (key.account_id, status.consumer_id, status.supersedes_reporting_status_id), ) elif leaf is not None: raise LedgerConflictError( "STATUS_SUPERSEDES_REQUIRED", "this chain already has a current statement; a new statement must " "explicitly supersede it", ) try: await connection.execute( "INSERT INTO reporting_consumer_statuses" " (reporting_status_id, account_id, consumer_id, delivery_config_id," " delivery_config_version, report_definition_id, period_start, period_end," " period_source_timezone, consumer_status, status_as_of, recorded_at," " supersedes_reporting_status_id, reporting_obligation_id," " reporting_revision_id, observed_revision_content_sha256, failure_code," " mismatch_code, consumer_commit_ref, seller_ledger_snapshot_id," " seller_ledger_as_of, superseded, content_sha256)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s," " %s, %s, %s, %s, %s, FALSE, %s)", ( status.reporting_status_id, status.account_id, status.consumer_id, status.delivery_config_id, status.delivery_config_version, status.report_definition_id, status.period_start, status.period_end, status.period_source_timezone, status.consumer_status, status.status_as_of, status.recorded_at, status.supersedes_reporting_status_id, status.reporting_obligation_id, status.reporting_revision_id, status.observed_revision_content_sha256, status.failure_code, status.mismatch_code, status.consumer_commit_ref, status.seller_ledger_snapshot_id, status.seller_ledger_as_of, digest, ), ) except Exception as error: raise _translate_integrity_error(error) from error await self._append_change( connection, status.account_id, "consumer_status", status.reporting_status_id, consumer_id=status.consumer_id, ) from adcp.reporting.ledger.status_snapshot import settle_snapshot_on await settle_snapshot_on(self, connection, account_id=status.account_id) if self._notifications_enabled: await self._dirty_status( connection, ReportingStatusScope( status.account_id, status.generation_key, status.reporting_obligation_id, status.consumer_id, ), "consumer_status", ReportingStatusEvidence("consumer_status", leaf[0]) if leaf else None, ReportingStatusEvidence( "consumer_status", status.reporting_status_id, supersedes_id=status.supersedes_reporting_status_id, ), ) return status, True async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: """Optional participant: the record and lifecycle share the source transaction.""" return await self.record_consumer_status(status) async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.ledger.status_snapshot import settle_snapshot_on from adcp.reporting.materializer.capture import private_snapshot async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) return private_snapshot( await settle_snapshot_on(self, connection, account_id=caller.account_id), ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id), ) @staticmethod async def _get_consumer_status( connection: Any, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: row = await ( await connection.execute( # Bare columns: this query has no `s` alias to qualify against. f"SELECT {_STATUS_COLUMNS_BARE} FROM reporting_consumer_statuses" # noqa: S608 # nosec B608 " WHERE reporting_status_id = %s AND account_id = %s AND consumer_id = %s", (status.reporting_status_id, status.account_id, status.consumer_id), ) ).fetchone() return _status_from_row(row) if row else None async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: async with self._connection() as connection: return await self._replay( connection, status, _fingerprint(_consumer_status_payload(status)) ) async def _replay( self, connection: Any, status: ConsumerStatusRecord, digest: str ) -> ConsumerStatusRecord | None: existing = await ( await connection.execute( "SELECT content_sha256 FROM reporting_consumer_statuses" " WHERE reporting_status_id = %s AND account_id = %s AND consumer_id = %s", (status.reporting_status_id, status.account_id, status.consumer_id), ) ).fetchone() if existing is None: return None if existing[0] != digest: raise LedgerConflictError( "STATUS_IDENTITY_CONFLICT", f"reporting_status_id {status.reporting_status_id} was already " "recorded with different content", ) stored = await self._get_consumer_status(connection, status) assert stored is not None return stored async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: # Attaches by obligation id *or* by the exact logical period key, so a # chain filed before the obligation existed is not lost, forked, or # reset when the seller later repairs the missing obligation. clause = "" params: list[Any] = [account_id, consumer_id] if reporting_obligation_ids is not None: clause = ( " AND EXISTS (" " SELECT 1 FROM reporting_obligations o" " WHERE o.reporting_obligation_id = ANY(%s)" " AND o.account_id = s.account_id" " AND (o.reporting_obligation_id = s.reporting_obligation_id OR (" " o.delivery_config_id = s.delivery_config_id" " AND o.delivery_config_version = s.delivery_config_version" " AND o.report_definition_id = s.report_definition_id" " AND o.period_start = s.period_start" " AND o.period_end = s.period_end)))" ) params.append(list(reporting_obligation_ids)) async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_STATUS_COLUMNS} FROM reporting_consumer_statuses s" # noqa: S608 # nosec B608 f" WHERE s.account_id = %s AND s.consumer_id = %s{clause}" " ORDER BY s.recorded_at, s.reporting_status_id", tuple(params), ) ).fetchall() return tuple(_status_from_row(row) for row in rows) # -- issue lifecycle -------------------------------------------------- async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) return await self._ensure_issue_opened_on( connection, issue_key=issue_key, account_id=account_id, consumer_id=consumer_id, observed_at=observed_at, status_scope=status_scope, ) async def _ensure_issue_opened_on( self, connection: Any, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, status_scope: ReportingStatusScope | None = None, enqueue: bool = True, ) -> ReportingIssueLifecycle: live = await self._live_issue(connection, issue_key, account_id) if live is not None: if live.consumer_id != consumer_id: raise ReportingNotificationError("invalid_status_scope") await self._dirty_issue(connection, live, status_scope, live, enqueue=enqueue) return live row = await ( await connection.execute( "SELECT generation, consumer_id, issue_id FROM reporting_issue_lifecycle" " WHERE account_id = %s AND issue_key = %s ORDER BY generation DESC LIMIT 1", (account_id, issue_key), ) ).fetchone() if row is not None: if row[1] != consumer_id: raise ReportingNotificationError("invalid_status_scope") previous = ( await ( await connection.execute( "SELECT scope FROM reporting_issue_status_scopes" " WHERE account_id=%s AND issue_id=%s", (account_id, row[2]), ) ).fetchone() if await self._issue_scope_storage_on(connection) else None ) previous_scope = decode_status_scope(previous[0]) if previous else None if status_scope is not None: validate_scope_refinement(previous_scope, status_scope) else: status_scope = previous_scope generation = int(row[0] if row else 0) + 1 issue_id = issue_id_for_occurrence(issue_key, generation) await connection.execute( "INSERT INTO reporting_issue_lifecycle" " (issue_key, account_id, generation, issue_id, consumer_id, opened_at, issue_state)" " VALUES (%s, %s, %s, %s, %s, %s, 'open')", (issue_key, account_id, generation, issue_id, consumer_id, _utc(observed_at)), ) record = ReportingIssueLifecycle( issue_key=issue_key, issue_id=issue_id, account_id=account_id, consumer_id=consumer_id, opened_at=_utc(observed_at), generation=generation, ) await self._dirty_issue(connection, record, status_scope, enqueue=enqueue) return record async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: if state not in {"acknowledged", "waived"}: raise LedgerConflictError( "ISSUE_STATE_NOT_OPERATOR_SETTABLE", f"issue_state {state!r} is not settable by an operator. 'resolved' is " "reachable only when the condition actually clears -- the projection " "retires it -- because a seller must not retire a mismatch out of a " "degraded projection while the statement that caused it is still the " "consumer's current leaf", ) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) live = await self._live_issue(connection, issue_key, account_id) if live is None: raise LedgerConflictError( "ISSUE_NOT_OPEN", f"no open issue {issue_key!r} for this account; a retired issue cannot be " "reopened, and a recurrence gets a new occurrence", ) check_issue_state_transition(live.issue_state, state) if state == "waived" and live.issue_state != "waived": from adcp.reporting.ledger.status_projection import bind_mismatch_waiver from adcp.reporting.ledger.status_snapshot import read_snapshot_on snapshot = await read_snapshot_on( connection, account_id=account_id, as_of=_utc(at), include_issue_scopes=await self._issue_scope_storage_on(connection), ) live = bind_mismatch_waiver(snapshot, live) await _save_waiver_binding(connection, live) waived_at = ( live.retired_at if live.waived_reporting_status_id is not None and live.retired_at is not None else _utc(at) ) await connection.execute( "UPDATE reporting_issue_lifecycle" " SET issue_state = %s," " external_ref = COALESCE(%s, external_ref)," " retired_at = CASE WHEN %s = 'waived' THEN %s ELSE retired_at END" " WHERE account_id = %s AND issue_key = %s AND generation = %s", ( state, external_ref, state, waived_at, account_id, issue_key, live.generation, ), ) refreshed = await self._issue_row(connection, issue_key, account_id, live.generation) assert refreshed is not None # Derive the no-op from the resulting row rather than predicting # it: an idempotent re-acknowledge changes nothing and enqueues # nothing, while anything that does move retained evidence stays # reconstructable for the projector. await self._dirty_issue(connection, refreshed, status_scope, live) return refreshed async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle | None: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) return await self._retire_issue_on( connection, issue_key=issue_key, account_id=account_id, at=at, status_scope=status_scope, ) async def _retire_issue_on( self, connection: Any, *, issue_key: str, account_id: str, at: datetime, status_scope: ReportingStatusScope | None = None, enqueue: bool = True, ) -> ReportingIssueLifecycle | None: live = await self._live_issue(connection, issue_key, account_id) if live is None or not issue_is_retirable(live.issue_state): return None if live.issue_state != "waived": check_issue_state_transition(live.issue_state, "resolved") await connection.execute( "UPDATE reporting_issue_lifecycle SET issue_state = 'resolved', retired_at = %s" " WHERE account_id = %s AND issue_key = %s AND generation = %s", (_utc(at), account_id, issue_key, live.generation), ) retired = await self._issue_row(connection, issue_key, account_id, live.generation) assert retired is not None await self._dirty_issue(connection, retired, status_scope, live, enqueue=enqueue) return retired async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: async with self._connection() as connection: return await self._live_issue(connection, issue_key, account_id) @staticmethod async def _live_issue( connection: Any, issue_key: str, account_id: str ) -> ReportingIssueLifecycle | None: row = await ( await connection.execute( f"SELECT {_ISSUE_COLUMNS} FROM reporting_issue_lifecycle" # noqa: S608 # nosec B608 " WHERE account_id = %s AND issue_key = %s" " AND issue_state IN ('open', 'acknowledged', 'waived')", (account_id, issue_key), ) ).fetchone() return await _with_waiver_binding(connection, _issue_from_row(row)) if row else None @staticmethod async def _issue_row( connection: Any, issue_key: str, account_id: str, generation: int ) -> ReportingIssueLifecycle | None: row = await ( await connection.execute( f"SELECT {_ISSUE_COLUMNS} FROM reporting_issue_lifecycle" # noqa: S608 # nosec B608 " WHERE account_id = %s AND issue_key = %s AND generation = %s", (account_id, issue_key, generation), ) ).fetchone() return await _with_waiver_binding(connection, _issue_from_row(row)) if row else None # -- snapshots -------------------------------------------------------- async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: account_id = caller.account_id async with self._connection() as connection: row = await ( await connection.execute( "SELECT COALESCE(MAX(seq), 0), now() FROM reporting_ledger_changes" " WHERE account_id=%s AND consumer_id=%s AND record_kind IN" " ('obligation','revision','adjustment','consumer_status')", (account_id, caller.consumer_id), ) ).fetchone() assert row is not None max_sequence = int(row[0]) as_of = _utc(self._clock()) if self._clock is not None else _utc(row[1]) return LedgerSnapshot( snapshot_id="rpls_" + _fingerprint([account_id, caller.consumer_id, filters_fingerprint, max_sequence])[ :32 ], account_id=account_id, consumer_id=caller.consumer_id, ledger_as_of=as_of, max_sequence=max_sequence, ) async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> LedgerPage: if consumer_id not in {None, snapshot.consumer_id}: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "snapshot belongs to another caller" ) consumer_id = snapshot.consumer_id lower = changes_after_sequence or 0 async with self._connection() as connection: rows = await ( await connection.execute( "SELECT seq, record_kind, record_id FROM reporting_ledger_changes" " WHERE account_id = %s AND consumer_id=%s AND seq > %s AND seq <= %s" " ORDER BY seq", (snapshot.account_id, consumer_id, lower, snapshot.max_sequence), ) ).fetchall() selected: list[tuple[int, LedgerRecordKind, Any]] = [] for _seq, kind, record_id in rows: record = await self._resolve( connection, snapshot.account_id, kind, record_id, consumer_id=consumer_id ) if record is None: continue if not await self._in_scope( connection, kind, record, delivery_config_ids, media_buy_ids, consumer_id, feed_purposes, period_start, period_end, ): continue selected.append((_seq, kind, record)) window = selected[offset : offset + limit] has_more = offset + limit < len(selected) return LedgerPage( obligations=tuple(item[2] for item in window if item[1] == "obligation"), revisions=tuple(item[2] for item in window if item[1] == "revision"), adjustments=tuple(item[2] for item in window if item[1] == "adjustment"), consumer_statuses=tuple(item[2] for item in window if item[1] == "consumer_status"), total_count=len(selected), has_more=has_more, cursor=( encode_cursor({"snapshot": snapshot.snapshot_id, "offset": offset + limit}) if has_more else None ), ) async def _resolve( self, connection: Any, account_id: str, kind: str, record_id: str, *, consumer_id: str | None = None, ) -> Any | None: if kind not in _RESOLVERS: return None # Higher-tier records are never projected by Core. table, columns, key, builder = _RESOLVERS[kind] consumer_filter = " AND consumer_id=%s" if kind == "consumer_status" else "" parameters = ( (account_id, record_id, consumer_id) if consumer_filter else (account_id, record_id) ) row = await ( await connection.execute( f"SELECT {columns} FROM {table} WHERE account_id = %s AND {key} = %s" # noqa: S608 # nosec B608 f"{consumer_filter}", # nosec B608 parameters, ) ).fetchone() return builder(row) if row else None async def _in_scope( self, connection: Any, kind: str, record: Any, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, consumer_id: str | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> bool: from adcp.reporting.ledger.status_projection import configuration_selected, period_selected if kind == "consumer_status": # A caller sees only its own statements; another consumer's # operational status is never disclosed. if consumer_id is None or record.consumer_id != consumer_id: return False owner = record start, end = record.period_start, record.period_end else: owner = await self._obligation_for(connection, kind, record) if owner is None or owner.consumer_id != consumer_id: return False start, end = owner.period.start, owner.period.end if media_buy_ids and not set(media_buy_ids).intersection(owner.media_buy_ids): return False configurations = await self._list_configurations_on( connection, account_id=record.account_id, consumer_id=owner.consumer_id, delivery_config_ids=[owner.delivery_config_id], ) configuration = next( (c for c in configurations if c.generation_key == owner.generation_key), None ) if configuration is None: return False return configuration_selected( configuration, delivery_config_ids=delivery_config_ids or (), media_buy_ids=media_buy_ids or (), feed_purposes=feed_purposes or (), ) and period_selected(start, end, period_start, period_end) async def _obligation_for( self, connection: Any, kind: str, record: Any ) -> ReportingObligationRecord | None: if kind == "obligation": found: ReportingObligationRecord = record return found if kind == "revision": return await self._resolve( connection, record.account_id, "obligation", record.reporting_obligation_id ) revision = await self._resolve( connection, record.account_id, "revision", record.adjusts_reporting_revision_id ) if revision is None: return None return await self._resolve( connection, revision.account_id, "obligation", revision.reporting_obligation_id ) # -- leasing ---------------------------------------------------------- async def _period_close_generations_on( self, connection: Any, identity: tuple[str, str, str, int] ) -> list[tuple[str, str, int]]: # A base-tier store may share a schema installed by another participant. # Probe on this transaction without caching schema presence or validating # every catalog object on the lease path. Call only after the account lock. present = await ( await connection.execute( "SELECT to_regclass('reporting_materializer_candidates') IS NOT NULL" ) ).fetchone() if not present[0]: return [] rows = await ( await connection.execute( "SELECT consumer_id,reporting_obligation_id,generation" " FROM reporting_materializer_candidates WHERE account_id=%s" " AND consumer_id=%s AND delivery_config_id=%s AND delivery_config_version=%s", identity, ) ).fetchall() return [(row[0], row[1], row[2]) for row in rows] async def _restore_period_close_generations_on( self, connection: Any, identity: tuple[str, str, str, int], snapshot: list[tuple[str, str, int]], ) -> None: # These SDK writes change only lease fields. All supported source writers # hold the same account lock, so no source change can intervene. Restore # only the captured generation plus the known single trigger increment; # never decrement blindly if the trigger was disabled or did not fire. # Unexpected generation changes remain fenced for schema/target validation. # Wakeups and due_at/reason retain the production tier's existing behavior. if not snapshot: return await connection.execute( "UPDATE reporting_materializer_candidates c SET generation=s.generation" " FROM unnest(%s::text[],%s::text[],%s::bigint[])" " AS s(consumer_id,reporting_obligation_id,generation)" " WHERE c.account_id=%s AND c.consumer_id=%s AND c.delivery_config_id=%s" " AND c.delivery_config_version=%s AND c.consumer_id=s.consumer_id" " AND c.reporting_obligation_id=s.reporting_obligation_id" " AND c.generation=s.generation+1", ( [row[0] for row in snapshot], [row[1] for row in snapshot], [row[2] for row in snapshot], *identity, ), ) async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: from psycopg.errors import LockNotAvailable from psycopg.pq import TransactionStatus moment = _utc(now) expires = moment + timedelta(seconds=lease_seconds) after = self._period_close_sample wait_until = None following = None result = None async with self._connection() as connection: # Never add a blocking account edge while a caller transaction may # already own locks. Standalone turns may queue only before the first # account acquisition, including acquisitions that find a stale row. can_wait = connection.info.transaction_status == TransactionStatus.IDLE async with connection.transaction(): await self._bound_lock_waits(connection) # A materializer installed by any participant adds an AFTER UPDATE # trigger taking the account lock. Sample without row locks, then take # that account lock before locking a configuration. The base store can # share that schema, so the ordering belongs here, not only on a tier. for _ in range(2): continuation = ( " AND (COALESCE(t.lease_turn,0)," " COALESCE(c.lease_expires_at,'-infinity'::timestamptz)," " c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_confi" "g_version)" " > (%s,COALESCE(%s::timestamptz,'-infinity'::timestamptz),%s,%s,%s,%s)" if after is not None else "" ) query = ( "SELECT c.account_id,c.consumer_id,c.delivery_config_id,c.delivery" "_config_version," # nosec B608 " COALESCE(t.lease_turn,0),c.lease_expires_at FROM" " reporting_configurations c" " LEFT JOIN adcp_reporting_configuration_lease_turns t" " ON (t.account_id,t.consumer_id,t.delivery_config_id,t.delivery_c" "onfig_version)" " = (c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_co" "nfig_version)" " WHERE NOT c.quarantined AND (c.lease_expires_at IS NULL OR c.lea" "se_expires_at<=%s)" + continuation + " ORDER BY COALESCE(t.lease_turn,0),c.lease_expires_at NULLS FIRST," " c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_confi" "g_version LIMIT 32" ) rows = await ( await connection.execute(query, (moment, *(after or ()))) ).fetchall() if rows or after is None: break after = None for candidate in rows: row = candidate[:4] following = (candidate[4], candidate[5], *row) locked = await ( await connection.execute( "SELECT pg_try_advisory_xact_lock(hashtext('adcp.reporting:' || %s))", (row[0],), ) ).fetchone() if not locked[0]: if not can_wait: continue # A try-lock alone can starve behind a continuous queue of # ordinary writers, even though each writer commits promptly. # Join that queue briefly, sharing one wait budget per turn. # The savepoint rolls back a timed-out wait and its SET LOCAL; # successful acquisition restores the caller's lock timeout. if wait_until is None: wait_until = monotonic() + _LEASE_ACCOUNT_WAIT_SECONDS remaining_ms = int((wait_until - monotonic()) * 1000) if remaining_ms <= 0: continue try: async with connection.transaction(): previous, configured_ms = await ( await connection.execute( "SELECT current_setting('lock_timeout'),setting::integer" " FROM pg_settings WHERE name='lock_timeout'" ) ).fetchone() limit_ms = min(remaining_ms, configured_ms or remaining_ms) await connection.execute( "SELECT set_config('lock_timeout',%s,true)", (f"{limit_ms}ms",) ) await connection.execute( "SELECT pg_advisory_xact_lock(hashtext('adcp.reporting:' ||" " %s))", (row[0],), ) await connection.execute( "SELECT set_config('lock_timeout',%s,true)", (previous,) ) except LockNotAvailable: continue can_wait = False snapshot = await self._period_close_generations_on(connection, tuple(row)) acquired = await ( await connection.execute( "UPDATE reporting_configurations SET lease_worker_id = %s," " lease_expires_at = %s" " WHERE (account_id,consumer_id,delivery_config_id,delivery_config" "_version) = (" " SELECT c.account_id,c.consumer_id,c.delivery_config_id,c.deliver" "y_config_version" " FROM reporting_configurations c WHERE c.account_id=%s" " AND c.consumer_id=%s AND c.delivery_config_id=%s AND c.delivery_" "config_version=%s" " AND NOT c.quarantined AND (c.lease_expires_at IS NULL OR c.lease" "_expires_at<=%s)" " FOR UPDATE OF c SKIP LOCKED) RETURNING account_id", (worker_id, expires, *row, moment), ) ).fetchone() if acquired is None: continue await self._restore_period_close_generations_on( connection, tuple(row), snapshot ) await connection.execute( "INSERT INTO adcp_reporting_configuration_lease_turns" " (account_id,consumer_id,delivery_config_id,delivery_config_versi" "on,lease_turn)" " VALUES(%s,%s,%s,%s,nextval('adcp_reporting_configuration_lease_t" "urn_seq'))" " ON CONFLICT(account_id,consumer_id,delivery_config_id,delivery_c" "onfig_version)" " DO UPDATE SET" " lease_turn=nextval('adcp_reporting_configuration_lease_turn_seq')", tuple(row), ) result = LeasedConfiguration(row[0], row[1], row[2], row[3], expires) break # Only sampling uses this hint: leases and fairness ranks stay transactional. # Continue past a busy prefix on the next turn; a successful turn returns to # durable fairness order. Empty tails wrap at most once without row locks. self._period_close_sample = following if result is None else None return result async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: key = lease.generation_key identity = ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) snapshot = await self._period_close_generations_on(connection, identity) released = await ( await connection.execute( "UPDATE reporting_configurations SET lease_worker_id = NULL," " lease_expires_at = NULL" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND lease_worker_id = %s" " AND lease_expires_at = %s RETURNING account_id", (*identity, worker_id, lease.lease_expires_at), ) ).fetchone() if released is not None: await self._restore_period_close_generations_on(connection, identity, snapshot)Durable reporting ledger over a caller-supplied connection pool.
Subclasses
Class variables
var is_durable : ClassVar[bool]
Methods
async def commit_adjustment(self,
adjustment: ReportingAdjustmentRecord) ‑> ReportingAdjustmentRecord-
Expand source code
async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, adjustment.account_id) existing = await ( await connection.execute( f"SELECT {_ADJUSTMENT_COLUMNS} FROM reporting_adjustments" # noqa: S608 # nosec B608 " WHERE reporting_adjustment_id = %s AND account_id = %s", (adjustment.reporting_adjustment_id, adjustment.account_id), ) ).fetchone() if existing is not None: stored = _adjustment_from_row(existing) if stored != adjustment: raise LedgerConflictError( "ADJUSTMENT_IMMUTABLE", "adjustment content is immutable" ) return stored revision = await ( await connection.execute( "SELECT finality, reporting_obligation_id FROM reporting_revisions" " WHERE reporting_revision_id = %s AND account_id = %s", (adjustment.adjusts_reporting_revision_id, adjustment.account_id), ) ).fetchone() if revision is None: raise LedgerConflictError( "REVISION_NOT_FOUND", "an adjustment must name a committed revision" ) if revision[0] != "official": raise LedgerConflictError( "ADJUSTMENT_REQUIRES_OFFICIAL", "adjustments correct an official revision; restate a snapshot with a " "superseding snapshot revision instead", ) obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE reporting_obligation_id = %s AND account_id = %s", (revision[1], adjustment.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "the adjustment target has no obligation" ) await self._require_writable_generation( connection, _obligation_from_row(obligation).generation_key ) validate_adjustment_currency(_obligation_from_row(obligation), adjustment) inserted = await ( await connection.execute( "INSERT INTO reporting_adjustments" " (reporting_adjustment_id, account_id, adjusts_reporting_revision_id," " reason_code, reason_detail, accounting_period_start," " accounting_period_end, control_total_deltas, correction_observed_at," " created_at, managed_control_total_deltas)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s::jsonb)" " ON CONFLICT (reporting_adjustment_id) DO NOTHING" " RETURNING reporting_adjustment_id", ( adjustment.reporting_adjustment_id, adjustment.account_id, adjustment.adjusts_reporting_revision_id, adjustment.reason_code, adjustment.reason_detail, adjustment.accounting_period_start, adjustment.accounting_period_end, _json([[name, value] for name, value in adjustment.control_total_deltas]), adjustment.correction_observed_at, adjustment.created_at, ( _json( [item.to_wire() for item in adjustment.managed_control_total_deltas] ) if adjustment.managed_control_total_deltas is not None else None ), ), ) ).fetchone() if inserted is None: raise LedgerConflictError("ADJUSTMENT_UNAVAILABLE", "adjustment is unavailable") await self._append_change( connection, adjustment.account_id, "adjustment", adjustment.reporting_adjustment_id, consumer_id=_obligation_from_row(obligation).consumer_id, ) if self._notifications_enabled: from adcp.reporting.ledger.notification_events import adjustment_event await self._record_notification( connection, adjustment_event( adjustment, await self._notification_now(connection), consumer_id=_obligation_from_row(obligation).consumer_id, ), ) await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "adjustment", after=ReportingStatusEvidence("adjustment", adjustment.reporting_adjustment_id), ) return adjustment async def commit_obligation(self,
obligation: ReportingObligationRecord) ‑> ReportingObligationRecord-
Expand source code
async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: key = obligation.generation_key async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) await self._require_writable_generation(connection, key) existing_row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND period_start = %s AND period_end = %s", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, obligation.period.start, obligation.period.end, ), ) ).fetchone() if existing_row is not None: return _obligation_from_row(existing_row) require_frozen_currency(obligation.currency) try: inserted = await ( await connection.execute( "INSERT INTO reporting_obligations" " (reporting_obligation_id, account_id, delivery_config_id," " delivery_config_version, report_definition_id, reporting_profile," " feed_purpose, period_key, period_start, period_end, source_timezone," " expected_at, scope_resolved_at, automated_recovery_deadline_at," " required_finality, coverage_status, media_buy_ids, package_ids," " schedule, definition, created_at, currency, consumer_id)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s," " %s::jsonb, %s::jsonb, %s::jsonb, %s::jsonb, %s, %s, %s)" " ON CONFLICT (account_id, consumer_id, delivery_config_id, delive" "ry_config_version," " period_start, period_end) DO NOTHING" " RETURNING reporting_obligation_id", ( obligation.reporting_obligation_id, key.account_id, key.delivery_config_id, key.delivery_config_version, obligation.report_definition_id, obligation.reporting_profile, obligation.feed_purpose, obligation.period.period_key, obligation.period.start, obligation.period.end, obligation.period.source_timezone, obligation.period.expected_at, obligation.scope_resolved_at, obligation.automated_recovery_deadline_at, obligation.required_finality, obligation.coverage_status, _json(sorted(obligation.media_buy_ids)), _json(sorted(obligation.package_ids)), _json(_schedule_payload(obligation.schedule)), ( _json(_definition_payload(obligation.definition)) if obligation.definition else None ), obligation.created_at, obligation.currency, obligation.consumer_id, ), ) ).fetchone() except Exception as error: raise _translate_integrity_error(error) from error if inserted is not None: await self._append_change( connection, obligation.account_id, "obligation", obligation.reporting_obligation_id, consumer_id=obligation.consumer_id, ) if self._notifications_enabled: await self._dirty_status( connection, ReportingStatusScope.for_obligation(obligation), "obligation", after=ReportingStatusEvidence( "obligation", obligation.reporting_obligation_id ), ) return obligation # Another worker won the period close; converge on its obligation. existing = await self.find_obligation( account_id=obligation.account_id, consumer_id=obligation.consumer_id, delivery_config_id=obligation.delivery_config_id, delivery_config_version=obligation.delivery_config_version, period_start=obligation.period.start, period_end=obligation.period.end, ) if existing is None: # pragma: no cover - only under concurrent deletion raise LedgerConflictError( "OBLIGATION_LOST", "the obligation vanished between insert and read" ) return existing async def commit_provisional_observation(self,
observation: ProvisionalObservation,
revision: ReportingRevisionRecord,
rows: Sequence[dict[str, Any]]) ‑> ReportingRevisionRecord-
Expand source code
async def commit_provisional_observation( self, observation: ProvisionalObservation, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: acquisition = observation.acquisition key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) if ( revision.account_id != acquisition.account_id or revision.reporting_obligation_id != acquisition.obligation_id or revision.reporting_revision_id != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation identity differs") async with self.transaction(), self._connection() as connection: await self._lock_account(connection, acquisition.account_id) await self._require_provisional_schema(connection) existing = await ( await connection.execute( "SELECT reporting_revision_id,payload FROM reporting_provisional_observations" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if existing is not None: retained_observation = ProvisionalObservation.from_wire(existing[1]) if ( retained_observation.acquisition != acquisition or existing[0] != observation.revision_id ): raise LedgerConflictError("OBSERVATION_CONFLICT", "observation replay differs") retained_revision = await self.get_revision( account_id=acquisition.account_id, reporting_revision_id=existing[0] ) if retained_revision is None: raise LedgerConflictError( "HISTORY_UNAVAILABLE", "observation revision is missing" ) # Reuse the immutable publication replay checks while holding # the observation's account lock and transaction. return await self.commit_revision(revision, rows) reserved = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if reserved is None or ProvisionalAcquisition.from_wire(reserved[0]) != acquisition: raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition was not reserved") checkpoint = await self.get_restatement_checkpoint( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) if checkpoint is not None and checkpoint.next_observation != acquisition.ordinal: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") committed = await self.commit_revision(revision, rows) await self.record_restatement_checkpoint( RestatementCheckpoint( acquisition.account_id, acquisition.obligation_id, observation.checked_at, acquisition.ordinal + 1, observation.provisional_until, ) ) await connection.execute( "INSERT INTO reporting_provisional_observations" " (account_id,reporting_obligation_id,ordinal,reporting_revision_id,payload)" " VALUES (%s,%s,%s,%s,%s::jsonb)", (*key, revision.reporting_revision_id, _json(observation.to_wire())), ) return committed async def commit_revision(self,
revision: ReportingRevisionRecord,
rows: Sequence[dict[str, Any]]) ‑> ReportingRevisionRecord-
Expand source code
async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]] ) -> ReportingRevisionRecord: rows = tuple(deepcopy(row) for row in rows) if revision.row_count != len(rows): raise LedgerConflictError( "ROW_COUNT_MISMATCH", f"revision declares {revision.row_count} rows but {len(rows)} were supplied", ) validate_managed_revision_rows(revision, rows) digest = _fingerprint(_revision_payload(revision)) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, revision.account_id) existing = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS}, content_sha256 FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE reporting_revision_id = %s AND account_id = %s", (revision.reporting_revision_id, revision.account_id), ) ).fetchone() if existing is not None: if existing[-1] != digest: raise LedgerConflictError( "REVISION_IMMUTABLE", f"revision {revision.reporting_revision_id} already exists with " "different content; a restatement is a new revision", ) return _revision_from_row(existing[:-1]) obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE reporting_obligation_id = %s AND account_id = %s", (revision.reporting_obligation_id, revision.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a revision must attach to an obligation committed at the period close", ) await self._require_writable_generation( connection, _obligation_from_row(obligation).generation_key ) validate_revision_currency(_obligation_from_row(obligation), revision, rows) if revision.supersedes_reporting_revision_id: await self._require_current_leaf(connection, revision) write_error = None try: await connection.execute( "INSERT INTO reporting_revisions" " (reporting_revision_id, account_id, reporting_obligation_id, finality," " revision_content_sha256, row_count, control_totals, observed_at," " data_through, created_at, supersedes_reporting_revision_id," " finality_basis, finality_policy_id, finalized_at, readable," " readable_at_commit, source_publication_id, source_manifest_sha256," " content_sha256, canonical_content_digest, managed_control_totals)" " VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s, %s," " %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb)", ( revision.reporting_revision_id, revision.account_id, revision.reporting_obligation_id, revision.finality, revision.revision_content_sha256, revision.row_count, _json([[name, value] for name, value in revision.control_totals]), revision.observed_at, revision.data_through, revision.created_at, revision.supersedes_reporting_revision_id, revision.finality_basis, revision.finality_policy_id, revision.finalized_at, revision.readable, revision.readable_at_commit, revision.source_publication_id, revision.source_manifest_sha256, digest, ( _json(revision.canonical_content_digest.to_wire()) if revision.canonical_content_digest is not None else None ), ( _json([item.to_wire() for item in revision.managed_control_totals]) if revision.managed_control_totals is not None else None ), ), ) except Exception as error: # psycopg raises UniqueViolation subclasses write_error = _translate_integrity_error(error) if write_error is not None: raise write_error if rows: await connection.cursor().executemany( "INSERT INTO reporting_revision_rows" " (reporting_revision_id, ordinal, row_payload)" " SELECT reporting_revision_id, %s, %s::jsonb FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s", [ (ordinal, _json(row), revision.account_id, revision.reporting_revision_id) for ordinal, row in enumerate(rows) ], ) await self._append_change( connection, revision.account_id, "revision", revision.reporting_revision_id, consumer_id=_obligation_from_row(obligation).consumer_id, ) if self._notifications_enabled: from adcp.reporting.ledger.notification_events import revision_event await self._record_notification( connection, revision_event( revision, await self._notification_now(connection), consumer_id=_obligation_from_row(obligation).consumer_id, ), ) await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "revision", after=ReportingStatusEvidence( "revision", revision.reporting_revision_id, readable=revision.readable, supersedes_id=revision.supersedes_reporting_revision_id, ), ) return revision async def create_schema(self) ‑> None-
Expand source code
async def create_schema(self) -> None: """Create or upgrade the ledger atomically, serializing concurrent boots. The bootstrap takes a transaction-scoped schema lock before any DDL. Keep the migration in that transaction, including with an autocommit pool, so a second process cannot observe a partially upgraded schema. """ async with self._connection() as connection: async with connection.transaction(): await self._create_schema_on(connection)Create or upgrade the ledger atomically, serializing concurrent boots.
The bootstrap takes a transaction-scoped schema lock before any DDL. Keep the migration in that transaction, including with an autocommit pool, so a second process cannot observe a partially upgraded schema.
async def ensure_issue_opened(self,
*,
issue_key: str,
account_id: str,
consumer_id: str | None,
observed_at: datetime,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle-
Expand source code
async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) return await self._ensure_issue_opened_on( connection, issue_key=issue_key, account_id=account_id, consumer_id=consumer_id, observed_at=observed_at, status_scope=status_scope, ) async def find_obligation(self,
*,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
period_start: datetime,
period_end: datetime) ‑> ReportingObligationRecord | None-
Expand source code
async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: key = ReportingConfigurationGenerationKey( account_id=account_id, consumer_id=consumer_id, delivery_config_id=delivery_config_id, delivery_config_version=delivery_config_version, ) async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND period_start = %s AND period_end = %s", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, period_start, period_end, ), ) ).fetchone() return _obligation_from_row(row) if row else None async def get_issue(self, *, issue_key: str, account_id: str) ‑> ReportingIssueLifecycle | None-
Expand source code
async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: async with self._connection() as connection: return await self._live_issue(connection, issue_key, account_id) async def get_obligation(self, *, account_id: str, reporting_obligation_id: str) ‑> ReportingObligationRecord | None-
Expand source code
async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, reporting_obligation_id), ) ).fetchone() return _obligation_from_row(row) if row else None async def get_provisional_acquisition(self, *, account_id: str, reporting_obligation_id: str, ordinal: int) ‑> ProvisionalAcquisition | None-
Expand source code
async def get_provisional_acquisition( self, *, account_id: str, reporting_obligation_id: str, ordinal: int ) -> ProvisionalAcquisition | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", (account_id, reporting_obligation_id, ordinal), ) ).fetchone() return ProvisionalAcquisition.from_wire(row[0]) if row is not None else None async def get_provisional_observation(self, *, account_id: str, reporting_obligation_id: str) ‑> ProvisionalObservation | None-
Expand source code
async def get_provisional_observation( self, *, account_id: str, reporting_obligation_id: str ) -> ProvisionalObservation | None: async with self._connection() as connection: await self._require_provisional_schema(connection) row = await ( await connection.execute( "SELECT payload FROM reporting_provisional_observations" " WHERE account_id=%s AND reporting_obligation_id=%s" " ORDER BY ordinal DESC LIMIT 1", (account_id, reporting_obligation_id), ) ).fetchone() return ProvisionalObservation.from_wire(row[0]) if row else None async def get_restatement_checkpoint(self, *, account_id: str, reporting_obligation_id: str) ‑> RestatementCheckpoint | None-
Expand source code
async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT account_id, reporting_obligation_id, checked_at," " next_observation, provisional_until" " FROM reporting_restatement_checkpoints" " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, reporting_obligation_id), ) ).fetchone() if row is None: return None return RestatementCheckpoint( account_id=row[0], reporting_obligation_id=row[1], checked_at=_utc(row[2]), next_observation=int(row[3]), provisional_until=_utc(row[4]) if row[4] else None, ) async def get_retry_schedule(self, *, scope_key: str) ‑> RetryScheduleEntry | None-
Expand source code
async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: async with self._connection() as connection: row = await ( await connection.execute( "SELECT retry_not_before, attempt, blocked, recorded_at" " FROM adcp_reporting_producer_retry_schedules" " WHERE scope_key = %s", (scope_key,), ) ).fetchone() return ( RetryScheduleEntry(scope_key, _utc(row[0]), int(row[1]), row[2], _utc(row[3])) if row else None ) async def get_revision(self, *, account_id: str, reporting_revision_id: str) ‑> ReportingRevisionRecord | None-
Expand source code
async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: async with self._connection() as connection: row = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS} FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_revision_id = %s", (account_id, reporting_revision_id), ) ).fetchone() return _revision_from_row(row) if row else None async def lease_period_close(self, *, worker_id: str, now: datetime, lease_seconds: float) ‑> LeasedConfiguration | None-
Expand source code
async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: from psycopg.errors import LockNotAvailable from psycopg.pq import TransactionStatus moment = _utc(now) expires = moment + timedelta(seconds=lease_seconds) after = self._period_close_sample wait_until = None following = None result = None async with self._connection() as connection: # Never add a blocking account edge while a caller transaction may # already own locks. Standalone turns may queue only before the first # account acquisition, including acquisitions that find a stale row. can_wait = connection.info.transaction_status == TransactionStatus.IDLE async with connection.transaction(): await self._bound_lock_waits(connection) # A materializer installed by any participant adds an AFTER UPDATE # trigger taking the account lock. Sample without row locks, then take # that account lock before locking a configuration. The base store can # share that schema, so the ordering belongs here, not only on a tier. for _ in range(2): continuation = ( " AND (COALESCE(t.lease_turn,0)," " COALESCE(c.lease_expires_at,'-infinity'::timestamptz)," " c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_confi" "g_version)" " > (%s,COALESCE(%s::timestamptz,'-infinity'::timestamptz),%s,%s,%s,%s)" if after is not None else "" ) query = ( "SELECT c.account_id,c.consumer_id,c.delivery_config_id,c.delivery" "_config_version," # nosec B608 " COALESCE(t.lease_turn,0),c.lease_expires_at FROM" " reporting_configurations c" " LEFT JOIN adcp_reporting_configuration_lease_turns t" " ON (t.account_id,t.consumer_id,t.delivery_config_id,t.delivery_c" "onfig_version)" " = (c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_co" "nfig_version)" " WHERE NOT c.quarantined AND (c.lease_expires_at IS NULL OR c.lea" "se_expires_at<=%s)" + continuation + " ORDER BY COALESCE(t.lease_turn,0),c.lease_expires_at NULLS FIRST," " c.account_id,c.consumer_id,c.delivery_config_id,c.delivery_confi" "g_version LIMIT 32" ) rows = await ( await connection.execute(query, (moment, *(after or ()))) ).fetchall() if rows or after is None: break after = None for candidate in rows: row = candidate[:4] following = (candidate[4], candidate[5], *row) locked = await ( await connection.execute( "SELECT pg_try_advisory_xact_lock(hashtext('adcp.reporting:' || %s))", (row[0],), ) ).fetchone() if not locked[0]: if not can_wait: continue # A try-lock alone can starve behind a continuous queue of # ordinary writers, even though each writer commits promptly. # Join that queue briefly, sharing one wait budget per turn. # The savepoint rolls back a timed-out wait and its SET LOCAL; # successful acquisition restores the caller's lock timeout. if wait_until is None: wait_until = monotonic() + _LEASE_ACCOUNT_WAIT_SECONDS remaining_ms = int((wait_until - monotonic()) * 1000) if remaining_ms <= 0: continue try: async with connection.transaction(): previous, configured_ms = await ( await connection.execute( "SELECT current_setting('lock_timeout'),setting::integer" " FROM pg_settings WHERE name='lock_timeout'" ) ).fetchone() limit_ms = min(remaining_ms, configured_ms or remaining_ms) await connection.execute( "SELECT set_config('lock_timeout',%s,true)", (f"{limit_ms}ms",) ) await connection.execute( "SELECT pg_advisory_xact_lock(hashtext('adcp.reporting:' ||" " %s))", (row[0],), ) await connection.execute( "SELECT set_config('lock_timeout',%s,true)", (previous,) ) except LockNotAvailable: continue can_wait = False snapshot = await self._period_close_generations_on(connection, tuple(row)) acquired = await ( await connection.execute( "UPDATE reporting_configurations SET lease_worker_id = %s," " lease_expires_at = %s" " WHERE (account_id,consumer_id,delivery_config_id,delivery_config" "_version) = (" " SELECT c.account_id,c.consumer_id,c.delivery_config_id,c.deliver" "y_config_version" " FROM reporting_configurations c WHERE c.account_id=%s" " AND c.consumer_id=%s AND c.delivery_config_id=%s AND c.delivery_" "config_version=%s" " AND NOT c.quarantined AND (c.lease_expires_at IS NULL OR c.lease" "_expires_at<=%s)" " FOR UPDATE OF c SKIP LOCKED) RETURNING account_id", (worker_id, expires, *row, moment), ) ).fetchone() if acquired is None: continue await self._restore_period_close_generations_on( connection, tuple(row), snapshot ) await connection.execute( "INSERT INTO adcp_reporting_configuration_lease_turns" " (account_id,consumer_id,delivery_config_id,delivery_config_versi" "on,lease_turn)" " VALUES(%s,%s,%s,%s,nextval('adcp_reporting_configuration_lease_t" "urn_seq'))" " ON CONFLICT(account_id,consumer_id,delivery_config_id,delivery_c" "onfig_version)" " DO UPDATE SET" " lease_turn=nextval('adcp_reporting_configuration_lease_turn_seq')", tuple(row), ) result = LeasedConfiguration(row[0], row[1], row[2], row[3], expires) break # Only sampling uses this hint: leases and fairness ranks stay transactional. # Continue past a busy prefix on the next turn; a successful turn returns to # durable fairness order. Empty tails wrap at most once without row locks. self._period_close_sample = following if result is None else None return result async def list_adjustments(self, *, account_id: str, reporting_revision_ids: Sequence[str]) ‑> tuple[ReportingAdjustmentRecord, ...]-
Expand source code
async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: if not reporting_revision_ids: return () async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_ADJUSTMENT_COLUMNS} FROM reporting_adjustments" # noqa: S608 # nosec B608 " WHERE account_id = %s AND adjusts_reporting_revision_id = ANY(%s)" " ORDER BY created_at, reporting_adjustment_id", (account_id, list(reporting_revision_ids)), ) ).fetchall() return tuple(_adjustment_from_row(row) for row in rows) async def list_all_configurations(self) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: """One ordered scan for trusted service recovery, including retired generations.""" async with self._connection() as connection: rows = await ( await connection.execute( "SELECT delivery_config_id, delivery_config_version, account_id," " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, consumer_id, quarantined" " FROM reporting_configurations WHERE NOT quarantined" " ORDER BY account_id, consumer_id, delivery_config_id, delivery_config_version" ) ).fetchall() return tuple(_configuration_from_row(row) for row in rows)One ordered scan for trusted service recovery, including retired generations.
async def list_configurations(self,
*,
caller: ReportingCaller,
delivery_config_ids: Sequence[str] | None = None) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: async with self._connection() as connection: return await self._list_configurations_on( connection, account_id=caller.account_id, consumer_id=caller.consumer_id, delivery_config_ids=delivery_config_ids, ) async def list_consumer_statuses(self,
*,
account_id: str,
consumer_id: str,
reporting_obligation_ids: Sequence[str] | None = None) ‑> tuple[ConsumerStatusRecord, ...]-
Expand source code
async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: # Attaches by obligation id *or* by the exact logical period key, so a # chain filed before the obligation existed is not lost, forked, or # reset when the seller later repairs the missing obligation. clause = "" params: list[Any] = [account_id, consumer_id] if reporting_obligation_ids is not None: clause = ( " AND EXISTS (" " SELECT 1 FROM reporting_obligations o" " WHERE o.reporting_obligation_id = ANY(%s)" " AND o.account_id = s.account_id" " AND (o.reporting_obligation_id = s.reporting_obligation_id OR (" " o.delivery_config_id = s.delivery_config_id" " AND o.delivery_config_version = s.delivery_config_version" " AND o.report_definition_id = s.report_definition_id" " AND o.period_start = s.period_start" " AND o.period_end = s.period_end)))" ) params.append(list(reporting_obligation_ids)) async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_STATUS_COLUMNS} FROM reporting_consumer_statuses s" # noqa: S608 # nosec B608 f" WHERE s.account_id = %s AND s.consumer_id = %s{clause}" " ORDER BY s.recorded_at, s.reporting_status_id", tuple(params), ) ).fetchall() return tuple(_status_from_row(row) for row in rows) async def list_revisions(self, *, account_id: str, reporting_obligation_id: str) ‑> tuple[ReportingRevisionRecord, ...]-
Expand source code
async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: async with self._connection() as connection: rows = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS} FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s" " ORDER BY created_at, reporting_revision_id", (account_id, reporting_obligation_id), ) ).fetchall() return tuple(_revision_from_row(row) for row in rows) async def open_snapshot(self,
*,
caller: ReportingCaller,
filters_fingerprint: str) ‑> LedgerSnapshot-
Expand source code
async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: account_id = caller.account_id async with self._connection() as connection: row = await ( await connection.execute( "SELECT COALESCE(MAX(seq), 0), now() FROM reporting_ledger_changes" " WHERE account_id=%s AND consumer_id=%s AND record_kind IN" " ('obligation','revision','adjustment','consumer_status')", (account_id, caller.consumer_id), ) ).fetchone() assert row is not None max_sequence = int(row[0]) as_of = _utc(self._clock()) if self._clock is not None else _utc(row[1]) return LedgerSnapshot( snapshot_id="rpls_" + _fingerprint([account_id, caller.consumer_id, filters_fingerprint, max_sequence])[ :32 ], account_id=account_id, consumer_id=caller.consumer_id, ledger_as_of=as_of, max_sequence=max_sequence, ) async def put_configuration(self,
configuration: ReportingConfiguration) ‑> None-
Expand source code
async def put_configuration(self, configuration: ReportingConfiguration) -> None: reject_reserved_authoritative_party(configuration) key = configuration.generation_key payload = _configuration_payload(configuration) digest = _fingerprint(payload) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) inserted_cursor = await connection.execute( "INSERT INTO reporting_configurations" " (delivery_config_id, delivery_config_version, account_id," " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, content_sha256, consumer_id, quarantined)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s, %s, %s, %s," " %s::jsonb, %s, %s, %s, %s)" " ON CONFLICT (account_id, consumer_id, delivery_config_id, delive" "ry_config_version) DO NOTHING" " RETURNING delivery_config_id", ( key.delivery_config_id, key.delivery_config_version, key.account_id, configuration.report_definition_id, configuration.reporting_profile, configuration.feed_purpose, configuration.required_finality, configuration.account_timezone, _json(payload["schedule"]), _json(sorted(configuration.media_buy_ids)), configuration.activated_at, configuration.deactivated_at, configuration.automated_recovery_window.total_seconds(), configuration.status_retention_days, _json(payload["definition"]) if payload["definition"] else None, configuration.authoritative_party, digest, configuration.consumer_id, configuration.quarantined, ), ) inserted = await inserted_cursor.fetchone() # Check *after* the insert. ON CONFLICT waits for a concurrent # winner; a fresh READ COMMITTED statement sees its retained # content. A pre-insert check followed by DO NOTHING could silently # accept a different immutable generation from a losing writer. row = await ( await connection.execute( "SELECT content_sha256, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, quarantined" " FROM reporting_configurations" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s FOR UPDATE", ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) ).fetchone() if row is not None and row[5]: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy evidence is read-only" ) if row is None or row[0] != digest: raise LedgerConflictError( "CONFIGURATION_GENERATION_IMMUTABLE", f"configuration {key.delivery_config_id}@{key.delivery_config_version} " "already exists with different content for this account; publish a new " "version instead of editing a retained generation", ) scope = ReportingStatusScope(configuration.account_id, configuration.generation_key) if inserted is not None: if self._notifications_enabled: await self._dirty_status( connection, scope, "configuration", after=configuration_evidence(configuration), ) return # rc.3 carries activation/deactivation and the recovery/retention # windows as lifecycle state over one immutable generation, so a # re-put that changes only those must apply -- otherwise a # deactivated feed keeps minting obligations -- and must co-commit # its status-dirty generation on this exact connection. An # unchanged re-put stays a no-op and enqueues nothing. retained = replace( configuration, activated_at=_utc(row[1]) if row[1] else None, deactivated_at=_utc(row[2]) if row[2] else None, automated_recovery_window=timedelta(seconds=float(row[3])), status_retention_days=row[4], ) if configuration_lifecycle(retained) == configuration_lifecycle(configuration): return await connection.execute( "UPDATE reporting_configurations SET activated_at = %s, deactivated_at = %s," " automated_recovery_seconds = %s, status_retention_days = %s" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s", ( configuration.activated_at, configuration.deactivated_at, configuration.automated_recovery_window.total_seconds(), configuration.status_retention_days, key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ), ) if self._notifications_enabled: await self._dirty_status( connection, scope, "configuration", before=configuration_evidence(retained), after=configuration_evidence(configuration), ) async def read_page(self,
*,
snapshot: LedgerSnapshot,
consumer_id: str | None,
delivery_config_ids: Sequence[str] | None,
media_buy_ids: Sequence[str] | None,
offset: int,
limit: int,
changes_after_sequence: int | None,
feed_purposes: Sequence[str] | None = None,
period_start: datetime | None = None,
period_end: datetime | None = None) ‑> LedgerPage-
Expand source code
async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, feed_purposes: Sequence[str] | None = None, period_start: datetime | None = None, period_end: datetime | None = None, ) -> LedgerPage: if consumer_id not in {None, snapshot.consumer_id}: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "snapshot belongs to another caller" ) consumer_id = snapshot.consumer_id lower = changes_after_sequence or 0 async with self._connection() as connection: rows = await ( await connection.execute( "SELECT seq, record_kind, record_id FROM reporting_ledger_changes" " WHERE account_id = %s AND consumer_id=%s AND seq > %s AND seq <= %s" " ORDER BY seq", (snapshot.account_id, consumer_id, lower, snapshot.max_sequence), ) ).fetchall() selected: list[tuple[int, LedgerRecordKind, Any]] = [] for _seq, kind, record_id in rows: record = await self._resolve( connection, snapshot.account_id, kind, record_id, consumer_id=consumer_id ) if record is None: continue if not await self._in_scope( connection, kind, record, delivery_config_ids, media_buy_ids, consumer_id, feed_purposes, period_start, period_end, ): continue selected.append((_seq, kind, record)) window = selected[offset : offset + limit] has_more = offset + limit < len(selected) return LedgerPage( obligations=tuple(item[2] for item in window if item[1] == "obligation"), revisions=tuple(item[2] for item in window if item[1] == "revision"), adjustments=tuple(item[2] for item in window if item[1] == "adjustment"), consumer_statuses=tuple(item[2] for item in window if item[1] == "consumer_status"), total_count=len(selected), has_more=has_more, cursor=( encode_cursor({"snapshot": snapshot.snapshot_id, "offset": offset + limit}) if has_more else None ), ) async def read_revision_rows(self,
*,
account_id: str,
reporting_revision_id: str,
cursor: str | None = None,
limit: int = 500) ‑> ReportingRowPage-
Expand source code
async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: from adcp.reporting.ledger.store import revision_row_offset offset = revision_row_offset(cursor, reporting_revision_id, limit) async with self._connection() as connection: owned = await ( await connection.execute( "SELECT row_count FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s", (account_id, reporting_revision_id), ) ).fetchone() if owned is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") rows = await ( await connection.execute( "SELECT row_payload FROM reporting_revision_rows" " WHERE reporting_revision_id = %s" " ORDER BY ordinal OFFSET %s LIMIT %s", (reporting_revision_id, offset, limit), ) ).fetchall() total = int(owned[0]) has_more = offset + limit < total return ReportingRowPage( reporting_revision_id=reporting_revision_id, rows=tuple(row[0] for row in rows), total_count=total, has_more=has_more, cursor=( encode_cursor( {"ownership": 2, "revision": reporting_revision_id, "offset": offset + limit} ) if has_more else None ), ) async def read_status_snapshot(self,
*,
caller: ReportingCaller) ‑> ReportingStatusSnapshot-
Expand source code
async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.ledger.status_snapshot import settle_snapshot_on from adcp.reporting.materializer.capture import private_snapshot async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) return private_snapshot( await settle_snapshot_on(self, connection, account_id=caller.account_id), ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id), ) async def record_consumer_status(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: from adcp.reporting.evidence import consumer_reference consumer_reference(status.consumer_id) key = status.generation_key digest = _fingerprint(_consumer_status_payload(status)) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, status.account_id) replay = await self._replay(connection, status, digest) if replay is not None: return replay, False from adcp.reporting.ledger.status_snapshot import ( read_snapshot_on, validate_status_evidence, ) validate_status_evidence( status, await read_snapshot_on( connection, account_id=status.account_id, clock=self._clock, include_issue_scopes=await self._issue_scope_storage_on(connection), ), ) leaf = await ( await connection.execute( "SELECT reporting_status_id FROM reporting_consumer_statuses" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND report_definition_id = %s" " AND period_start = %s AND period_end = %s AND superseded = FALSE", ( key.account_id, status.consumer_id, key.delivery_config_id, key.delivery_config_version, status.report_definition_id, status.period_start, status.period_end, ), ) ).fetchone() if status.supersedes_reporting_status_id: if leaf is None or leaf[0] != status.supersedes_reporting_status_id: raise LedgerConflictError( "STATUS_SUPERSEDES_STALE", "supersedes_reporting_status_id must name this chain's current leaf; " "a stale pointer would let a successful retry erase a recorded outage", ) await connection.execute( "UPDATE reporting_consumer_statuses SET superseded = TRUE" " WHERE account_id = %s AND consumer_id = %s AND reporting_status_id = %s", (key.account_id, status.consumer_id, status.supersedes_reporting_status_id), ) elif leaf is not None: raise LedgerConflictError( "STATUS_SUPERSEDES_REQUIRED", "this chain already has a current statement; a new statement must " "explicitly supersede it", ) try: await connection.execute( "INSERT INTO reporting_consumer_statuses" " (reporting_status_id, account_id, consumer_id, delivery_config_id," " delivery_config_version, report_definition_id, period_start, period_end," " period_source_timezone, consumer_status, status_as_of, recorded_at," " supersedes_reporting_status_id, reporting_obligation_id," " reporting_revision_id, observed_revision_content_sha256, failure_code," " mismatch_code, consumer_commit_ref, seller_ledger_snapshot_id," " seller_ledger_as_of, superseded, content_sha256)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s," " %s, %s, %s, %s, %s, FALSE, %s)", ( status.reporting_status_id, status.account_id, status.consumer_id, status.delivery_config_id, status.delivery_config_version, status.report_definition_id, status.period_start, status.period_end, status.period_source_timezone, status.consumer_status, status.status_as_of, status.recorded_at, status.supersedes_reporting_status_id, status.reporting_obligation_id, status.reporting_revision_id, status.observed_revision_content_sha256, status.failure_code, status.mismatch_code, status.consumer_commit_ref, status.seller_ledger_snapshot_id, status.seller_ledger_as_of, digest, ), ) except Exception as error: raise _translate_integrity_error(error) from error await self._append_change( connection, status.account_id, "consumer_status", status.reporting_status_id, consumer_id=status.consumer_id, ) from adcp.reporting.ledger.status_snapshot import settle_snapshot_on await settle_snapshot_on(self, connection, account_id=status.account_id) if self._notifications_enabled: await self._dirty_status( connection, ReportingStatusScope( status.account_id, status.generation_key, status.reporting_obligation_id, status.consumer_id, ), "consumer_status", ReportingStatusEvidence("consumer_status", leaf[0]) if leaf else None, ReportingStatusEvidence( "consumer_status", status.reporting_status_id, supersedes_id=status.supersedes_reporting_status_id, ), ) return status, True async def record_consumer_status_with_lifecycle(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: """Optional participant: the record and lifecycle share the source transaction.""" return await self.record_consumer_status(status)Optional participant: the record and lifecycle share the source transaction.
async def record_restatement_checkpoint(self,
checkpoint: RestatementCheckpoint) ‑> RestatementCheckpoint-
Expand source code
async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: async with self._connection() as connection: obligation = await ( await connection.execute( "SELECT 1 FROM reporting_obligations" " WHERE reporting_obligation_id = %s AND account_id = %s", (checkpoint.reporting_obligation_id, checkpoint.account_id), ) ).fetchone() if obligation is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) row = await ( await connection.execute( "INSERT INTO reporting_restatement_checkpoints" " (account_id, reporting_obligation_id, checked_at, next_observation," " provisional_until)" " VALUES (%s, %s, %s, %s, %s)" " ON CONFLICT (reporting_obligation_id) DO UPDATE SET" " checked_at = EXCLUDED.checked_at," " next_observation = EXCLUDED.next_observation," " provisional_until = EXCLUDED.provisional_until" " WHERE reporting_restatement_checkpoints.account_id = EXCLUDED.account_id" " AND (reporting_restatement_checkpoints.next_observation" " < EXCLUDED.next_observation" " OR (reporting_restatement_checkpoints.next_observation" " = EXCLUDED.next_observation" " AND reporting_restatement_checkpoints.checked_at" " < EXCLUDED.checked_at))" " RETURNING account_id, reporting_obligation_id, checked_at," " next_observation, provisional_until", ( checkpoint.account_id, checkpoint.reporting_obligation_id, checkpoint.checked_at, checkpoint.next_observation, checkpoint.provisional_until, ), ) ).fetchone() if row is None: stored = await self.get_restatement_checkpoint( account_id=checkpoint.account_id, reporting_obligation_id=checkpoint.reporting_obligation_id, ) if stored is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "a restatement checkpoint must attach to an obligation for this account", ) return stored return RestatementCheckpoint( account_id=row[0], reporting_obligation_id=row[1], checked_at=_utc(row[2]), next_observation=int(row[3]), provisional_until=_utc(row[4]) if row[4] else None, ) async def record_retry_schedule(self,
entry: RetryScheduleEntry) ‑> RetryScheduleEntry-
Expand source code
async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: async with self._connection() as connection: row = await ( await connection.execute( "INSERT INTO adcp_reporting_producer_retry_schedules" " (scope_key, retry_not_before, attempt, blocked, recorded_at)" " VALUES (%s, %s, %s, %s, %s)" " ON CONFLICT (scope_key) DO UPDATE SET" " retry_not_before = CASE WHEN EXCLUDED.attempt = 0 OR EXCLUDED.blocked" " OR adcp_reporting_producer_retry_schedules.blocked OR %s" " THEN EXCLUDED.retry_not_before ELSE GREATEST(" " adcp_reporting_producer_retry_schedules.retry_not_before," " EXCLUDED.retry_not_before) END," " attempt = EXCLUDED.attempt, blocked = EXCLUDED.blocked," " recorded_at = EXCLUDED.recorded_at" " WHERE EXCLUDED.recorded_at >=" " adcp_reporting_producer_retry_schedules.recorded_at" " AND (EXCLUDED.attempt = 0 OR EXCLUDED.blocked OR %s" " OR (NOT adcp_reporting_producer_retry_schedules.blocked AND" " adcp_reporting_producer_retry_schedules.attempt <= EXCLUDED.attempt))" " RETURNING retry_not_before, attempt, blocked, recorded_at", ( entry.scope_key, entry.retry_not_before, entry.attempt, entry.blocked, entry.recorded_at, entry.replayed, entry.replayed, ), ) ).fetchone() if row is None: stored = await self.get_retry_schedule(scope_key=entry.scope_key) assert stored is not None return stored return RetryScheduleEntry(entry.scope_key, _utc(row[0]), int(row[1]), row[2], _utc(row[3])) async def release_period_close(self,
lease: LeasedConfiguration,
*,
worker_id: str) ‑> None-
Expand source code
async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: key = lease.generation_key identity = ( key.account_id, key.consumer_id, key.delivery_config_id, key.delivery_config_version, ) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, key.account_id) snapshot = await self._period_close_generations_on(connection, identity) released = await ( await connection.execute( "UPDATE reporting_configurations SET lease_worker_id = NULL," " lease_expires_at = NULL" " WHERE account_id = %s AND consumer_id = %s AND delivery_config_id = %s" " AND delivery_config_version = %s AND lease_worker_id = %s" " AND lease_expires_at = %s RETURNING account_id", (*identity, worker_id, lease.lease_expires_at), ) ).fetchone() if released is not None: await self._restore_period_close_generations_on(connection, identity, snapshot) async def reserve_provisional_acquisition(self, acquisition: ProvisionalAcquisition) ‑> ProvisionalAcquisition-
Expand source code
async def reserve_provisional_acquisition( self, acquisition: ProvisionalAcquisition ) -> ProvisionalAcquisition: async with self.transaction(), self._connection() as connection: await self._lock_account(connection, acquisition.account_id) await self._require_provisional_schema(connection) key = (acquisition.account_id, acquisition.obligation_id, acquisition.ordinal) existing = await ( await connection.execute( "SELECT payload FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND reporting_obligation_id=%s AND ordinal=%s", key, ) ).fetchone() if existing is not None: return ProvisionalAcquisition.from_wire(existing[0]) obligation = await self.get_obligation( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) if obligation is None: raise LedgerConflictError("OBLIGATION_NOT_FOUND", "unknown observation obligation") if not acquisition.binds(obligation): raise LedgerConflictError("OBSERVATION_CONFLICT", "acquisition generation differs") duplicate = await ( await connection.execute( "SELECT 1 FROM reporting_provisional_acquisitions" " WHERE account_id=%s AND source_execution_key=%s", (acquisition.account_id, acquisition.execution_key), ) ).fetchone() if duplicate is not None: raise LedgerConflictError( "OBSERVATION_CONFLICT", "execution key is already reserved" ) checkpoint = await self.get_restatement_checkpoint( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) expected = ( checkpoint.next_observation if checkpoint else len( await self.list_revisions( account_id=acquisition.account_id, reporting_obligation_id=acquisition.obligation_id, ) ) ) if acquisition.ordinal != expected: raise LedgerConflictError("OBSERVATION_CONFLICT", "observation ordinal changed") await connection.execute( "INSERT INTO reporting_provisional_acquisitions" " (account_id,reporting_obligation_id,ordinal,source_execution_key,payload)" " VALUES (%s,%s,%s,%s,%s::jsonb)", (*key, acquisition.execution_key, _json(acquisition.to_wire())), ) return ProvisionalAcquisition.from_wire(acquisition.to_wire()) async def resolve_consumer_status_replay(self,
status: ConsumerStatusRecord) ‑> ConsumerStatusRecord | None-
Expand source code
async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: async with self._connection() as connection: return await self._replay( connection, status, _fingerprint(_consumer_status_payload(status)) ) async def retire_issue(self,
*,
issue_key: str,
account_id: str,
at: datetime,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle | None-
Expand source code
async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle | None: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) return await self._retire_issue_on( connection, issue_key=issue_key, account_id=account_id, at=at, status_scope=status_scope, ) async def set_issue_state(self,
*,
issue_key: str,
account_id: str,
state: "Literal['acknowledged', 'waived']",
at: datetime,
external_ref: str | None = None,
status_scope: ReportingStatusScope | None = None) ‑> ReportingIssueLifecycle-
Expand source code
async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, status_scope: ReportingStatusScope | None = None, ) -> ReportingIssueLifecycle: if state not in {"acknowledged", "waived"}: raise LedgerConflictError( "ISSUE_STATE_NOT_OPERATOR_SETTABLE", f"issue_state {state!r} is not settable by an operator. 'resolved' is " "reachable only when the condition actually clears -- the projection " "retires it -- because a seller must not retire a mismatch out of a " "degraded projection while the statement that caused it is still the " "consumer's current leaf", ) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) live = await self._live_issue(connection, issue_key, account_id) if live is None: raise LedgerConflictError( "ISSUE_NOT_OPEN", f"no open issue {issue_key!r} for this account; a retired issue cannot be " "reopened, and a recurrence gets a new occurrence", ) check_issue_state_transition(live.issue_state, state) if state == "waived" and live.issue_state != "waived": from adcp.reporting.ledger.status_projection import bind_mismatch_waiver from adcp.reporting.ledger.status_snapshot import read_snapshot_on snapshot = await read_snapshot_on( connection, account_id=account_id, as_of=_utc(at), include_issue_scopes=await self._issue_scope_storage_on(connection), ) live = bind_mismatch_waiver(snapshot, live) await _save_waiver_binding(connection, live) waived_at = ( live.retired_at if live.waived_reporting_status_id is not None and live.retired_at is not None else _utc(at) ) await connection.execute( "UPDATE reporting_issue_lifecycle" " SET issue_state = %s," " external_ref = COALESCE(%s, external_ref)," " retired_at = CASE WHEN %s = 'waived' THEN %s ELSE retired_at END" " WHERE account_id = %s AND issue_key = %s AND generation = %s", ( state, external_ref, state, waived_at, account_id, issue_key, live.generation, ), ) refreshed = await self._issue_row(connection, issue_key, account_id, live.generation) assert refreshed is not None # Derive the no-op from the resulting row rather than predicting # it: an idempotent re-acknowledge changes nothing and enqueues # nothing, while anything that does move retained evidence stays # reconstructable for the projector. await self._dirty_issue(connection, refreshed, status_scope, live) return refreshed async def set_revision_readable(self, *, account_id: str, reporting_revision_id: str, readable: bool) ‑> None-
Expand source code
async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, account_id) existing = await ( await connection.execute( "SELECT readable, reporting_obligation_id FROM reporting_revisions" " WHERE account_id = %s AND reporting_revision_id = %s FOR UPDATE", (account_id, reporting_revision_id), ) ).fetchone() if existing is None: raise LedgerConflictError("REVISION_NOT_FOUND", "no such revision for this account") if existing[0] == readable: return await connection.execute( "UPDATE reporting_revisions SET readable = %s" " WHERE account_id = %s AND reporting_revision_id = %s", (readable, account_id, reporting_revision_id), ) if self._notifications_enabled: obligation = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # nosec B608 " WHERE account_id = %s AND reporting_obligation_id = %s", (account_id, existing[1]), ) ).fetchone() assert obligation is not None await self._dirty_status( connection, ReportingStatusScope.for_obligation(_obligation_from_row(obligation)), "readability", ReportingStatusEvidence( "revision", reporting_revision_id, readable=existing[0] ), ReportingStatusEvidence("revision", reporting_revision_id, readable=readable), ) async def transaction(self) ‑> AsyncIterator[PgReportingLedgerStore]-
Expand source code
@asynccontextmanager async def transaction(self) -> AsyncIterator[PgReportingLedgerStore]: """Group source operations into one committed status boundary. Pool ownership remains with the adopter. Nested turns are savepoints; callers acquire accounts in canonical order when touching several. """ async with self._connection() as connection, connection.transaction(): token = _BOUND_CONNECTION.set((self._pool, asyncio.current_task(), connection)) try: yield self finally: _BOUND_CONNECTION.reset(token)Group source operations into one committed status boundary.
Pool ownership remains with the adopter. Nested turns are savepoints; callers acquire accounts in canonical order when touching several.
class PgReportingReconciliationStore (*,
pool: AsyncConnectionPool,
clock: Callable[[], datetime] | None = None,
notifications: bool = False)-
Expand source code
class PgReportingReconciliationStore(PgReportingLedgerStore, _ReconciliationOperations): """Evidence and receipt heads commit with the feed, including autocommit pools. All state decisions run under the same per-account transaction lock as Core writes. Receipt replacement additionally uses a conditional head update; accepted heads and immutable evidence are protected by database triggers. """ async def _commit(self, record: RecordT) -> tuple[RecordT, bool]: try: return await self._commit_record(record) except Exception as error: # A concurrent/raw SQL writer must not turn a constraint detail # (which may include an entire row) into a provider-payload echo. if not str(getattr(error, "sqlstate", "")).startswith("23"): raise unavailable() async def _commit_record(self, record: RecordT) -> tuple[RecordT, bool]: candidate = cast(RecordT, decode_record(payload(record))) who = principal(candidate) async with self._connection() as connection: async with connection.transaction(): await self._lock_account(connection, who.account_id) return await self._commit_record_on(connection, candidate) async def _commit_record_on( self, connection: Any, record: RecordT, *, notify: bool = True, dirty: bool = True ) -> tuple[RecordT, bool]: """Connection-bound primitive. Caller holds the account transaction lock. Materializer finish uses this exact connection for evidence, caller feed, projection dirty work and its acknowledgment. No connection is acquired and no external code runs here. ``notify`` and ``dirty`` are independent: suppressing a readiness event must never also drop the projection work that an ordinary public write has always produced. """ candidate = decode_record(payload(record)) who = principal(candidate) records = await self._records(connection, who) existing = replay(candidate, records) if existing is not None: return cast(RecordT, existing), False context = await self._delivery_context(connection, candidate) now = await self._delivery_time_on(connection, candidate) stored = validate_transition(candidate, records, context, now) await self._insert(connection, stored) await self._append_reconciliation_change(connection, stored) if (notify or dirty) and self._notifications_enabled: from adcp.reporting.ledger.notification_events import ( delivery_dirty, materialization_event, ) if notify: event = materialization_event( stored, records, context.obligation, context.revision, context.configuration, now, ) if event is not None: await self._record_notification(connection, event) if dirty: scope, reason, evidence = delivery_dirty(stored, context.obligation) await self._dirty_status(connection, scope, reason, after=evidence) return cast(RecordT, stored), True async def _delivery_time_on(self, connection: Any, record: ReportingDeliveryRecord) -> datetime: """Connection-bound clock seam; old public clock overrides remain supported.""" if self._clock is not None: return self._clock() time_row = await (await connection.execute("SELECT clock_timestamp()")).fetchone() assert time_row is not None return cast(datetime, time_row[0]) async def _append_reconciliation_change( self, connection: Any, record: ReportingDeliveryRecord ) -> None: who = principal(record) row = await ( await connection.execute( "INSERT INTO reporting_reconciliation_heads (account_id, consumer_id, max_sequence)" " VALUES (%s, %s, 1) ON CONFLICT (account_id, consumer_id) DO UPDATE" " SET max_sequence = reporting_reconciliation_heads.max_sequence + 1" " RETURNING max_sequence", (who.account_id, who.consumer_id), ) ).fetchone() assert row is not None await connection.execute( "INSERT INTO reporting_reconciliation_changes" " (account_id, consumer_id, seq, namespace, record_id, record_kind," " change_id, content_sha256, committed_at)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, COALESCE(%s, clock_timestamp()))", ( who.account_id, who.consumer_id, row[0], *record_identity(record), record.kind, change_id(record), fingerprint(record), self._clock() if self._clock is not None else None, ), ) async def _validate_feed( self, connection: Any, who: ReportingDeliveryPrincipal ) -> tuple[int, datetime]: # Scope both sides before the join, count, or boundary. A missing/moved # record or feed row must fail, never disappear through an inner join. # The payload digest scan covers the caller's whole retained graph, not # just the rows a requested filter happens to select: a single retained # payload that disagrees with its fingerprint must fail every page. # The joined SQL fragment is constant; every caller value is parameterized. row = await ( await connection.execute( "SELECT count(c.seq), COALESCE(max(c.seq), 0)," # nosec B608 " COALESCE((SELECT max_sequence FROM reporting_reconciliation_heads" " WHERE account_id = %s AND consumer_id = %s), 0)," " COALESCE(bool_or(c.seq IS NULL OR r.record_id IS NULL), false)," " COALESCE((SELECT bool_or(x.content_sha256" " <> reporting_payload_sha256(x.payload))" " FROM reporting_reconciliation_records x" " WHERE x.account_id = %s AND x.consumer_id = %s), false), clock_timestamp()" " FROM (SELECT * FROM reporting_reconciliation_changes" " WHERE account_id = %s AND consumer_id = %s) c" " FULL JOIN (SELECT * FROM reporting_reconciliation_records" " WHERE account_id = %s AND consumer_id = %s) r ON " + _FEED_JOIN, (who.account_id, who.consumer_id) * 4, ) ).fetchone() assert row is not None if row[0] != row[1] or row[1] != row[2] or row[3] or row[4]: fail("REPORTING_HISTORY_CORRUPT") return row[2], row[5] async def _records( self, connection: Any, who: ReportingDeliveryPrincipal, maximum: int | None = None ) -> tuple[ReportingDeliveryRecord, ...]: await self._validate_feed(connection, who) return tuple(item.record for item in await self._changes(connection, who, maximum)) async def _changes( self, connection: Any, who: ReportingDeliveryPrincipal, maximum: int | None = None, *, after: int = 0, limit: int | None = None, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> tuple[ReportingReconciliationChange, ...]: rows = await ( await connection.execute( "SELECT r.payload, r.content_sha256, c.seq, r.change_id, " f"{_SELECT_IDENTITY} FROM reporting_reconciliation_changes c" # noqa: S608 # nosec B608 f" JOIN reporting_reconciliation_records r ON {_FEED_JOIN}" " WHERE c.account_id = %s AND c.consumer_id = %s" " AND (%s::bigint IS NULL OR c.seq <= %s::bigint) AND c.seq > %s" f" AND {_FEED_FILTER} ORDER BY c.seq LIMIT %s", ( who.account_id, who.consumer_id, maximum, maximum, after, *_filter_params(filters), limit, ), ) ).fetchall() records = tuple(decode_record(row[0]) for row in rows) if any( principal(record) != who or fingerprint(record) != row[1] or row[2] is None or change_id(record) != row[3] or storage_identity(record) != tuple(row[4:]) for record, row in zip(records, rows) ): fail("REPORTING_HISTORY_CORRUPT") return tuple( ReportingReconciliationChange(row[2], record) for row, record in zip(rows, records) ) async def _change_count( self, connection: Any, caller: ReportingDeliveryPrincipal, after: int, maximum: int, filters: ReportingReconciliationFilter, ) -> int: row = await ( await connection.execute( "SELECT count(*) FROM reporting_reconciliation_changes c" f" JOIN reporting_reconciliation_records r ON {_FEED_JOIN}" # noqa: S608 # nosec B608 " WHERE c.account_id = %s AND c.consumer_id = %s AND c.seq > %s AND c.seq <= %s" f" AND {_FEED_FILTER}", (caller.account_id, caller.consumer_id, after, maximum, *_filter_params(filters)), ) ).fetchone() assert row is not None return int(row[0]) async def _delivery_context( self, connection: Any, record: ReportingDeliveryRecord ) -> DeliveryContext: who = principal(record) configuration = None if isinstance(record, ReportingDestinationBinding) or self._notifications_enabled: generation = ( record.generation_key if isinstance(record, ReportingDestinationBinding) else record.scope.generation_key ) row = await ( await connection.execute( "SELECT delivery_config_id, delivery_config_version, account_id," " report_definition_id, reporting_profile, feed_purpose, required_finality," " account_timezone, schedule, media_buy_ids, activated_at, deactivated_at," " automated_recovery_seconds, status_retention_days, definition," " authoritative_party, consumer_id, quarantined" " FROM reporting_configurations WHERE account_id = %s" " AND consumer_id = %s AND delivery_config_id = %s AND delivery_co" "nfig_version = %s", ( who.account_id, who.consumer_id, generation.delivery_config_id, generation.delivery_config_version, ), ) ).fetchone() configuration = _configuration_from_row(row) if row else None if isinstance(record, ReportingDestinationBinding): return DeliveryContext(configuration=configuration) obligation_row = await ( await connection.execute( f"SELECT {_OBLIGATION_COLUMNS} FROM reporting_obligations" # noqa: S608 # nosec B608 " WHERE account_id = %s AND consumer_id = %s AND reporting_obligation_id = %s", (who.account_id, who.consumer_id, record.scope.reporting_obligation_id), ) ).fetchone() revision_id = getattr(record, "reporting_revision_id", None) if isinstance(record, ReportingAdjustmentReceiptRecord): revision_id = record.adjusts_reporting_revision_id revision_row = await ( await connection.execute( f"SELECT {_REVISION_COLUMNS} FROM reporting_revisions" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_revision_id = %s", (who.account_id, revision_id), ) ).fetchone() adjustment_row = None if isinstance(record, ReportingAdjustmentReceiptRecord): adjustment_row = await ( await connection.execute( f"SELECT {_ADJUSTMENT_COLUMNS} FROM reporting_adjustments" # noqa: S608 # nosec B608 " WHERE account_id = %s AND reporting_adjustment_id = %s", (who.account_id, record.reporting_adjustment_id), ) ).fetchone() return DeliveryContext( configuration=configuration, obligation=_obligation_from_row(obligation_row) if obligation_row else None, revision=_revision_from_row(revision_row) if revision_row else None, adjustment=_adjustment_from_row(adjustment_row) if adjustment_row else None, ) async def _insert(self, connection: Any, record: ReportingDeliveryRecord) -> None: await connection.execute( "INSERT INTO reporting_reconciliation_records" " (account_id, consumer_id, namespace, record_id, record_kind, delivery_config_id," " delivery_config_version, reporting_obligation_id, reporting_revision_id," " reporting_materialization_id, reporting_adjustment_id, attempt_number," " receipt_chain_key, receipt_status, supersedes_receipt_id, payload," " content_sha256, change_id)" " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s," " %s::jsonb, %s, %s)", ( *storage_identity(record), _json(payload(record)), fingerprint(record), change_id(record), ), ) async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: requested = boundary is not None async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) maximum, now = await self._validate_feed(connection, caller) if boundary is None: boundary = change_boundary( caller, maximum, self._clock() if self._clock is not None else now ) if ( type(boundary) is not ReportingReconciliationSnapshotToken or boundary.caller != caller or boundary.min_sequence != 0 or boundary.filters != ReportingReconciliationFilter() ): unavailable() validate_boundary(caller, boundary, maximum) records = tuple( item.record for item in await self._changes(connection, caller, boundary.max_sequence) ) if len(records) != boundary.total_count: # See _boundary_unavailable: the caller's token, not the store. _boundary_unavailable(requested) return ReportingReconciliationSnapshot(caller, boundary, records) async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: after, boundary, last_key = read_position(caller, changes_after, cursor, limit, filters) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) maximum, now = await self._validate_feed(connection, caller) if boundary is None: count = await self._change_count(connection, caller, after, maximum, filters) boundary = change_boundary( caller, maximum, self._clock() if self._clock is not None else now, after=after, total_count=count, filters=filters, ) validate_boundary(caller, boundary, maximum) if last_key is not None: last = await self._changes( connection, caller, after, after=after - 1, limit=1, filters=filters ) if not last or change_id(last[0].record) != last_key: raise LedgerConflictError( "INVALID_CHECKPOINT", "reconciliation key is unavailable" ) count = await self._change_count( connection, caller, boundary.min_sequence, boundary.max_sequence, filters ) if count != boundary.total_count: _boundary_unavailable(True) changes = await self._changes( connection, caller, boundary.max_sequence, after=after, limit=limit + 1, filters=filters, ) return change_page(caller, boundary, after, changes, limit)Evidence and receipt heads commit with the feed, including autocommit pools.
All state decisions run under the same per-account transaction lock as Core writes. Receipt replacement additionally uses a conditional head update; accepted heads and immutable evidence are protected by database triggers.
Ancestors
- PgReportingLedgerStore
- adcp.reporting.ledger.delivery._ReconciliationOperations
Subclasses
Methods
async def read_reconciliation_changes(self,
*,
caller: ReportingDeliveryPrincipal,
changes_after: ReportingReconciliationCheckpoint | None = None,
cursor: ReportingReconciliationCursor | None = None,
limit: int = 100,
filters: ReportingReconciliationFilter = ReportingReconciliationFilter(record_kinds=(), reporting_obligation_id=None)) ‑> ReportingReconciliationPage-
Expand source code
async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: after, boundary, last_key = read_position(caller, changes_after, cursor, limit, filters) async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) maximum, now = await self._validate_feed(connection, caller) if boundary is None: count = await self._change_count(connection, caller, after, maximum, filters) boundary = change_boundary( caller, maximum, self._clock() if self._clock is not None else now, after=after, total_count=count, filters=filters, ) validate_boundary(caller, boundary, maximum) if last_key is not None: last = await self._changes( connection, caller, after, after=after - 1, limit=1, filters=filters ) if not last or change_id(last[0].record) != last_key: raise LedgerConflictError( "INVALID_CHECKPOINT", "reconciliation key is unavailable" ) count = await self._change_count( connection, caller, boundary.min_sequence, boundary.max_sequence, filters ) if count != boundary.total_count: _boundary_unavailable(True) changes = await self._changes( connection, caller, boundary.max_sequence, after=after, limit=limit + 1, filters=filters, ) return change_page(caller, boundary, after, changes, limit) async def read_reconciliation_snapshot(self,
*,
caller: ReportingDeliveryPrincipal,
boundary: ReportingReconciliationSnapshotToken | None = None) ‑> ReportingReconciliationSnapshot-
Expand source code
async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: requested = boundary is not None async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) maximum, now = await self._validate_feed(connection, caller) if boundary is None: boundary = change_boundary( caller, maximum, self._clock() if self._clock is not None else now ) if ( type(boundary) is not ReportingReconciliationSnapshotToken or boundary.caller != caller or boundary.min_sequence != 0 or boundary.filters != ReportingReconciliationFilter() ): unavailable() validate_boundary(caller, boundary, maximum) records = tuple( item.record for item in await self._changes(connection, caller, boundary.max_sequence) ) if len(records) != boundary.total_count: # See _boundary_unavailable: the caller's token, not the store. _boundary_unavailable(requested) return ReportingReconciliationSnapshot(caller, boundary, records)
Inherited members
class ProducerOfferings (snapshot_offering_id: str | None = None,
official_offering_id: str | None = None,
publication_namespace: str = 'reporting-source:default',
requested_metrics: tuple[str, ...] = ('impressions', 'spend'),
requested_dimensions: tuple[str, ...] = (),
currency: str = 'USD',
source_scope: dict[str, Any] = <factory>,
slice_timeout: timedelta = datetime.timedelta(seconds=600))-
Expand source code
@dataclass(frozen=True) class ProducerOfferings: """Which source offering serves which finality for one configuration. Snapshot and official source publications use different offerings. A source-declared settling policy may use them sequentially for one ledger obligation, so the producer needs both named explicitly rather than inferring one from the other. """ snapshot_offering_id: str | None = None official_offering_id: str | None = None publication_namespace: str = "reporting-source:default" requested_metrics: tuple[str, ...] = ("impressions", "spend") requested_dimensions: tuple[str, ...] = () currency: str = "USD" source_scope: dict[str, Any] = field(default_factory=dict) slice_timeout: timedelta = timedelta(minutes=10) def offering_for(self, finality: str) -> str | None: return self.official_offering_id if finality == "official" else self.snapshot_offering_idWhich source offering serves which finality for one configuration.
Snapshot and official source publications use different offerings. A source-declared settling policy may use them sequentially for one ledger obligation, so the producer needs both named explicitly rather than inferring one from the other.
Instance variables
var currency : strvar official_offering_id : str | Nonevar publication_namespace : strvar requested_dimensions : tuple[str, ...]var requested_metrics : tuple[str, ...]var slice_timeout : datetime.timedeltavar snapshot_offering_id : str | Nonevar source_scope : dict[str, typing.Any]
Methods
def offering_for(self, finality: str) ‑> str | None-
Expand source code
def offering_for(self, finality: str) -> str | None: return self.official_offering_id if finality == "official" else self.snapshot_offering_id
class ReportingAdjustmentReceiptRecord (scope: ReportingDeliveryScope,
reporting_receipt_id: str,
reporting_adjustment_id: str,
adjusts_reporting_revision_id: str,
status: ReceiptStatus,
observed_adjustment_sha256: str,
observed_at: datetime,
supersedes_reporting_receipt_id: str | None = None,
rejection_codes: tuple[str, ...] = (),
received_at: datetime | None = None,
*,
kind: "Literal['adjustment_receipt']" = 'adjustment_receipt')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id)ReportingAdjustmentReceiptRecord(scope: 'ReportingDeliveryScope', reporting_receipt_id: 'str', reporting_adjustment_id: 'str', adjusts_reporting_revision_id: 'str', status: 'ReceiptStatus', observed_adjustment_sha256: 'str', observed_at: 'datetime', supersedes_reporting_receipt_id: 'str | None' = None, rejection_codes: 'tuple[str, …]' = (), received_at: 'datetime | None' = None, *, kind: "Literal['adjustment_receipt']" = 'adjustment_receipt')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var adjusts_reporting_revision_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) prop key : ReportingReceiptKey-
Expand source code
@property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var kind : Literal['adjustment_receipt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_adjustment_sha256 : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var received_at : datetime.datetime | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var rejection_codes : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var reporting_adjustment_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var reporting_receipt_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var status : Literal['accepted', 'rejected']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var supersedes_reporting_receipt_id : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingAdjustmentReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_adjustment_id: str adjusts_reporting_revision_id: str status: ReceiptStatus observed_adjustment_sha256: str observed_at: datetime supersedes_reporting_receipt_id: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["adjustment_receipt"] = field(default="adjustment_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_adjustment_id, maximum=255) reporting_identifier(self.adjusts_reporting_revision_id, maximum=255) sha256_value(self.observed_adjustment_sha256) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id)
class ReportingAdjustmentRecord (reporting_adjustment_id: str,
account_id: str,
adjusts_reporting_revision_id: str,
reason_code: "Literal['invalid_traffic', 'late_attribution', 'source_correction', 'mapping_correction', 'commercial_adjustment', 'other']",
accounting_period_start: datetime,
accounting_period_end: datetime,
control_total_deltas: tuple[tuple[str, str], ...],
correction_observed_at: datetime,
created_at: datetime,
reason_detail: str | None = None,
managed_control_total_deltas: tuple[ReportingControlTotalRecord, ...] | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingAdjustmentRecord: """An immutable post-official accounting correction. An official revision is terminal. When the source corrects it, the correction lands here as a signed delta against an open accounting period, preserving the original invoice-to-revision binding rather than rewriting it. """ reporting_adjustment_id: str account_id: str adjusts_reporting_revision_id: str reason_code: Literal[ "invalid_traffic", "late_attribution", "source_correction", "mapping_correction", "commercial_adjustment", "other", ] accounting_period_start: datetime accounting_period_end: datetime control_total_deltas: tuple[tuple[str, str], ...] correction_observed_at: datetime created_at: datetime reason_detail: str | None = None managed_control_total_deltas: tuple[ReportingControlTotalRecord, ...] | None = None def __post_init__(self) -> None: object.__setattr__( self, "control_total_deltas", tuple(tuple(item) for item in self.control_total_deltas) ) if self.managed_control_total_deltas is not None: object.__setattr__( self, "managed_control_total_deltas", freeze_control_totals(self.managed_control_total_deltas, self.control_total_deltas), )An immutable post-official accounting correction.
An official revision is terminal. When the source corrects it, the correction lands here as a signed delta against an open accounting period, preserving the original invoice-to-revision binding rather than rewriting it.
Instance variables
var account_id : strvar accounting_period_end : datetime.datetimevar accounting_period_start : datetime.datetimevar adjusts_reporting_revision_id : strvar control_total_deltas : tuple[tuple[str, str], ...]var correction_observed_at : datetime.datetimevar created_at : datetime.datetimevar managed_control_total_deltas : tuple[ReportingControlTotalRecord, ...] | Nonevar reason_code : Literal['invalid_traffic', 'late_attribution', 'source_correction', 'mapping_correction', 'commercial_adjustment', 'other']var reason_detail : str | Nonevar reporting_adjustment_id : str
class ReportingCaller (*args, **kwargs)-
Expand source code
class ReportingCaller(Protocol): """Authenticated account/caller coordinates supplied by a trusted transport.""" @property def account_id(self) -> str: ... @property def consumer_id(self) -> str: ...Authenticated account/caller coordinates supplied by a trusted transport.
Ancestors
- typing.Protocol
- typing.Generic
Instance variables
prop account_id : str-
Expand source code
@property def account_id(self) -> str: ... prop consumer_id : str-
Expand source code
@property def consumer_id(self) -> str: ...
class ReportingCanonicalDigest (value: str,
canonicalization_id: str,
canonicalization_uri: str,
canonicalization_sha256: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingCanonicalDigest: """A trusted publisher's logical-row digest under a pinned managed contract. This is distinct from Core's fixed ``revision_content_sha256``. A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation. """ value: str canonicalization_id: str canonicalization_uri: str canonicalization_sha256: str __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if any( type(value) is not str for value in ( self.value, self.canonicalization_id, self.canonicalization_uri, self.canonicalization_sha256, ) ): raise ValueError("canonical evidence requires immutable string values") sha256_value(self.value) sha256_value(self.canonicalization_sha256) _public_text(self.canonicalization_id, maximum=128) uri = self.canonicalization_uri try: parsed = urlsplit(uri) valid = ( parsed.scheme == "https" and parsed.hostname is not None and re.fullmatch(r"(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}", parsed.hostname) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment and uri.isascii() and not re.search(r"[\s\\%]", uri) and not re.search(r"(?i)(token|secret|signature|credential)", uri) ) except (TypeError, ValueError): valid = False if not valid: raise ValueError("canonicalization requires a public HTTPS contract URI") def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, } @classmethod def from_wire(cls, value: object) -> ReportingCanonicalDigest: if ( not isinstance(value, dict) or set(value) != { "algorithm", "value", "canonicalization_id", "canonicalization_uri", "canonicalization_sha256", } or value.get("algorithm") != "sha256" ): raise ValueError("invalid retained canonical evidence") return cls( value["value"], value["canonicalization_id"], value["canonicalization_uri"], value["canonicalization_sha256"], )A trusted publisher's logical-row digest under a pinned managed contract.
This is distinct from Core's fixed
revision_content_sha256(). A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation.Static methods
def from_wire(value: object) ‑> ReportingCanonicalDigest
Instance variables
var canonicalization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingCanonicalDigest: """A trusted publisher's logical-row digest under a pinned managed contract. This is distinct from Core's fixed ``revision_content_sha256``. A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation. """ value: str canonicalization_id: str canonicalization_uri: str canonicalization_sha256: str __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if any( type(value) is not str for value in ( self.value, self.canonicalization_id, self.canonicalization_uri, self.canonicalization_sha256, ) ): raise ValueError("canonical evidence requires immutable string values") sha256_value(self.value) sha256_value(self.canonicalization_sha256) _public_text(self.canonicalization_id, maximum=128) uri = self.canonicalization_uri try: parsed = urlsplit(uri) valid = ( parsed.scheme == "https" and parsed.hostname is not None and re.fullmatch(r"(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}", parsed.hostname) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment and uri.isascii() and not re.search(r"[\s\\%]", uri) and not re.search(r"(?i)(token|secret|signature|credential)", uri) ) except (TypeError, ValueError): valid = False if not valid: raise ValueError("canonicalization requires a public HTTPS contract URI") def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, } @classmethod def from_wire(cls, value: object) -> ReportingCanonicalDigest: if ( not isinstance(value, dict) or set(value) != { "algorithm", "value", "canonicalization_id", "canonicalization_uri", "canonicalization_sha256", } or value.get("algorithm") != "sha256" ): raise ValueError("invalid retained canonical evidence") return cls( value["value"], value["canonicalization_id"], value["canonicalization_uri"], value["canonicalization_sha256"], ) var canonicalization_sha256 : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingCanonicalDigest: """A trusted publisher's logical-row digest under a pinned managed contract. This is distinct from Core's fixed ``revision_content_sha256``. A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation. """ value: str canonicalization_id: str canonicalization_uri: str canonicalization_sha256: str __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if any( type(value) is not str for value in ( self.value, self.canonicalization_id, self.canonicalization_uri, self.canonicalization_sha256, ) ): raise ValueError("canonical evidence requires immutable string values") sha256_value(self.value) sha256_value(self.canonicalization_sha256) _public_text(self.canonicalization_id, maximum=128) uri = self.canonicalization_uri try: parsed = urlsplit(uri) valid = ( parsed.scheme == "https" and parsed.hostname is not None and re.fullmatch(r"(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}", parsed.hostname) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment and uri.isascii() and not re.search(r"[\s\\%]", uri) and not re.search(r"(?i)(token|secret|signature|credential)", uri) ) except (TypeError, ValueError): valid = False if not valid: raise ValueError("canonicalization requires a public HTTPS contract URI") def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, } @classmethod def from_wire(cls, value: object) -> ReportingCanonicalDigest: if ( not isinstance(value, dict) or set(value) != { "algorithm", "value", "canonicalization_id", "canonicalization_uri", "canonicalization_sha256", } or value.get("algorithm") != "sha256" ): raise ValueError("invalid retained canonical evidence") return cls( value["value"], value["canonicalization_id"], value["canonicalization_uri"], value["canonicalization_sha256"], ) var canonicalization_uri : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingCanonicalDigest: """A trusted publisher's logical-row digest under a pinned managed contract. This is distinct from Core's fixed ``revision_content_sha256``. A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation. """ value: str canonicalization_id: str canonicalization_uri: str canonicalization_sha256: str __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if any( type(value) is not str for value in ( self.value, self.canonicalization_id, self.canonicalization_uri, self.canonicalization_sha256, ) ): raise ValueError("canonical evidence requires immutable string values") sha256_value(self.value) sha256_value(self.canonicalization_sha256) _public_text(self.canonicalization_id, maximum=128) uri = self.canonicalization_uri try: parsed = urlsplit(uri) valid = ( parsed.scheme == "https" and parsed.hostname is not None and re.fullmatch(r"(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}", parsed.hostname) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment and uri.isascii() and not re.search(r"[\s\\%]", uri) and not re.search(r"(?i)(token|secret|signature|credential)", uri) ) except (TypeError, ValueError): valid = False if not valid: raise ValueError("canonicalization requires a public HTTPS contract URI") def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, } @classmethod def from_wire(cls, value: object) -> ReportingCanonicalDigest: if ( not isinstance(value, dict) or set(value) != { "algorithm", "value", "canonicalization_id", "canonicalization_uri", "canonicalization_sha256", } or value.get("algorithm") != "sha256" ): raise ValueError("invalid retained canonical evidence") return cls( value["value"], value["canonicalization_id"], value["canonicalization_uri"], value["canonicalization_sha256"], ) var value : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingCanonicalDigest: """A trusted publisher's logical-row digest under a pinned managed contract. This is distinct from Core's fixed ``revision_content_sha256``. A future managed publisher must verify the pinned contract and compute this value before destination work; destination output cannot establish the expectation. """ value: str canonicalization_id: str canonicalization_uri: str canonicalization_sha256: str __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if any( type(value) is not str for value in ( self.value, self.canonicalization_id, self.canonicalization_uri, self.canonicalization_sha256, ) ): raise ValueError("canonical evidence requires immutable string values") sha256_value(self.value) sha256_value(self.canonicalization_sha256) _public_text(self.canonicalization_id, maximum=128) uri = self.canonicalization_uri try: parsed = urlsplit(uri) valid = ( parsed.scheme == "https" and parsed.hostname is not None and re.fullmatch(r"(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}", parsed.hostname) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment and uri.isascii() and not re.search(r"[\s\\%]", uri) and not re.search(r"(?i)(token|secret|signature|credential)", uri) ) except (TypeError, ValueError): valid = False if not valid: raise ValueError("canonicalization requires a public HTTPS contract URI") def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, } @classmethod def from_wire(cls, value: object) -> ReportingCanonicalDigest: if ( not isinstance(value, dict) or set(value) != { "algorithm", "value", "canonicalization_id", "canonicalization_uri", "canonicalization_sha256", } or value.get("algorithm") != "sha256" ): raise ValueError("invalid retained canonical evidence") return cls( value["value"], value["canonicalization_id"], value["canonicalization_uri"], value["canonicalization_sha256"], )
Methods
def to_wire(self) ‑> dict[str, str]-
Expand source code
def to_wire(self) -> dict[str, str]: return { "algorithm": "sha256", "value": self.value, "canonicalization_id": self.canonicalization_id, "canonicalization_uri": self.canonicalization_uri, "canonicalization_sha256": self.canonicalization_sha256, }
class ReportingConfiguration (delivery_config_id: str,
delivery_config_version: int,
account_id: str,
consumer_id: str,
report_definition_id: str,
reporting_profile: str,
feed_purpose: str,
schedule: ReportingScheduleSpec,
required_finality: ReportingFinality,
account_timezone: str = 'UTC',
activated_at: datetime | None = None,
deactivated_at: datetime | None = None,
media_buy_ids: tuple[str, ...] = (),
automated_recovery_window: timedelta = datetime.timedelta(seconds=21600),
status_retention_days: int = 400,
definition: ReportingDefinitionBinding | None = None,
authoritative_party: "Literal['seller', 'consumer']" = 'seller',
quarantined: bool = False)-
Expand source code
@dataclass(frozen=True) class ReportingConfiguration: """One accepted reporting configuration generation. A generation is immutable. Campaign starts, stops, and configuration changes alter *future* obligations; they never alter past ones, because a buyer that retained this generation must be able to re-derive the same expectations from it years later. """ delivery_config_id: str delivery_config_version: int account_id: str consumer_id: str report_definition_id: str reporting_profile: str feed_purpose: str schedule: ReportingScheduleSpec required_finality: ReportingFinality account_timezone: str = "UTC" activated_at: datetime | None = None deactivated_at: datetime | None = None media_buy_ids: tuple[str, ...] = () automated_recovery_window: timedelta = timedelta(hours=6) status_retention_days: int = 400 definition: ReportingDefinitionBinding | None = None # AdCP 3.2.0-rc.3 reserves this for the buyer-deposited billing revision # task scoped to a later minor. ``seller`` is the default and the only # value any 3.2 seller accepts; ``consumer`` is carried rather than # dropped so :meth:`ReportingLedgerStore.put_configuration` can reject it # with UNSUPPORTED_FEATURE. The spec forbids silently coercing it. authoritative_party: Literal["seller", "consumer"] = "seller" # Maintenance imports are read-only; recovery requires a new generation. quarantined: bool = False def __post_init__(self) -> None: principal_reference(self.account_id) consumer_reference(self.consumer_id) @property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, )One accepted reporting configuration generation.
A generation is immutable. Campaign starts, stops, and configuration changes alter future obligations; they never alter past ones, because a buyer that retained this generation must be able to re-derive the same expectations from it years later.
Instance variables
var account_id : strvar account_timezone : strvar activated_at : datetime.datetime | Nonevar automated_recovery_window : datetime.timedeltavar consumer_id : strvar deactivated_at : datetime.datetime | Nonevar definition : ReportingDefinitionBinding | Nonevar delivery_config_id : strvar delivery_config_version : intvar feed_purpose : strprop generation_key : ReportingConfigurationGenerationKey-
Expand source code
@property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) var media_buy_ids : tuple[str, ...]var quarantined : boolvar report_definition_id : strvar reporting_profile : strvar required_finality : Literal['snapshot', 'official']var schedule : ReportingScheduleSpecvar status_retention_days : int
class ReportingConfigurationGenerationKey (account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int)-
Expand source code
@dataclass(frozen=True) class ReportingConfigurationGenerationKey: """The caller/account-qualified identity of one accepted configuration generation. ``delivery_config_id`` is caller-selected and may be reused by another account. Use this value for lookups, joins, and leases rather than a tuple that could omit the account. It is immutable and hashable for use in maps. """ account_id: str consumer_id: str delivery_config_id: str delivery_config_version: int def __post_init__(self) -> None: principal_reference(self.account_id) consumer_reference(self.consumer_id)The caller/account-qualified identity of one accepted configuration generation.
delivery_config_idis caller-selected and may be reused by another account. Use this value for lookups, joins, and leases rather than a tuple that could omit the account. It is immutable and hashable for use in maps.Instance variables
var account_id : strvar consumer_id : strvar delivery_config_id : strvar delivery_config_version : int
class ReportingControlTotalRecord (name: str,
value: str,
value_type: "Literal['integer', 'decimal']",
unit: str | None = None)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingControlTotalRecord: """An exact expected or observed wire total, with its declared type and unit.""" name: str value: str value_type: Literal["integer", "decimal"] unit: str | None = None __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if ( any(type(value) is not str for value in (self.name, self.value, self.value_type)) or (self.unit is not None and type(self.unit) is not str) or self.value_type not in {"integer", "decimal"} ): raise ValueError("control total evidence requires immutable typed values") reporting_identifier(self.name, maximum=128) if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,127}", self.name) is None: raise ValueError("reporting total names require public metric identifiers") pattern = ( r"-?(?:0|[1-9][0-9]*)" if self.value_type == "integer" else r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?" ) if re.fullmatch(pattern, self.value) is None: raise ValueError("control totals require canonical numeric strings matching their type") if self.unit is not None: _public_text(self.unit, maximum=32) def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result @classmethod def from_wire(cls, value: object) -> ReportingControlTotalRecord: if ( not isinstance(value, dict) or not {"name", "value", "value_type"} <= set(value) or set(value) - {"name", "value", "value_type", "unit"} ): raise ValueError("invalid retained control total evidence") return cls(value["name"], value["value"], value["value_type"], value.get("unit"))An exact expected or observed wire total, with its declared type and unit.
Static methods
def from_wire(value: object) ‑> ReportingControlTotalRecord
Instance variables
var name : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingControlTotalRecord: """An exact expected or observed wire total, with its declared type and unit.""" name: str value: str value_type: Literal["integer", "decimal"] unit: str | None = None __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if ( any(type(value) is not str for value in (self.name, self.value, self.value_type)) or (self.unit is not None and type(self.unit) is not str) or self.value_type not in {"integer", "decimal"} ): raise ValueError("control total evidence requires immutable typed values") reporting_identifier(self.name, maximum=128) if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,127}", self.name) is None: raise ValueError("reporting total names require public metric identifiers") pattern = ( r"-?(?:0|[1-9][0-9]*)" if self.value_type == "integer" else r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?" ) if re.fullmatch(pattern, self.value) is None: raise ValueError("control totals require canonical numeric strings matching their type") if self.unit is not None: _public_text(self.unit, maximum=32) def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result @classmethod def from_wire(cls, value: object) -> ReportingControlTotalRecord: if ( not isinstance(value, dict) or not {"name", "value", "value_type"} <= set(value) or set(value) - {"name", "value", "value_type", "unit"} ): raise ValueError("invalid retained control total evidence") return cls(value["name"], value["value"], value["value_type"], value.get("unit")) var unit : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingControlTotalRecord: """An exact expected or observed wire total, with its declared type and unit.""" name: str value: str value_type: Literal["integer", "decimal"] unit: str | None = None __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if ( any(type(value) is not str for value in (self.name, self.value, self.value_type)) or (self.unit is not None and type(self.unit) is not str) or self.value_type not in {"integer", "decimal"} ): raise ValueError("control total evidence requires immutable typed values") reporting_identifier(self.name, maximum=128) if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,127}", self.name) is None: raise ValueError("reporting total names require public metric identifiers") pattern = ( r"-?(?:0|[1-9][0-9]*)" if self.value_type == "integer" else r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?" ) if re.fullmatch(pattern, self.value) is None: raise ValueError("control totals require canonical numeric strings matching their type") if self.unit is not None: _public_text(self.unit, maximum=32) def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result @classmethod def from_wire(cls, value: object) -> ReportingControlTotalRecord: if ( not isinstance(value, dict) or not {"name", "value", "value_type"} <= set(value) or set(value) - {"name", "value", "value_type", "unit"} ): raise ValueError("invalid retained control total evidence") return cls(value["name"], value["value"], value["value_type"], value.get("unit")) var value : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingControlTotalRecord: """An exact expected or observed wire total, with its declared type and unit.""" name: str value: str value_type: Literal["integer", "decimal"] unit: str | None = None __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if ( any(type(value) is not str for value in (self.name, self.value, self.value_type)) or (self.unit is not None and type(self.unit) is not str) or self.value_type not in {"integer", "decimal"} ): raise ValueError("control total evidence requires immutable typed values") reporting_identifier(self.name, maximum=128) if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,127}", self.name) is None: raise ValueError("reporting total names require public metric identifiers") pattern = ( r"-?(?:0|[1-9][0-9]*)" if self.value_type == "integer" else r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?" ) if re.fullmatch(pattern, self.value) is None: raise ValueError("control totals require canonical numeric strings matching their type") if self.unit is not None: _public_text(self.unit, maximum=32) def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result @classmethod def from_wire(cls, value: object) -> ReportingControlTotalRecord: if ( not isinstance(value, dict) or not {"name", "value", "value_type"} <= set(value) or set(value) - {"name", "value", "value_type", "unit"} ): raise ValueError("invalid retained control total evidence") return cls(value["name"], value["value"], value["value_type"], value.get("unit")) var value_type : Literal['integer', 'decimal']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingControlTotalRecord: """An exact expected or observed wire total, with its declared type and unit.""" name: str value: str value_type: Literal["integer", "decimal"] unit: str | None = None __pydantic_config__: ClassVar[ConfigDict] = ConfigDict( extra="forbid", hide_input_in_errors=True ) def __post_init__(self) -> None: if ( any(type(value) is not str for value in (self.name, self.value, self.value_type)) or (self.unit is not None and type(self.unit) is not str) or self.value_type not in {"integer", "decimal"} ): raise ValueError("control total evidence requires immutable typed values") reporting_identifier(self.name, maximum=128) if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,127}", self.name) is None: raise ValueError("reporting total names require public metric identifiers") pattern = ( r"-?(?:0|[1-9][0-9]*)" if self.value_type == "integer" else r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?" ) if re.fullmatch(pattern, self.value) is None: raise ValueError("control totals require canonical numeric strings matching their type") if self.unit is not None: _public_text(self.unit, maximum=32) def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result @classmethod def from_wire(cls, value: object) -> ReportingControlTotalRecord: if ( not isinstance(value, dict) or not {"name", "value", "value_type"} <= set(value) or set(value) - {"name", "value", "value_type", "unit"} ): raise ValueError("invalid retained control total evidence") return cls(value["name"], value["value"], value["value_type"], value.get("unit"))
Methods
def to_wire(self) ‑> dict[str, str]-
Expand source code
def to_wire(self) -> dict[str, str]: result = {"name": self.name, "value": self.value, "value_type": self.value_type} if self.unit is not None: result["unit"] = self.unit return result
class ReportingCurrencyError (code: str, message: str)-
Expand source code
class ReportingCurrencyError(ValueError): """Reporting money cannot be interpreted safely; ``code`` is stable.""" def __init__(self, code: str, message: str) -> None: super().__init__(f"{code}: {message}") self.code = codeReporting money cannot be interpreted safely;
codeis stable.Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class ReportingDefinitionBinding (report_definition_uri: str,
report_definition_sha256: str,
schema_version: str,
schema_uri: str,
schema_sha256: str,
schema_dialect: str = 'https://json-schema.org/draft/2020-12/schema',
schema_ref_policy: str = 'local_fragment_only',
monetary_metric_units: tuple[tuple[str, str], ...] = (),
monetary_control_total_units: tuple[tuple[str, str], ...] = ())-
Expand source code
@dataclass(frozen=True) class ReportingDefinitionBinding: """The content-addressed report definition an obligation reports against. Core's wire records are self-describing: a retained revision names the exact definition and row schema it was produced under, by URI *and* digest, so a consumer reading it years later can verify it is reading what it thinks it is. Without this, ``report_definition_id`` is just a string two parties hope still means the same thing. Optional on :class:`ReportingConfiguration` only so an in-process pilot can get moving; a seller advertising ``reporting.core`` must supply it, because :class:`~adcp.reporting.ledger.status.ReportingStatusHandler` cannot emit a schema-valid ``reporting-revision`` record without it. """ report_definition_uri: str report_definition_sha256: str schema_version: str schema_uri: str schema_sha256: str schema_dialect: str = "https://json-schema.org/draft/2020-12/schema" schema_ref_policy: str = "local_fragment_only" # Trusted projections of the content-addressed definition, not adapter or # buyer context. Tuples keep these declarations immutable after acceptance. monetary_metric_units: tuple[tuple[str, str], ...] = () monetary_control_total_units: tuple[tuple[str, str], ...] = () def __post_init__(self) -> None: for field_name in ("monetary_metric_units", "monetary_control_total_units"): units = tuple( (name, validate_currency(unit)) for name, unit in getattr(self, field_name) ) if len(dict(units)) != len(units): raise ValueError(f"duplicate names in {field_name}") object.__setattr__(self, field_name, units) def to_storage(self) -> dict[str, Any]: """Retain monetary semantics without changing AdCP wire records or old hashes.""" payload = self.to_wire() if self.monetary_metric_units: payload["monetary_metric_units"] = list(self.monetary_metric_units) if self.monetary_control_total_units: payload["monetary_control_total_units"] = list(self.monetary_control_total_units) return payload def to_wire(self) -> dict[str, Any]: return { "report_definition_uri": self.report_definition_uri, "report_definition_sha256": self.report_definition_sha256, "schema_version": self.schema_version, "schema_uri": self.schema_uri, "schema_sha256": self.schema_sha256, "schema_dialect": self.schema_dialect, "schema_ref_policy": self.schema_ref_policy, }The content-addressed report definition an obligation reports against.
Core's wire records are self-describing: a retained revision names the exact definition and row schema it was produced under, by URI and digest, so a consumer reading it years later can verify it is reading what it thinks it is. Without this,
report_definition_idis just a string two parties hope still means the same thing.Optional on :class:
ReportingConfigurationonly so an in-process pilot can get moving; a seller advertisingreporting.coremust supply it, because :class:~adcp.reporting.ledger.status.ReportingStatusHandlercannot emit a schema-validreporting-revisionrecord without it.Instance variables
var monetary_control_total_units : tuple[tuple[str, str], ...]var monetary_metric_units : tuple[tuple[str, str], ...]var report_definition_sha256 : strvar report_definition_uri : strvar schema_dialect : strvar schema_ref_policy : strvar schema_sha256 : strvar schema_uri : strvar schema_version : str
Methods
def to_storage(self) ‑> dict[str, typing.Any]-
Expand source code
def to_storage(self) -> dict[str, Any]: """Retain monetary semantics without changing AdCP wire records or old hashes.""" payload = self.to_wire() if self.monetary_metric_units: payload["monetary_metric_units"] = list(self.monetary_metric_units) if self.monetary_control_total_units: payload["monetary_control_total_units"] = list(self.monetary_control_total_units) return payloadRetain monetary semantics without changing AdCP wire records or old hashes.
def to_wire(self) ‑> dict[str, typing.Any]-
Expand source code
def to_wire(self) -> dict[str, Any]: return { "report_definition_uri": self.report_definition_uri, "report_definition_sha256": self.report_definition_sha256, "schema_version": self.schema_version, "schema_uri": self.schema_uri, "schema_sha256": self.schema_sha256, "schema_dialect": self.schema_dialect, "schema_ref_policy": self.schema_ref_policy, }
class ReportingDeliveryEscalation (consumer_mismatch_escalation: timedelta | None = None,
operations_contact_url: str | None = None,
operations_contact_email: str | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingDeliveryEscalation: """The seller's advertised escalation commitment for consumer mismatches. Both halves of AdCP 3.2.0-rc.3's ``reporting_delivery_capabilities.consumer_mismatch_escalation_seconds`` / ``operations_contact`` pair. The schema makes the contact mandatory when the window is advertised, so this class refuses the window without one: committing to escalate with nowhere to escalate *to* is the failure the requirement exists to prevent. ``operations_contact`` is inert human-facing metadata. Agents surface it to an operator and MUST NOT fetch the URL, send protocol traffic to it, or treat either value as a credential or a callback. """ consumer_mismatch_escalation: timedelta | None = None operations_contact_url: str | None = None operations_contact_email: str | None = None def __post_init__(self) -> None: has_contact = bool(self.operations_contact_url or self.operations_contact_email) if self.consumer_mismatch_escalation is not None and not has_contact: raise ValueError( "consumer_mismatch_escalation requires operations_contact_url or " "operations_contact_email; the schema makes the contact mandatory so the " "escalation has a destination" ) if ( self.consumer_mismatch_escalation is not None and self.consumer_mismatch_escalation.total_seconds() < 0 ): raise ValueError("consumer_mismatch_escalation cannot be negative") if ( self.consumer_mismatch_escalation is not None and self.consumer_mismatch_escalation.total_seconds() % 1 ): raise ValueError("consumer_mismatch_escalation requires integral seconds") if self.operations_contact_url is not None and not self.operations_contact_url.startswith( "https://" ): # Same hardened public-origin shape as the offering document URIs. # Enforcing the scheme here keeps "never dereference" true by # construction for the obvious loopback/credentialed cases. raise ValueError("operations_contact_url must be an https:// URL") def to_wire(self) -> dict[str, Any]: """The fragment a seller merges into its advertised capability block.""" payload: dict[str, Any] = {} if self.consumer_mismatch_escalation is not None: payload["consumer_mismatch_escalation_seconds"] = int( self.consumer_mismatch_escalation.total_seconds() ) contact = { key: value for key, value in ( ("url", self.operations_contact_url), ("email", self.operations_contact_email), ) if value is not None } if contact: payload["operations_contact"] = contact return payloadThe seller's advertised escalation commitment for consumer mismatches.
Both halves of AdCP 3.2.0-rc.3's
reporting_delivery_capabilities.consumer_mismatch_escalation_seconds/operations_contactpair. The schema makes the contact mandatory when the window is advertised, so this class refuses the window without one: committing to escalate with nowhere to escalate to is the failure the requirement exists to prevent.operations_contactis inert human-facing metadata. Agents surface it to an operator and MUST NOT fetch the URL, send protocol traffic to it, or treat either value as a credential or a callback.Instance variables
var consumer_mismatch_escalation : datetime.timedelta | Nonevar operations_contact_email : str | Nonevar operations_contact_url : str | None
Methods
def to_wire(self) ‑> dict[str, typing.Any]-
Expand source code
def to_wire(self) -> dict[str, Any]: """The fragment a seller merges into its advertised capability block.""" payload: dict[str, Any] = {} if self.consumer_mismatch_escalation is not None: payload["consumer_mismatch_escalation_seconds"] = int( self.consumer_mismatch_escalation.total_seconds() ) contact = { key: value for key, value in ( ("url", self.operations_contact_url), ("email", self.operations_contact_email), ) if value is not None } if contact: payload["operations_contact"] = contact return payloadThe fragment a seller merges into its advertised capability block.
class ReportingDeliveryPrincipal (account_id: str, consumer_id: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryPrincipal(_ClosedValue): """Trusted account and consumer identity, resolved from authenticated transport.""" account_id: str consumer_id: str def __post_init__(self) -> None: _freeze_fields(self) principal_reference(self.account_id) from adcp.reporting.evidence import consumer_reference consumer_reference(self.consumer_id)Trusted account and consumer identity, resolved from authenticated transport.
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var account_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryPrincipal(_ClosedValue): """Trusted account and consumer identity, resolved from authenticated transport.""" account_id: str consumer_id: str def __post_init__(self) -> None: _freeze_fields(self) principal_reference(self.account_id) from adcp.reporting.evidence import consumer_reference consumer_reference(self.consumer_id) var consumer_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryPrincipal(_ClosedValue): """Trusted account and consumer identity, resolved from authenticated transport.""" account_id: str consumer_id: str def __post_init__(self) -> None: _freeze_fields(self) principal_reference(self.account_id) from adcp.reporting.evidence import consumer_reference consumer_reference(self.consumer_id)
class ReportingDeliveryScope (generation_key: ReportingConfigurationGenerationKey,
consumer_id: str,
reporting_obligation_id: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryScope(_ClosedValue): generation_key: ReportingConfigurationGenerationKey consumer_id: str reporting_obligation_id: str def __post_init__(self) -> None: _freeze_fields(self) if type(self.generation_key) is not ReportingConfigurationGenerationKey: raise ValueError("reporting scope requires the typed configuration generation") self.principal if self.consumer_id != self.generation_key.consumer_id: raise ValueError("reporting generation belongs to another caller") reporting_identifier(self.generation_key.delivery_config_id, maximum=64) _positive(self.generation_key.delivery_config_version) reporting_identifier(self.reporting_obligation_id, maximum=255) @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id)ReportingDeliveryScope(generation_key: 'ReportingConfigurationGenerationKey', consumer_id: 'str', reporting_obligation_id: 'str')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var consumer_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryScope(_ClosedValue): generation_key: ReportingConfigurationGenerationKey consumer_id: str reporting_obligation_id: str def __post_init__(self) -> None: _freeze_fields(self) if type(self.generation_key) is not ReportingConfigurationGenerationKey: raise ValueError("reporting scope requires the typed configuration generation") self.principal if self.consumer_id != self.generation_key.consumer_id: raise ValueError("reporting generation belongs to another caller") reporting_identifier(self.generation_key.delivery_config_id, maximum=64) _positive(self.generation_key.delivery_config_version) reporting_identifier(self.reporting_obligation_id, maximum=255) @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var generation_key : ReportingConfigurationGenerationKey-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryScope(_ClosedValue): generation_key: ReportingConfigurationGenerationKey consumer_id: str reporting_obligation_id: str def __post_init__(self) -> None: _freeze_fields(self) if type(self.generation_key) is not ReportingConfigurationGenerationKey: raise ValueError("reporting scope requires the typed configuration generation") self.principal if self.consumer_id != self.generation_key.consumer_id: raise ValueError("reporting generation belongs to another caller") reporting_identifier(self.generation_key.delivery_config_id, maximum=64) _positive(self.generation_key.delivery_config_version) reporting_identifier(self.reporting_obligation_id, maximum=255) @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) prop principal : ReportingDeliveryPrincipal-
Expand source code
@property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var reporting_obligation_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDeliveryScope(_ClosedValue): generation_key: ReportingConfigurationGenerationKey consumer_id: str reporting_obligation_id: str def __post_init__(self) -> None: _freeze_fields(self) if type(self.generation_key) is not ReportingConfigurationGenerationKey: raise ValueError("reporting scope requires the typed configuration generation") self.principal if self.consumer_id != self.generation_key.consumer_id: raise ValueError("reporting generation belongs to another caller") reporting_identifier(self.generation_key.delivery_config_id, maximum=64) _positive(self.generation_key.delivery_config_version) reporting_identifier(self.reporting_obligation_id, maximum=255) @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id)
class ReportingDestinationBinding (generation_key: ReportingConfigurationGenerationKey,
consumer_id: str,
destination_ref: str,
trusted_binding_ref: str,
method: DeliveryMethod,
transport: str,
verification_profile: VerificationProfile,
reconciliation_mode: "Literal['delivery_only', 'consumer_receipt']",
feed_purpose: "Literal['pacing', 'analytics', 'billing']",
resource_retention_days: int,
created_at: datetime,
format: ReportingFormat | None = None,
reader_compatibility: tuple[str, ...] = (),
success_status: "Literal['available', 'delivered']" = 'available',
*,
kind: "Literal['destination_binding']" = 'destination_binding')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id)Immutable public portion of one trusted principal/configuration binding.
trusted_binding_refidentifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability.Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var consumer_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var created_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var destination_ref : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var feed_purpose : Literal['pacing', 'analytics', 'billing']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var format : Literal['jsonl', 'csv', 'parquet', 'avro', 'orc'] | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var generation_key : ReportingConfigurationGenerationKey-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var kind : Literal['destination_binding']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var method : Literal['file_transfer', 'dataset_share', 'warehouse_materialization']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) prop principal : ReportingDeliveryPrincipal-
Expand source code
@property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var reader_compatibility : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var reconciliation_mode : Literal['delivery_only', 'consumer_receipt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var resource_retention_days : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var success_status : Literal['available', 'delivered']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var transport : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var trusted_binding_ref : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id) var verification_profile : Literal['canonical_digest', 'manifest_checksums', 'native_commit']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingDestinationBinding(_ClosedValue): """Immutable public portion of one trusted principal/configuration binding. ``trusted_binding_ref`` identifies an immutable trusted configuration, not a mutable 'latest' alias. Credentials are resolved later behind that reference. Storing this record neither configures a writer nor advertises a capability. """ generation_key: ReportingConfigurationGenerationKey consumer_id: str destination_ref: str trusted_binding_ref: str = field(repr=False) method: DeliveryMethod transport: str verification_profile: VerificationProfile reconciliation_mode: Literal["delivery_only", "consumer_receipt"] feed_purpose: Literal["pacing", "analytics", "billing"] resource_retention_days: int created_at: datetime format: ReportingFormat | None = None reader_compatibility: tuple[str, ...] = () success_status: Literal["available", "delivered"] = "available" kind: Literal["destination_binding"] = field(default="destination_binding", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) ReportingDeliveryScope(self.generation_key, self.consumer_id, "binding") destination_reference(self.destination_ref) destination_reference(self.trusted_binding_ref) if re.fullmatch(r"[a-z][a-z0-9_.-]{0,63}", self.transport) is None: raise ValueError("reporting transport requires a public protocol label") reporting_identifier(self.transport, maximum=64) _positive(self.resource_retention_days) object.__setattr__(self, "created_at", aware_utc(self.created_at)) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") if self.method == "file_transfer" and self.format is None: raise ValueError("file transfer requires a declared format") if self.verification_profile == "manifest_checksums" and self.method != "file_transfer": raise ValueError("manifest checksum verification requires file transfer") if self.method == "warehouse_materialization" and self.success_status != "delivered": raise ValueError("warehouse materialization requires destination delivery") if self.method == "dataset_share" and self.success_status != "available": raise ValueError("dataset share requires representative-consumer availability") if self.feed_purpose == "billing" and self.verification_profile != "canonical_digest": raise ValueError("billing receipts require canonical digest verification") @property def principal(self) -> ReportingDeliveryPrincipal: return ReportingDeliveryPrincipal(self.generation_key.account_id, self.consumer_id)
class ReportingDestinationStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingDestinationStore(Protocol): """Trusted configuration ingestion. Never accept a destination from a receipt body.""" async def put_destination_binding( self, record: ReportingDestinationBinding ) -> tuple[ReportingDestinationBinding, bool]: ... async def bind_obligation_delivery( self, record: ReportingObligationDeliveryRecord ) -> tuple[ReportingObligationDeliveryRecord, bool]: ... async def get_destination_binding( self, *, caller: ReportingDeliveryPrincipal, generation_key: ReportingConfigurationGenerationKey, ) -> ReportingDestinationBinding | None: ... async def get_obligation_delivery( self, scope: ReportingDeliveryScope ) -> ReportingObligationDeliveryRecord | None: ...Trusted configuration ingestion. Never accept a destination from a receipt body.
Ancestors
- typing.Protocol
- typing.Generic
Subclasses
Methods
async def bind_obligation_delivery(self,
record: ReportingObligationDeliveryRecord) ‑> tuple[ReportingObligationDeliveryRecord, bool]-
Expand source code
async def bind_obligation_delivery( self, record: ReportingObligationDeliveryRecord ) -> tuple[ReportingObligationDeliveryRecord, bool]: ... async def get_destination_binding(self,
*,
caller: ReportingDeliveryPrincipal,
generation_key: ReportingConfigurationGenerationKey) ‑> ReportingDestinationBinding | None-
Expand source code
async def get_destination_binding( self, *, caller: ReportingDeliveryPrincipal, generation_key: ReportingConfigurationGenerationKey, ) -> ReportingDestinationBinding | None: ... async def get_obligation_delivery(self,
scope: ReportingDeliveryScope) ‑> ReportingObligationDeliveryRecord | None-
Expand source code
async def get_obligation_delivery( self, scope: ReportingDeliveryScope ) -> ReportingObligationDeliveryRecord | None: ... async def put_destination_binding(self,
record: ReportingDestinationBinding) ‑> tuple[ReportingDestinationBinding, bool]-
Expand source code
async def put_destination_binding( self, record: ReportingDestinationBinding ) -> tuple[ReportingDestinationBinding, bool]: ...
class ReportingIssue (issue_id: str,
code: str,
severity: "Literal['info', 'delayed', 'action_required']",
responsible_party: "Literal['buyer', 'seller', 'provider']",
recommended_action: str,
reporting_obligation_id: str | None = None,
delivery_config_id: str | None = None,
delivery_config_version: int | None = None,
feed_purpose: str | None = None,
media_buy_ids: tuple[str, ...] = (),
period_start: datetime | None = None,
period_end: datetime | None = None,
expected_at: datetime | None = None,
reporting_status_id: str | None = None,
message: str | None = None,
opened_at: datetime | None = None,
issue_state: ReportingIssueStateValue | None = None,
external_ref: str | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingIssue: """A typed, actionable statement about why a scope is not healthy. Issue identity is stable and derived, not stored: the same unresolved condition yields the same ``issue_id`` on every read, so a consumer can deduplicate across polls and a notification can reference it. """ issue_id: str code: str severity: Literal["info", "delayed", "action_required"] responsible_party: Literal["buyer", "seller", "provider"] recommended_action: str reporting_obligation_id: str | None = None delivery_config_id: str | None = None delivery_config_version: int | None = None feed_purpose: str | None = None media_buy_ids: tuple[str, ...] = () period_start: datetime | None = None period_end: datetime | None = None expected_at: datetime | None = None reporting_status_id: str | None = None message: str | None = None # AdCP 3.2.0-rc.3 issue lifecycle. ``opened_at`` is when the seller first # observed this logical condition and MUST NOT advance while the same # ``issue_id`` is re-emitted -- it anchors the escalation clock, so a # re-emission that reset it would let a seller hold an unresolved mismatch # below action_required forever. Required on CONSUMER_STATUS_MISMATCH. opened_at: datetime | None = None issue_state: ReportingIssueStateValue | None = None # Inert correlation text for the party's own tracker. Never dereferenced, # resolved, or executed, and never reused across callers on a # caller-scoped issue -- that would leak one tenant's blast radius to # another. external_ref: str | None = None def to_wire(self) -> dict[str, Any]: payload: dict[str, Any] = { "issue_id": self.issue_id, "code": self.code, "severity": self.severity, "responsible_party": self.responsible_party, "recommended_action": self.recommended_action, } optional: dict[str, Any] = { "reporting_obligation_id": self.reporting_obligation_id, "delivery_config_id": self.delivery_config_id, "delivery_config_version": self.delivery_config_version, "feed_purpose": self.feed_purpose, "period_start": self.period_start, "period_end": self.period_end, "expected_at": self.expected_at, "reporting_status_id": self.reporting_status_id, "message": self.message, "opened_at": self.opened_at, "issue_state": self.issue_state, "external_ref": self.external_ref, } for key, value in optional.items(): if value is not None: payload[key] = value.isoformat() if isinstance(value, datetime) else value if self.media_buy_ids: payload["media_buy_ids"] = list(self.media_buy_ids) return payloadA typed, actionable statement about why a scope is not healthy.
Issue identity is stable and derived, not stored: the same unresolved condition yields the same
issue_idon every read, so a consumer can deduplicate across polls and a notification can reference it.Instance variables
var code : strvar delivery_config_id : str | Nonevar delivery_config_version : int | Nonevar expected_at : datetime.datetime | Nonevar external_ref : str | Nonevar feed_purpose : str | Nonevar issue_id : strvar issue_state : Literal['open', 'acknowledged', 'resolved', 'waived'] | Nonevar media_buy_ids : tuple[str, ...]var message : str | Nonevar opened_at : datetime.datetime | Nonevar period_end : datetime.datetime | Nonevar period_start : datetime.datetime | Nonevar recommended_action : strvar reporting_obligation_id : str | Nonevar reporting_status_id : str | Nonevar responsible_party : Literal['buyer', 'seller', 'provider']var severity : Literal['info', 'delayed', 'action_required']
Methods
def to_wire(self) ‑> dict[str, typing.Any]-
Expand source code
def to_wire(self) -> dict[str, Any]: payload: dict[str, Any] = { "issue_id": self.issue_id, "code": self.code, "severity": self.severity, "responsible_party": self.responsible_party, "recommended_action": self.recommended_action, } optional: dict[str, Any] = { "reporting_obligation_id": self.reporting_obligation_id, "delivery_config_id": self.delivery_config_id, "delivery_config_version": self.delivery_config_version, "feed_purpose": self.feed_purpose, "period_start": self.period_start, "period_end": self.period_end, "expected_at": self.expected_at, "reporting_status_id": self.reporting_status_id, "message": self.message, "opened_at": self.opened_at, "issue_state": self.issue_state, "external_ref": self.external_ref, } for key, value in optional.items(): if value is not None: payload[key] = value.isoformat() if isinstance(value, datetime) else value if self.media_buy_ids: payload["media_buy_ids"] = list(self.media_buy_ids) return payload
class ReportingIssueLifecycle (issue_key: str,
issue_id: str,
account_id: str,
opened_at: datetime,
issue_state: ReportingIssueStateValue = 'open',
generation: int = 1,
consumer_id: str | None = None,
external_ref: str | None = None,
retired_at: datetime | None = None,
waived_reporting_status_id: str | None = None,
waived_conflict_sha256: str | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingIssueLifecycle: """Durable state for one logical issue, so ``opened_at`` can stay fixed. Issue *identity* elsewhere in this module is derived (see :func:`~adcp.reporting.ledger.health.issue_id_for`) because the conditions it names are monotone for one immutable obligation. A consumer mismatch is not monotone: the buyer can supersede its statement, the seller can restate, and the same logical disagreement can go quiet and come back. AdCP 3.2.0-rc.3 also requires ``opened_at`` to survive every re-emission, because it anchors the escalation clock -- a derived timestamp would reset on each poll and an unattended mismatch would never escalate. ``issue_key`` identifies the *condition*; ``issue_id`` identifies one *occurrence* of it. Resolution bumps ``generation`` on recurrence; an exact bilateral waiver preserves its terminal row and a later disagreement uses a new private condition key. Both receive a new ``issue_id`` and ``opened_at`` as the spec requires, while an unresolved condition keeps both across a severity change from ``delayed`` to ``action_required``. """ issue_key: str issue_id: str account_id: str opened_at: datetime issue_state: ReportingIssueStateValue = "open" generation: int = 1 #: Caller-scoped issues (every consumer mismatch) carry the consumer whose #: statement caused them. ``None`` is a seller-wide condition. consumer_id: str | None = None external_ref: str | None = None retired_at: datetime | None = None #: Private rc.6 waiver binding, captured under the source transaction. #: A legacy waiver without this evidence cannot suppress a new projection. waived_reporting_status_id: str | None = None waived_conflict_sha256: str | None = None def __post_init__(self) -> None: principal_reference(self.account_id) if self.consumer_id is not None: consumer_reference(self.consumer_id) @property def live(self) -> bool: """Whether this occurrence still stands, publishable or not. ``waived`` retains its terminal record and suppresses only its exact statement/conflict binding. A later disagreement uses a new private condition key, leaving this audit record and the one-live-row constraint intact. Agreement never rewrites a waiver as ``resolved``. ``resolved`` frees the condition to recur under a new ``issue_id``, and only the projection can set it (see :meth:`~adcp.reporting.ledger.store.ReportingLedgerStore.retire_issue`). """ return self.issue_state in {"open", "acknowledged", "waived"} @property def published(self) -> bool: """Whether this occurrence belongs in ``issues[]``. Only ``open`` and ``acknowledged``. Retiring an issue removes it from the projection rather than publishing it in a terminal state, so a reader that treats a nonempty ``issues[]`` as degradation stays correct. Waived occurrences contribute neither an issue nor degradation. """ return self.issue_state in {"open", "acknowledged"}Durable state for one logical issue, so
opened_atcan stay fixed.Issue identity elsewhere in this module is derived (see :func:
~adcp.reporting.ledger.health.issue_id_for) because the conditions it names are monotone for one immutable obligation. A consumer mismatch is not monotone: the buyer can supersede its statement, the seller can restate, and the same logical disagreement can go quiet and come back. AdCP 3.2.0-rc.3 also requiresopened_atto survive every re-emission, because it anchors the escalation clock – a derived timestamp would reset on each poll and an unattended mismatch would never escalate.issue_keyidentifies the condition;issue_ididentifies one occurrence of it. Resolution bumpsgenerationon recurrence; an exact bilateral waiver preserves its terminal row and a later disagreement uses a new private condition key. Both receive a newissue_idandopened_atas the spec requires, while an unresolved condition keeps both across a severity change fromdelayedtoaction_required.Instance variables
var account_id : strvar consumer_id : str | None-
Caller-scoped issues (every consumer mismatch) carry the consumer whose statement caused them.
Noneis a seller-wide condition. var external_ref : str | Nonevar generation : intvar issue_id : strvar issue_key : strvar issue_state : Literal['open', 'acknowledged', 'resolved', 'waived']prop live : bool-
Expand source code
@property def live(self) -> bool: """Whether this occurrence still stands, publishable or not. ``waived`` retains its terminal record and suppresses only its exact statement/conflict binding. A later disagreement uses a new private condition key, leaving this audit record and the one-live-row constraint intact. Agreement never rewrites a waiver as ``resolved``. ``resolved`` frees the condition to recur under a new ``issue_id``, and only the projection can set it (see :meth:`~adcp.reporting.ledger.store.ReportingLedgerStore.retire_issue`). """ return self.issue_state in {"open", "acknowledged", "waived"}Whether this occurrence still stands, publishable or not.
waivedretains its terminal record and suppresses only its exact statement/conflict binding. A later disagreement uses a new private condition key, leaving this audit record and the one-live-row constraint intact. Agreement never rewrites a waiver asresolved.resolvedfrees the condition to recur under a newissue_id, and only the projection can set it (see :meth:~adcp.reporting.ledger.store.ReportingLedgerStore.retire_issue). var opened_at : datetime.datetimeprop published : bool-
Expand source code
@property def published(self) -> bool: """Whether this occurrence belongs in ``issues[]``. Only ``open`` and ``acknowledged``. Retiring an issue removes it from the projection rather than publishing it in a terminal state, so a reader that treats a nonempty ``issues[]`` as degradation stays correct. Waived occurrences contribute neither an issue nor degradation. """ return self.issue_state in {"open", "acknowledged"}Whether this occurrence belongs in
issues[].Only
openandacknowledged. Retiring an issue removes it from the projection rather than publishing it in a terminal state, so a reader that treats a nonemptyissues[]as degradation stays correct. Waived occurrences contribute neither an issue nor degradation. var retired_at : datetime.datetime | Nonevar waived_conflict_sha256 : str | Nonevar waived_reporting_status_id : str | None-
Private rc.6 waiver binding, captured under the source transaction. A legacy waiver without this evidence cannot suppress a new projection.
class ReportingLedgerStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingLedgerStore(Protocol): """Durable home for obligations, revisions, adjustments, and statuses.""" async def create_schema(self) -> None: """Idempotently create or upgrade this store's schema. Safe on every boot.""" ... # -- configurations -------------------------------------------------- async def put_configuration(self, configuration: ReportingConfiguration) -> None: """Record an accepted configuration generation. A generation is immutable: re-putting one with changed content is a conflict, because a buyer that retained it derives expectations from it. """ ... async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: ... async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: """Enumerate retained generations for trusted service startup recovery. This administrative API is never exposed through buyer task handlers; those continue to use account-scoped reads after authorization. """ raise NotImplementedError # -- obligations ----------------------------------------------------- async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: """Commit an obligation, or return the existing one for its period. Idempotent by ``(account, config generation, period)``, not by id: two workers racing a period close must converge on one obligation. New records require an explicit, validated currency. A legacy record with unknown currency can be read/replayed but cannot be filled here. """ ... async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: ... async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: ... # -- revisions ------------------------------------------------------- async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: """Commit an immutable revision and its frozen rows. Enforces terminal officials, exact supersession, and idempotent replay of an identical ``reporting_revision_id``. """ ... async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: ... async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: ... async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: """Walk one revision's frozen rows in stable order.""" ... async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: """Record that a revision's content is (no longer) readable. Retention expiry and storage loss are real; representing them is how an obligation becomes honestly ``action_required`` instead of staying ``complete`` over evidence nobody can read. """ ... # -- adjustments ----------------------------------------------------- async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: ... async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: ... # -- consumer status (preview) --------------------------------------- async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: """Append a consumer status statement, superseding the chain's leaf. Returns ``(record, recorded)`` where ``recorded`` is ``False`` for an exact idempotent replay. Supersession is atomic: naming a stale or missing leaf must raise rather than fork the chain, so a successful retry cannot erase a recorded outage. """ ... async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: """Return the stored row when this exact statement was already recorded. Exists so the ingest can answer "is this an exact retry?" *before* it validates anything time-dependent. ``record_consumer_status`` already makes that check, but it runs last, and some validation the ingest does first -- notably "``content_mismatch`` must name the revision the seller currently requires" -- has an answer that changes as the seller publishes. An exact retry sent after a restatement would then be rejected for a reason that did not apply when it was first accepted, and the spec's ``result_mapping`` requires ``unchanged``. Returns ``None`` when the id is unknown, so the caller proceeds to validate and record. Raises :class:`LedgerConflictError` with ``STATUS_IDENTITY_CONFLICT`` when the id exists with different content: reuse with changed content is still a conflict, and answering ``unchanged`` there would let a buyer rewrite a recorded statement. """ ... async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: ... # -- issue lifecycle ------------------------------------------------- async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, ) -> ReportingIssueLifecycle: """Return this condition's live occurrence, opening one if needed. Idempotent: the second caller gets the first caller's ``opened_at``. That is the whole point -- AdCP 3.2.0-rc.3 anchors the escalation clock to ``opened_at``, so a re-emission that advanced it would let an unattended mismatch stay below ``action_required`` indefinitely. This is the one write the read path performs. ``get_reporting_status`` is where a consumer mismatch is *first observed*, and a stable first-observation timestamp cannot be derived from immutable evidence alone. Implementations must make it safe under concurrent reads; two readers of one condition must converge on one row rather than open two occurrences. A condition retired as ``resolved`` or ``waived`` opens a *new* occurrence with the next ``generation``, a new ``issue_id``, and a new ``opened_at``, which is exactly the spec's "a recurrence after retirement receives a new issue_id". """ ... async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, ) -> ReportingIssueLifecycle: """Move a live occurrence forward, or attach an ``external_ref``. Deliberately cannot set ``resolved``. The spec forbids retiring a ``CONSUMER_STATUS_MISMATCH`` out of a degraded projection while the statement that caused it is still the consumer's current leaf, so resolution is not an operator action -- it is what the projection does when the condition actually clears (see :meth:`retire_issue`). Otherwise a seller could unilaterally erase a buyer-attributed disagreement, which is the one outcome this separately attributed loop exists to prevent. ``waived`` requires explicit off-protocol agreement by this consumer and seller for the exact issue, causing statement and diagnosed conflict. The adopter must retain its private consent audit before calling this method; ``external_ref`` is inert correlation, not proof. Rc.6 removes that issue and restores underlying seller health, without changing the consumer statement. Later statements or different conflicts are evaluated independently, never covered by this waiver. """ ... async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime ) -> ReportingIssueLifecycle | None: """Mark this condition's live occurrence ``resolved``. Called by the projection when the condition no longer holds. Returns ``None`` when there was nothing live to retire, so a repeated projection is convergent rather than an error. """ ... async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: """The live occurrence of this condition, or ``None``.""" ... # -- snapshots and pagination ---------------------------------------- async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: """Take a consistent read boundary for one account and filter set.""" ... async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, ) -> LedgerPage: ... # -- worker leasing -------------------------------------------------- async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: """Lease one configuration generation for period-close work. Leasing rather than locking so a worker that dies mid-close releases its work by expiry instead of wedging the period forever, and so two workers cannot both close the same period. Implementations should prefer the generation whose work is most overdue. """ ... async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: ...Durable home for obligations, revisions, adjustments, and statuses.
Ancestors
- typing.Protocol
- typing.Generic
Methods
async def commit_adjustment(self,
adjustment: ReportingAdjustmentRecord) ‑> ReportingAdjustmentRecord-
Expand source code
async def commit_adjustment( self, adjustment: ReportingAdjustmentRecord ) -> ReportingAdjustmentRecord: ... async def commit_obligation(self,
obligation: ReportingObligationRecord) ‑> ReportingObligationRecord-
Expand source code
async def commit_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: """Commit an obligation, or return the existing one for its period. Idempotent by ``(account, config generation, period)``, not by id: two workers racing a period close must converge on one obligation. New records require an explicit, validated currency. A legacy record with unknown currency can be read/replayed but cannot be filled here. """ ...Commit an obligation, or return the existing one for its period.
Idempotent by
(account, config generation, period), not by id: two workers racing a period close must converge on one obligation. New records require an explicit, validated currency. A legacy record with unknown currency can be read/replayed but cannot be filled here. async def commit_revision(self,
revision: ReportingRevisionRecord,
rows: Sequence[dict[str, Any]]) ‑> ReportingRevisionRecord-
Expand source code
async def commit_revision( self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]], ) -> ReportingRevisionRecord: """Commit an immutable revision and its frozen rows. Enforces terminal officials, exact supersession, and idempotent replay of an identical ``reporting_revision_id``. """ ...Commit an immutable revision and its frozen rows.
Enforces terminal officials, exact supersession, and idempotent replay of an identical
reporting_revision_id. async def create_schema(self) ‑> None-
Expand source code
async def create_schema(self) -> None: """Idempotently create or upgrade this store's schema. Safe on every boot.""" ...Idempotently create or upgrade this store's schema. Safe on every boot.
async def ensure_issue_opened(self,
*,
issue_key: str,
account_id: str,
consumer_id: str | None,
observed_at: datetime) ‑> ReportingIssueLifecycle-
Expand source code
async def ensure_issue_opened( self, *, issue_key: str, account_id: str, consumer_id: str | None, observed_at: datetime, ) -> ReportingIssueLifecycle: """Return this condition's live occurrence, opening one if needed. Idempotent: the second caller gets the first caller's ``opened_at``. That is the whole point -- AdCP 3.2.0-rc.3 anchors the escalation clock to ``opened_at``, so a re-emission that advanced it would let an unattended mismatch stay below ``action_required`` indefinitely. This is the one write the read path performs. ``get_reporting_status`` is where a consumer mismatch is *first observed*, and a stable first-observation timestamp cannot be derived from immutable evidence alone. Implementations must make it safe under concurrent reads; two readers of one condition must converge on one row rather than open two occurrences. A condition retired as ``resolved`` or ``waived`` opens a *new* occurrence with the next ``generation``, a new ``issue_id``, and a new ``opened_at``, which is exactly the spec's "a recurrence after retirement receives a new issue_id". """ ...Return this condition's live occurrence, opening one if needed.
Idempotent: the second caller gets the first caller's
opened_at. That is the whole point – AdCP 3.2.0-rc.3 anchors the escalation clock toopened_at, so a re-emission that advanced it would let an unattended mismatch stay belowaction_requiredindefinitely.This is the one write the read path performs.
get_reporting_statusis where a consumer mismatch is first observed, and a stable first-observation timestamp cannot be derived from immutable evidence alone. Implementations must make it safe under concurrent reads; two readers of one condition must converge on one row rather than open two occurrences.A condition retired as
resolvedorwaivedopens a new occurrence with the nextgeneration, a newissue_id, and a newopened_at, which is exactly the spec's "a recurrence after retirement receives a new issue_id". async def find_obligation(self,
*,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
period_start: datetime,
period_end: datetime) ‑> ReportingObligationRecord | None-
Expand source code
async def find_obligation( self, *, account_id: str, consumer_id: str, delivery_config_id: str, delivery_config_version: int, period_start: datetime, period_end: datetime, ) -> ReportingObligationRecord | None: ... async def get_issue(self, *, issue_key: str, account_id: str) ‑> ReportingIssueLifecycle | None-
Expand source code
async def get_issue(self, *, issue_key: str, account_id: str) -> ReportingIssueLifecycle | None: """The live occurrence of this condition, or ``None``.""" ...The live occurrence of this condition, or
None. async def get_obligation(self, *, account_id: str, reporting_obligation_id: str) ‑> ReportingObligationRecord | None-
Expand source code
async def get_obligation( self, *, account_id: str, reporting_obligation_id: str ) -> ReportingObligationRecord | None: ... async def get_revision(self, *, account_id: str, reporting_revision_id: str) ‑> ReportingRevisionRecord | None-
Expand source code
async def get_revision( self, *, account_id: str, reporting_revision_id: str ) -> ReportingRevisionRecord | None: ... async def lease_period_close(self, *, worker_id: str, now: datetime, lease_seconds: float) ‑> LeasedConfiguration | None-
Expand source code
async def lease_period_close( self, *, worker_id: str, now: datetime, lease_seconds: float ) -> LeasedConfiguration | None: """Lease one configuration generation for period-close work. Leasing rather than locking so a worker that dies mid-close releases its work by expiry instead of wedging the period forever, and so two workers cannot both close the same period. Implementations should prefer the generation whose work is most overdue. """ ...Lease one configuration generation for period-close work.
Leasing rather than locking so a worker that dies mid-close releases its work by expiry instead of wedging the period forever, and so two workers cannot both close the same period. Implementations should prefer the generation whose work is most overdue.
async def list_adjustments(self, *, account_id: str, reporting_revision_ids: Sequence[str]) ‑> tuple[ReportingAdjustmentRecord, ...]-
Expand source code
async def list_adjustments( self, *, account_id: str, reporting_revision_ids: Sequence[str] ) -> tuple[ReportingAdjustmentRecord, ...]: ... async def list_all_configurations(self) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_all_configurations(self) -> tuple[ReportingConfiguration, ...]: """Enumerate retained generations for trusted service startup recovery. This administrative API is never exposed through buyer task handlers; those continue to use account-scoped reads after authorization. """ raise NotImplementedErrorEnumerate retained generations for trusted service startup recovery.
This administrative API is never exposed through buyer task handlers; those continue to use account-scoped reads after authorization.
async def list_configurations(self,
*,
caller: ReportingCaller,
delivery_config_ids: Sequence[str] | None = None) ‑> tuple[ReportingConfiguration, ...]-
Expand source code
async def list_configurations( self, *, caller: ReportingCaller, delivery_config_ids: Sequence[str] | None = None ) -> tuple[ReportingConfiguration, ...]: ... async def list_consumer_statuses(self,
*,
account_id: str,
consumer_id: str,
reporting_obligation_ids: Sequence[str] | None = None) ‑> tuple[ConsumerStatusRecord, ...]-
Expand source code
async def list_consumer_statuses( self, *, account_id: str, consumer_id: str, reporting_obligation_ids: Sequence[str] | None = None, ) -> tuple[ConsumerStatusRecord, ...]: ... async def list_revisions(self, *, account_id: str, reporting_obligation_id: str) ‑> tuple[ReportingRevisionRecord, ...]-
Expand source code
async def list_revisions( self, *, account_id: str, reporting_obligation_id: str ) -> tuple[ReportingRevisionRecord, ...]: ... async def open_snapshot(self,
*,
caller: ReportingCaller,
filters_fingerprint: str) ‑> LedgerSnapshot-
Expand source code
async def open_snapshot( self, *, caller: ReportingCaller, filters_fingerprint: str ) -> LedgerSnapshot: """Take a consistent read boundary for one account and filter set.""" ...Take a consistent read boundary for one account and filter set.
async def put_configuration(self,
configuration: ReportingConfiguration) ‑> None-
Expand source code
async def put_configuration(self, configuration: ReportingConfiguration) -> None: """Record an accepted configuration generation. A generation is immutable: re-putting one with changed content is a conflict, because a buyer that retained it derives expectations from it. """ ...Record an accepted configuration generation.
A generation is immutable: re-putting one with changed content is a conflict, because a buyer that retained it derives expectations from it.
async def read_page(self,
*,
snapshot: LedgerSnapshot,
consumer_id: str | None,
delivery_config_ids: Sequence[str] | None,
media_buy_ids: Sequence[str] | None,
offset: int,
limit: int,
changes_after_sequence: int | None) ‑> LedgerPage-
Expand source code
async def read_page( self, *, snapshot: LedgerSnapshot, consumer_id: str | None, delivery_config_ids: Sequence[str] | None, media_buy_ids: Sequence[str] | None, offset: int, limit: int, changes_after_sequence: int | None, ) -> LedgerPage: ... async def read_revision_rows(self,
*,
account_id: str,
reporting_revision_id: str,
cursor: str | None = None,
limit: int = 500) ‑> ReportingRowPage-
Expand source code
async def read_revision_rows( self, *, account_id: str, reporting_revision_id: str, cursor: str | None = None, limit: int = 500, ) -> ReportingRowPage: """Walk one revision's frozen rows in stable order.""" ...Walk one revision's frozen rows in stable order.
async def record_consumer_status(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: """Append a consumer status statement, superseding the chain's leaf. Returns ``(record, recorded)`` where ``recorded`` is ``False`` for an exact idempotent replay. Supersession is atomic: naming a stale or missing leaf must raise rather than fork the chain, so a successful retry cannot erase a recorded outage. """ ...Append a consumer status statement, superseding the chain's leaf.
Returns
(record, recorded)whererecordedisFalsefor an exact idempotent replay. Supersession is atomic: naming a stale or missing leaf must raise rather than fork the chain, so a successful retry cannot erase a recorded outage. async def release_period_close(self,
lease: LeasedConfiguration,
*,
worker_id: str) ‑> None-
Expand source code
async def release_period_close(self, lease: LeasedConfiguration, *, worker_id: str) -> None: ... async def resolve_consumer_status_replay(self,
status: ConsumerStatusRecord) ‑> ConsumerStatusRecord | None-
Expand source code
async def resolve_consumer_status_replay( self, status: ConsumerStatusRecord ) -> ConsumerStatusRecord | None: """Return the stored row when this exact statement was already recorded. Exists so the ingest can answer "is this an exact retry?" *before* it validates anything time-dependent. ``record_consumer_status`` already makes that check, but it runs last, and some validation the ingest does first -- notably "``content_mismatch`` must name the revision the seller currently requires" -- has an answer that changes as the seller publishes. An exact retry sent after a restatement would then be rejected for a reason that did not apply when it was first accepted, and the spec's ``result_mapping`` requires ``unchanged``. Returns ``None`` when the id is unknown, so the caller proceeds to validate and record. Raises :class:`LedgerConflictError` with ``STATUS_IDENTITY_CONFLICT`` when the id exists with different content: reuse with changed content is still a conflict, and answering ``unchanged`` there would let a buyer rewrite a recorded statement. """ ...Return the stored row when this exact statement was already recorded.
Exists so the ingest can answer "is this an exact retry?" before it validates anything time-dependent.
record_consumer_statusalready makes that check, but it runs last, and some validation the ingest does first – notably "content_mismatchmust name the revision the seller currently requires" – has an answer that changes as the seller publishes. An exact retry sent after a restatement would then be rejected for a reason that did not apply when it was first accepted, and the spec'sresult_mappingrequiresunchanged.Returns
Nonewhen the id is unknown, so the caller proceeds to validate and record. Raises :class:LedgerConflictErrorwithSTATUS_IDENTITY_CONFLICTwhen the id exists with different content: reuse with changed content is still a conflict, and answeringunchangedthere would let a buyer rewrite a recorded statement. async def retire_issue(self, *, issue_key: str, account_id: str, at: datetime) ‑> ReportingIssueLifecycle | None-
Expand source code
async def retire_issue( self, *, issue_key: str, account_id: str, at: datetime ) -> ReportingIssueLifecycle | None: """Mark this condition's live occurrence ``resolved``. Called by the projection when the condition no longer holds. Returns ``None`` when there was nothing live to retire, so a repeated projection is convergent rather than an error. """ ...Mark this condition's live occurrence
resolved.Called by the projection when the condition no longer holds. Returns
Nonewhen there was nothing live to retire, so a repeated projection is convergent rather than an error. async def set_issue_state(self,
*,
issue_key: str,
account_id: str,
state: "Literal['acknowledged', 'waived']",
at: datetime,
external_ref: str | None = None) ‑> ReportingIssueLifecycle-
Expand source code
async def set_issue_state( self, *, issue_key: str, account_id: str, state: Literal["acknowledged", "waived"], at: datetime, external_ref: str | None = None, ) -> ReportingIssueLifecycle: """Move a live occurrence forward, or attach an ``external_ref``. Deliberately cannot set ``resolved``. The spec forbids retiring a ``CONSUMER_STATUS_MISMATCH`` out of a degraded projection while the statement that caused it is still the consumer's current leaf, so resolution is not an operator action -- it is what the projection does when the condition actually clears (see :meth:`retire_issue`). Otherwise a seller could unilaterally erase a buyer-attributed disagreement, which is the one outcome this separately attributed loop exists to prevent. ``waived`` requires explicit off-protocol agreement by this consumer and seller for the exact issue, causing statement and diagnosed conflict. The adopter must retain its private consent audit before calling this method; ``external_ref`` is inert correlation, not proof. Rc.6 removes that issue and restores underlying seller health, without changing the consumer statement. Later statements or different conflicts are evaluated independently, never covered by this waiver. """ ...Move a live occurrence forward, or attach an
external_ref.Deliberately cannot set
resolved. The spec forbids retiring aCONSUMER_STATUS_MISMATCHout of a degraded projection while the statement that caused it is still the consumer's current leaf, so resolution is not an operator action – it is what the projection does when the condition actually clears (see :meth:retire_issue). Otherwise a seller could unilaterally erase a buyer-attributed disagreement, which is the one outcome this separately attributed loop exists to prevent.waivedrequires explicit off-protocol agreement by this consumer and seller for the exact issue, causing statement and diagnosed conflict. The adopter must retain its private consent audit before calling this method;external_refis inert correlation, not proof. Rc.6 removes that issue and restores underlying seller health, without changing the consumer statement. Later statements or different conflicts are evaluated independently, never covered by this waiver. async def set_revision_readable(self, *, account_id: str, reporting_revision_id: str, readable: bool) ‑> None-
Expand source code
async def set_revision_readable( self, *, account_id: str, reporting_revision_id: str, readable: bool ) -> None: """Record that a revision's content is (no longer) readable. Retention expiry and storage loss are real; representing them is how an obligation becomes honestly ``action_required`` instead of staying ``complete`` over evidence nobody can read. """ ...Record that a revision's content is (no longer) readable.
Retention expiry and storage loss are real; representing them is how an obligation becomes honestly
action_requiredinstead of stayingcompleteover evidence nobody can read.
class ReportingMaterializationAttempt (scope: ReportingDeliveryScope,
reporting_revision_id: str,
reporting_materialization_id: str,
attempt: int,
created_at: datetime,
*,
kind: "Literal['materialization_attempt']" = 'materialization_attempt')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id)ReportingMaterializationAttempt(scope: 'ReportingDeliveryScope', reporting_revision_id: 'str', reporting_materialization_id: 'str', attempt: 'int', created_at: 'datetime', *, kind: "Literal['materialization_attempt']" = 'materialization_attempt')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var attempt : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var created_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) prop key : ReportingMaterializationKey-
Expand source code
@property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var kind : Literal['materialization_attempt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var reporting_materialization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var reporting_revision_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationAttempt(_ClosedValue): scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str attempt: int created_at: datetime kind: Literal["materialization_attempt"] = field( default="materialization_attempt", kw_only=True ) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.attempt) object.__setattr__(self, "created_at", aware_utc(self.created_at)) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id)
class ReportingMaterializationCheck (scope: ReportingDeliveryScope,
reporting_materialization_id: str,
check_id: str,
state: "Literal['readable', 'unavailable', 'corrupt']",
checked_at: datetime,
*,
kind: "Literal['materialization_check']" = 'materialization_check')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at))Append-only storage observations; they never mutate terminal evidence.
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var check_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at)) var checked_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at)) var kind : Literal['materialization_check']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at)) var reporting_materialization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at)) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at)) var state : Literal['readable', 'unavailable', 'corrupt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationCheck(_ClosedValue): """Append-only storage observations; they never mutate terminal evidence.""" scope: ReportingDeliveryScope reporting_materialization_id: str check_id: str state: Literal["readable", "unavailable", "corrupt"] checked_at: datetime kind: Literal["materialization_check"] = field(default="materialization_check", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) reporting_identifier(self.check_id, maximum=255) object.__setattr__(self, "checked_at", aware_utc(self.checked_at))
class ReportingMaterializationKey (principal: ReportingDeliveryPrincipal,
reporting_materialization_id: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_materialization_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255)ReportingMaterializationKey(principal: 'ReportingDeliveryPrincipal', reporting_materialization_id: 'str')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var principal : ReportingDeliveryPrincipal-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_materialization_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255) var reporting_materialization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_materialization_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_materialization_id, maximum=255)
class ReportingMaterializationRecord (scope: ReportingDeliveryScope,
reporting_revision_id: str,
reporting_materialization_id: str,
status: "Literal['available', 'delivered', 'failed']",
completed_at: datetime,
resource: ReportingResourceRecord | None = None,
verification: ReportingVerificationRecord | None = None,
failure_code: MaterializationFailure | None = None,
*,
kind: "Literal['materialization']" = 'materialization')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id)One terminal outcome. The pending attempt is retained separately.
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var completed_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var failure_code : Literal['WRITE_FAILED', 'VERIFICATION_FAILED', 'CONTENT_CORRUPT', 'RESOURCE_UNAVAILABLE'] | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) prop key : ReportingMaterializationKey-
Expand source code
@property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var kind : Literal['materialization']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var reporting_materialization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var reporting_revision_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var resource : ReportingResourceRecord | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var status : Literal['available', 'delivered', 'failed']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id) var verification : ReportingVerificationRecord | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationRecord(_ClosedValue): """One terminal outcome. The pending attempt is retained separately.""" scope: ReportingDeliveryScope reporting_revision_id: str reporting_materialization_id: str status: Literal["available", "delivered", "failed"] completed_at: datetime resource: ReportingResourceRecord | None = None verification: ReportingVerificationRecord | None = None failure_code: MaterializationFailure | None = None kind: Literal["materialization"] = field(default="materialization", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) object.__setattr__(self, "completed_at", aware_utc(self.completed_at)) if self.status == "failed": if ( self.failure_code is None or self.resource is not None or self.verification is not None ): raise ValueError( "failed materializations retain only a safe failure classification" ) elif self.resource is None or self.verification is None or self.failure_code is not None: raise ValueError( "successful materializations require resource and verification evidence" ) @property def key(self) -> ReportingMaterializationKey: return ReportingMaterializationKey(self.scope.principal, self.reporting_materialization_id)
class ReportingMaterializationStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingMaterializationStore(Protocol): async def commit_materialization_attempt( self, record: ReportingMaterializationAttempt ) -> tuple[ReportingMaterializationAttempt, bool]: ... async def commit_materialization( self, record: ReportingMaterializationRecord ) -> tuple[ReportingMaterializationRecord, bool]: ... async def record_materialization_check( self, record: ReportingMaterializationCheck ) -> tuple[ReportingMaterializationCheck, bool]: ... async def get_materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: ...Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol): def meth(self) -> int: ...Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C: def meth(self) -> int: return 0 def func(x: Proto) -> int: return x.meth() func(C()) # Passes static type checkSee PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto(Protocol[T]): def meth(self) -> T: ...Ancestors
- typing.Protocol
- typing.Generic
Subclasses
Methods
async def commit_materialization(self,
record: ReportingMaterializationRecord) ‑> tuple[ReportingMaterializationRecord, bool]-
Expand source code
async def commit_materialization( self, record: ReportingMaterializationRecord ) -> tuple[ReportingMaterializationRecord, bool]: ... async def commit_materialization_attempt(self,
record: ReportingMaterializationAttempt) ‑> tuple[ReportingMaterializationAttempt, bool]-
Expand source code
async def commit_materialization_attempt( self, record: ReportingMaterializationAttempt ) -> tuple[ReportingMaterializationAttempt, bool]: ... async def get_materialization(self,
key: ReportingMaterializationKey) ‑> ReportingMaterializationView | None-
Expand source code
async def get_materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: ... async def record_materialization_check(self,
record: ReportingMaterializationCheck) ‑> tuple[ReportingMaterializationCheck, bool]-
Expand source code
async def record_materialization_check( self, record: ReportingMaterializationCheck ) -> tuple[ReportingMaterializationCheck, bool]: ...
class ReportingMaterializationView (attempt: ReportingMaterializationAttempt,
binding: ReportingDestinationBinding,
outcome: ReportingMaterializationRecord | None,
checks: tuple[ReportingMaterializationCheck, ...])-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationView: attempt: ReportingMaterializationAttempt binding: ReportingDestinationBinding outcome: ReportingMaterializationRecord | None checks: tuple[ReportingMaterializationCheck, ...] @property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return resultReportingMaterializationView(attempt: 'ReportingMaterializationAttempt', binding: 'ReportingDestinationBinding', outcome: 'ReportingMaterializationRecord | None', checks: 'tuple[ReportingMaterializationCheck, …]')
Instance variables
var attempt : ReportingMaterializationAttempt-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationView: attempt: ReportingMaterializationAttempt binding: ReportingDestinationBinding outcome: ReportingMaterializationRecord | None checks: tuple[ReportingMaterializationCheck, ...] @property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return result var binding : ReportingDestinationBinding-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationView: attempt: ReportingMaterializationAttempt binding: ReportingDestinationBinding outcome: ReportingMaterializationRecord | None checks: tuple[ReportingMaterializationCheck, ...] @property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return result prop check : ReportingMaterializationCheck | None-
Expand source code
@property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) var checks : tuple[ReportingMaterializationCheck, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationView: attempt: ReportingMaterializationAttempt binding: ReportingDestinationBinding outcome: ReportingMaterializationRecord | None checks: tuple[ReportingMaterializationCheck, ...] @property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return result var outcome : ReportingMaterializationRecord | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingMaterializationView: attempt: ReportingMaterializationAttempt binding: ReportingDestinationBinding outcome: ReportingMaterializationRecord | None checks: tuple[ReportingMaterializationCheck, ...] @property def check(self) -> ReportingMaterializationCheck | None: return max(self.checks, key=lambda item: item.checked_at, default=None) def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return result
Methods
def readable_at(self, at: datetime) ‑> bool-
Expand source code
def readable_at(self, at: datetime) -> bool: outcome = self.outcome check = max( (item for item in self.checks if item.checked_at <= at), key=lambda item: item.checked_at, default=None, ) return bool( outcome is not None and outcome.status in {"available", "delivered"} and outcome.resource is not None and outcome.completed_at <= at < outcome.resource.expires_at and (check is None or check.state == "readable") ) def to_wire(self) ‑> dict[str, typing.Any]-
Expand source code
def to_wire(self) -> dict[str, Any]: """An inert projection for future handlers. Never exposes the trusted reference.""" attempt, binding, outcome = self.attempt, self.binding, self.outcome generation = attempt.scope.generation_key result: dict[str, Any] = { "reporting_materialization_id": attempt.reporting_materialization_id, "reporting_revision_id": attempt.reporting_revision_id, "reporting_obligation_id": attempt.scope.reporting_obligation_id, "delivery_config_id": generation.delivery_config_id, "delivery_config_version": generation.delivery_config_version, "destination_ref": binding.destination_ref, "feed_purpose": binding.feed_purpose, "method": binding.method, "transport": binding.transport, "attempt": attempt.attempt, "status": outcome.status if outcome is not None else "pending", "created_at": iso(attempt.created_at), } if outcome is None: return result if outcome.status == "failed": result.update(failed_at=iso(outcome.completed_at), failure_code=outcome.failure_code) return result resource, verification = outcome.resource, outcome.verification assert resource is not None and verification is not None descriptor = { key: value for key, value in asdict(resource).items() if value is not None and key != "object_refs" } descriptor["expires_at"] = iso(resource.expires_at) descriptor["reader_compatibility"] = list(resource.reader_compatibility) if resource.kind == "manifest": descriptor["manifest_version"] = "1.0" evidence: dict[str, Any] = { "verified_at": iso(verification.verified_at), "verification_path": verification.verification_path, "verification_profile": verification.verification_profile, "row_count": verification.row_count, "control_totals": totals_to_wire(verification.control_totals), } if verification.canonical_content_digest is not None: evidence["canonical_content_digest"] = verification.canonical_content_digest.to_wire() if verification.physical_checksums: evidence["physical_checksums"] = [ asdict(item) for item in verification.physical_checksums ] if verification.native_version_ref is not None: evidence["native_commit_evidence"] = { "native_version_ref": verification.native_version_ref, "observed_through": verification.native_observed_through, } result.update( ready_at=iso(outcome.completed_at), resource=descriptor, verification=evidence ) return resultAn inert projection for future handlers. Never exposes the trusted reference.
class ReportingObligationDeliveryRecord (scope: ReportingDeliveryScope,
currency: str,
resource_retained_until: datetime,
created_at: datetime,
*,
kind: "Literal['obligation_delivery']" = 'obligation_delivery')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at))ReportingObligationDeliveryRecord(scope: 'ReportingDeliveryScope', currency: 'str', resource_retained_until: 'datetime', created_at: 'datetime', *, kind: "Literal['obligation_delivery']" = 'obligation_delivery')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var created_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at)) var currency : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at)) var kind : Literal['obligation_delivery']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at)) var resource_retained_until : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at)) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingObligationDeliveryRecord(_ClosedValue): scope: ReportingDeliveryScope currency: str resource_retained_until: datetime created_at: datetime kind: Literal["obligation_delivery"] = field(default="obligation_delivery", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) validate_currency(self.currency) object.__setattr__(self, "resource_retained_until", aware_utc(self.resource_retained_until)) object.__setattr__(self, "created_at", aware_utc(self.created_at))
class ReportingObligationRecord (reporting_obligation_id: str,
account_id: str,
consumer_id: str,
delivery_config_id: str,
delivery_config_version: int,
report_definition_id: str,
reporting_profile: str,
feed_purpose: str,
period: ReportingPeriodBoundary,
scope_resolved_at: datetime,
media_buy_ids: tuple[str, ...],
required_finality: ReportingFinality,
automated_recovery_deadline_at: datetime,
schedule: ReportingScheduleSpec,
coverage_status: "Literal['full', 'partial', 'none', 'unknown']" = 'full',
package_ids: tuple[str, ...] = (),
definition: ReportingDefinitionBinding | None = None,
created_at: datetime = <factory>,
currency: str | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingObligationRecord: """What *should* exist for one configuration generation and period. Committed at the period boundary, independently of whether source data is available and before any revision. ``scope_resolved_at`` equals the period end: that is the instant the media-buy denominator froze, and a coverage evaluation at any other instant is describing a different scope. """ reporting_obligation_id: str account_id: str consumer_id: str delivery_config_id: str delivery_config_version: int report_definition_id: str reporting_profile: str feed_purpose: str period: ReportingPeriodBoundary scope_resolved_at: datetime media_buy_ids: tuple[str, ...] required_finality: ReportingFinality automated_recovery_deadline_at: datetime schedule: ReportingScheduleSpec coverage_status: Literal["full", "partial", "none", "unknown"] = "full" package_ids: tuple[str, ...] = () definition: ReportingDefinitionBinding | None = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) # None describes legacy evidence only. New writes must freeze a currency; # neither a restart nor a source response may fill an unknown historical one. currency: str | None = None @property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) def __post_init__(self) -> None: principal_reference(self.account_id) consumer_reference(self.consumer_id) if self.currency is not None: validate_currency(self.currency) if self.definition is not None: validate_currency_units( self.currency, ( *self.definition.monetary_metric_units, *self.definition.monetary_control_total_units, ), ) object.__setattr__(self, "media_buy_ids", tuple(self.media_buy_ids)) object.__setattr__(self, "package_ids", tuple(self.package_ids)) if _utc(self.scope_resolved_at) != _utc(self.period.end): raise ValueError( "scope_resolved_at must equal the period end; the denominator froze there " "and a coverage evaluation at another instant describes another scope" ) if _utc(self.automated_recovery_deadline_at) < _utc(self.period.expected_at): raise ValueError("the automated recovery deadline cannot precede expected_at")What should exist for one configuration generation and period.
Committed at the period boundary, independently of whether source data is available and before any revision.
scope_resolved_atequals the period end: that is the instant the media-buy denominator froze, and a coverage evaluation at any other instant is describing a different scope.Instance variables
var account_id : strvar automated_recovery_deadline_at : datetime.datetimevar consumer_id : strvar coverage_status : Literal['full', 'partial', 'none', 'unknown']var created_at : datetime.datetimevar currency : str | Nonevar definition : ReportingDefinitionBinding | Nonevar delivery_config_id : strvar delivery_config_version : intvar feed_purpose : strprop generation_key : ReportingConfigurationGenerationKey-
Expand source code
@property def generation_key(self) -> ReportingConfigurationGenerationKey: return ReportingConfigurationGenerationKey( account_id=self.account_id, consumer_id=self.consumer_id, delivery_config_id=self.delivery_config_id, delivery_config_version=self.delivery_config_version, ) var media_buy_ids : tuple[str, ...]var package_ids : tuple[str, ...]var period : ReportingPeriodBoundaryvar report_definition_id : strvar reporting_obligation_id : strvar reporting_profile : strvar required_finality : Literal['snapshot', 'official']var schedule : ReportingScheduleSpecvar scope_resolved_at : datetime.datetime
class ReportingPeriodBoundary (period_key: str,
start: datetime,
end: datetime,
source_timezone: str,
expected_at: datetime)-
Expand source code
@dataclass(frozen=True) class ReportingPeriodBoundary: """One derived half-open period plus the instant its report becomes late.""" period_key: str start: datetime end: datetime source_timezone: str expected_at: datetime def __post_init__(self) -> None: if _utc(self.end) <= _utc(self.start): raise ValueError("a reporting period must be nonempty") if _utc(self.expected_at) < _utc(self.end): raise ValueError("expected_at cannot precede the period end")One derived half-open period plus the instant its report becomes late.
Instance variables
var end : datetime.datetimevar expected_at : datetime.datetimevar period_key : strvar source_timezone : strvar start : datetime.datetime
class ReportingPhysicalChecksum (object_ref: str, algorithm: "Literal['sha256', 'sha512']", value: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingPhysicalChecksum(_ClosedValue): object_ref: str algorithm: Literal["sha256", "sha512"] value: str def __post_init__(self) -> None: _freeze_fields(self) file_object_reference(self.object_ref) size = 64 if self.algorithm == "sha256" else 128 if ( not isinstance(self.value, str) or re.fullmatch(rf"[a-fA-F0-9]{{{size}}}", self.value) is None ): raise ValueError("physical checksum length must match its algorithm")ReportingPhysicalChecksum(object_ref: 'str', algorithm: "Literal['sha256', 'sha512']", value: 'str')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var algorithm : Literal['sha256', 'sha512']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingPhysicalChecksum(_ClosedValue): object_ref: str algorithm: Literal["sha256", "sha512"] value: str def __post_init__(self) -> None: _freeze_fields(self) file_object_reference(self.object_ref) size = 64 if self.algorithm == "sha256" else 128 if ( not isinstance(self.value, str) or re.fullmatch(rf"[a-fA-F0-9]{{{size}}}", self.value) is None ): raise ValueError("physical checksum length must match its algorithm") var object_ref : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingPhysicalChecksum(_ClosedValue): object_ref: str algorithm: Literal["sha256", "sha512"] value: str def __post_init__(self) -> None: _freeze_fields(self) file_object_reference(self.object_ref) size = 64 if self.algorithm == "sha256" else 128 if ( not isinstance(self.value, str) or re.fullmatch(rf"[a-fA-F0-9]{{{size}}}", self.value) is None ): raise ValueError("physical checksum length must match its algorithm") var value : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingPhysicalChecksum(_ClosedValue): object_ref: str algorithm: Literal["sha256", "sha512"] value: str def __post_init__(self) -> None: _freeze_fields(self) file_object_reference(self.object_ref) size = 64 if self.algorithm == "sha256" else 128 if ( not isinstance(self.value, str) or re.fullmatch(rf"[a-fA-F0-9]{{{size}}}", self.value) is None ): raise ValueError("physical checksum length must match its algorithm")
class ReportingProducer (*,
source: ReportingSourceExecutor,
offerings: ProducerOfferings,
store: ReportingLedgerStore,
object_reader: ReportingSourceStagedObjectReader | None = None,
escalation: ReportingDeliveryEscalation | None = None,
worker_id: str = 'reporting-producer',
lease_seconds: float = 60.0,
max_periods_per_turn: int = 64,
clock: Callable[[], datetime] | None = None,
currency_resolver: CurrencyResolver | None = None,
revision_verifier: ReportingRevisionVerifier | None = None,
retry_initial_delay: timedelta = datetime.timedelta(seconds=60),
retry_max_delay: timedelta = datetime.timedelta(seconds=1800),
post_deadline_retry_interval: timedelta = datetime.timedelta(seconds=3600),
retry_store: RetryScheduleStore | None = None)-
Expand source code
class ReportingProducer: """Closes periods, drives the source, and commits immutable revisions.""" def __init__( self, *, source: ReportingSourceExecutor, offerings: ProducerOfferings, store: ReportingLedgerStore, object_reader: ReportingSourceStagedObjectReader | None = None, escalation: ReportingDeliveryEscalation | None = None, worker_id: str = "reporting-producer", lease_seconds: float = 60.0, max_periods_per_turn: int = 64, clock: Callable[[], datetime] | None = None, currency_resolver: CurrencyResolver | None = None, revision_verifier: ReportingRevisionVerifier | None = None, retry_initial_delay: timedelta = timedelta(minutes=1), retry_max_delay: timedelta = timedelta(minutes=30), post_deadline_retry_interval: timedelta = timedelta(hours=1), retry_store: RetryScheduleStore | None = None, ) -> None: if ( retry_initial_delay <= timedelta(0) or retry_max_delay < retry_initial_delay or post_deadline_retry_interval <= timedelta(0) ): raise ValueError("retry delays must be positive and maximum must exceed initial") if retry_store is None: if not isinstance(store, RetryScheduleStore): raise TypeError( "ReportingProducer requires a RetryScheduleStore to persist retry state" ) retry_store = store self._source = source self._offerings = offerings self._store = store self._object_reader = object_reader self._escalation = escalation or ReportingDeliveryEscalation() self._worker_id = worker_id self._lease_seconds = lease_seconds self._max_periods_per_turn = max_periods_per_turn self._clock = clock or (lambda: datetime.now(timezone.utc)) self._currency_resolver = ( currency_resolver if currency_resolver is not None else FixedCurrencyResolver(offerings.currency) ) self._revision_verifier = revision_verifier self._retry_initial_delay = retry_initial_delay self._retry_max_delay = retry_max_delay self._post_deadline_retry_interval = post_deadline_retry_interval self._retry_store = retry_store @property def store(self) -> ReportingLedgerStore: return self._store @property def escalation(self) -> ReportingDeliveryEscalation: """The advertised escalation commitment, for the status handler. Pass the same object to :class:`~adcp.reporting.ledger.status.ReportingStatusHandler` so the projection honours exactly the window the seller published. A handler with a different window than the capability block would escalate on a clock no buyer can see. """ return self._escalation def advertised_reporting_delivery( self, *, consumer_status_task: bool, offerings: Sequence[Mapping[str, Any]], automated_recovery_window: timedelta, status_retention_days: int, extra: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """The complete ``media_buy.reporting_delivery`` block for this producer. Returns a *whole* capability document, not a fragment, so the result can be validated against ``core/reporting-delivery-capabilities.json`` before it is published. A fragment would push six required fields onto the caller to remember, and an under-filled capability block is exactly the kind of thing that passes review and fails a buyer's validator. The seller supplies what only it knows -- its ``offerings``, its seller-wide recovery window and retention. This method supplies the task names and the Reliable Reporting declarations, because those follow from the producer actually running rather than from configuration. ``consumer_status_task`` is an explicit argument rather than inferred: advertising it while the ingest is disabled is the half-implemented loop this module's docstring warns about, and a buyer that can file statements nobody reads believes it has told you. """ return _advertised_reporting_delivery( escalation=self._escalation, consumer_status_task=consumer_status_task, offerings=offerings, automated_recovery_window=automated_recovery_window, status_retention_days=status_retention_days, extra=extra, ) # -- the worker turn ------------------------------------------------- async def run_worker(self) -> WorkerTurn: """Run one leased turn. Safe to call from cron, a loop, or a supervisor. Returns immediately with an empty turn when nothing is leasable, so a caller can back off rather than spin. Standalone PostgreSQL turns retry deadlocks and lock timeouts at most twice, after rollback and fenced release. Each retry must acquire a new lease; other failures propagate. """ from adcp.reporting.ledger.pg import PgReportingLedgerStore retryable = ( self._store._worker_lock_errors() if isinstance(self._store, PgReportingLedgerStore) else () ) for attempt in range(3): leased = None with source_turn(): try: now = self._clock() leased = await self._store.lease_period_close( worker_id=self._worker_id, now=now, lease_seconds=self._lease_seconds ) turn = WorkerTurn(leased=leased) if leased is None: return turn await self._work_leased_configuration(leased, turn, now=now) except retryable: # Store transactions have already exited/rolled back. A # committed lease is released only under its original fence; # a competitor may win before the next acquisition. if leased is not None: await self._release_worker_lease(leased, retryable) if attempt == 2: raise except BaseException as original: if leased is not None: try: await self._release_worker_lease(leased, retryable) except retryable: # Never turn an immutable-content conflict or # cancellation into a retryable cleanup failure. raise original from None raise else: # Exhausted release retries escape from here, rather than # starting another turn or reporting a false empty success. await self._release_worker_lease(leased, retryable) return turn await asyncio.sleep(0.05 * (2**attempt)) raise AssertionError("unreachable worker retry") # pragma: no cover async def _release_worker_lease( self, leased: LeasedConfiguration, retryable: tuple[type[Exception], ...] ) -> None: for attempt in range(3): try: await self._store.release_period_close(leased, worker_id=self._worker_id) return except retryable: if attempt == 2: raise await asyncio.sleep(0.05 * (2**attempt)) async def _work_leased_configuration( self, leased: LeasedConfiguration, turn: WorkerTurn, *, now: datetime ) -> None: from adcp.reporting.production.contracts import _SourceAuthorizationRevokedError try: configuration = next( ( candidate for candidate in await self._store.list_configurations( caller=ReportingDeliveryPrincipal(leased.account_id, leased.consumer_id), delivery_config_ids=[leased.delivery_config_id], ) if candidate.generation_key == leased.generation_key ), None, ) if configuration is None or configuration.quarantined: return require_account_work(configuration.account_id) await self._close_elapsed_periods(configuration, turn, now=now) await self._acquire_pending(configuration, turn, now=now) except _SourceAuthorizationRevokedError: # Keep pending acquisitions retryable. Authorization may be # restored on the next turn; it is not a history failure. return async def run_configuration( self, configuration: ReportingConfiguration, *, now: datetime | None = None, ) -> WorkerTurn: """Run one turn for an already-routed configuration generation. High-level orchestrators use this entry point after freezing adapter, currency, and source scope for a specific generation. The orchestrator owns cross-process scheduling; obligation and revision writes remain convergent and immutable in the ledger store. Most adopters should continue using :meth:`run_worker`, whose store lease chooses a configuration automatically. """ from adcp.reporting.production.contracts import _SourceAuthorizationRevokedError boundary = now or self._clock() turn = WorkerTurn() with source_turn(): try: require_account_work(configuration.account_id) await self._close_elapsed_periods(configuration, turn, now=boundary) await self._acquire_pending(configuration, turn, now=boundary) except _SourceAuthorizationRevokedError: return turn return turn # -- step 1: obligations before reports ------------------------------ async def close_elapsed_periods( self, configuration: ReportingConfiguration, *, now: datetime | None = None ) -> list[ReportingObligationRecord]: """Commit an obligation for every elapsed eligible period. Public because a seller often wants to run this on its own cadence -- the obligation must land in the first ledger snapshot strictly after the period boundary, independent of whether the source is healthy. """ if configuration.quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy generations cannot run" ) turn = WorkerTurn() return await self._close_elapsed_periods(configuration, turn, now=now or self._clock()) async def _close_elapsed_periods( self, configuration: ReportingConfiguration, turn: WorkerTurn, *, now: datetime, ) -> list[ReportingObligationRecord]: from adcp.reporting.ledger.producer_progress import ReportingProducerProgress progress = self._store if isinstance(self._store, ReportingProducerProgress) else None after = None if progress is None else await progress.producer_closed_through(configuration) committed: list[ReportingObligationRecord] = [] for boundary in self._elapsed_periods(configuration, now=now, after=after): existing = await self._store.find_obligation( account_id=configuration.account_id, consumer_id=configuration.consumer_id, delivery_config_id=configuration.delivery_config_id, delivery_config_version=configuration.delivery_config_version, period_start=boundary.start, period_end=boundary.end, ) if existing is not None: if progress is not None: await progress.commit_producer_period( configuration, existing, previous_end=after ) after = boundary.end continue obligation = ReportingObligationRecord( reporting_obligation_id=self._obligation_id(configuration, boundary), account_id=configuration.account_id, consumer_id=configuration.consumer_id, delivery_config_id=configuration.delivery_config_id, delivery_config_version=configuration.delivery_config_version, report_definition_id=configuration.report_definition_id, reporting_profile=configuration.reporting_profile, feed_purpose=configuration.feed_purpose, period=boundary, # The denominator froze at the period boundary. Resolving it # "now" would silently include a media buy that started after # the period it is being reported against. scope_resolved_at=boundary.end, media_buy_ids=configuration.media_buy_ids, required_finality=configuration.required_finality, automated_recovery_deadline_at=( boundary.expected_at + configuration.automated_recovery_window ), schedule=configuration.schedule, definition=configuration.definition, created_at=now, ) resolved = self._currency_resolver(configuration, obligation) currency = await resolved if inspect.isawaitable(resolved) else resolved obligation = replace(obligation, currency=validate_currency(currency)) stored = ( await self._store.commit_obligation(obligation) if progress is None else await progress.commit_producer_period( configuration, obligation, previous_end=after ) ) after = boundary.end committed.append(stored) turn.obligations_committed.append(stored.reporting_obligation_id) return committed def _elapsed_periods( self, configuration: ReportingConfiguration, *, now: datetime, after: datetime | None = None, ) -> list[ReportingPeriodBoundary]: """Every eligible period that has closed but is not yet obligated. A period is eligible once its *end* is at or before now. Activation owes the first full period; deactivation after a period has started retains that whole period and its original SLA. Polling forecasts use the same committed-generation iterator. """ from itertools import islice from adcp.reporting.ledger.schedule import committed_periods boundaries: list[ReportingPeriodBoundary] = [] near = ( None if after is None else after + iso_duration_to_timedelta(configuration.schedule.delivery_sla) ) periods = ( period for period in committed_periods(configuration, near=near) if after is None or period.end > after ) for boundary in islice(periods, self._max_periods_per_turn): if _utc(boundary.end) > _utc(now): break boundaries.append(boundary) return boundaries @staticmethod def _obligation_id( configuration: ReportingConfiguration, boundary: ReportingPeriodBoundary ) -> str: digest = hashlib.sha256( canonical_json_utf8_v1( [ configuration.account_id, configuration.consumer_id, configuration.delivery_config_id, configuration.delivery_config_version, configuration.report_definition_id, _utc(boundary.start).isoformat(), _utc(boundary.end).isoformat(), ] ) ).hexdigest() return f"rpo_{digest[:40]}" # -- steps 2-4: acquire, commit, restate ----------------------------- async def _acquire_pending( self, configuration: ReportingConfiguration, turn: WorkerTurn, *, now: datetime ) -> None: from adcp.reporting.ledger.producer_progress import ReportingProducerProgress if isinstance(self._store, ReportingProducerProgress): await self._acquire_progress(self._store, configuration, turn, now=now) return for boundary in self._elapsed_periods(configuration, now=now): obligation = await self._store.find_obligation( account_id=configuration.account_id, consumer_id=configuration.consumer_id, delivery_config_id=configuration.delivery_config_id, delivery_config_version=configuration.delivery_config_version, period_start=boundary.start, period_end=boundary.end, ) if obligation is None: continue if _utc(now) < self._source_available_at(obligation): continue try: policy = self._settling_policy(configuration, obligation) if policy is None: await self.acquire_obligation(configuration, obligation, turn=turn, now=now) else: await self._acquire_with_settling_policy( configuration, obligation, policy=policy, turn=turn, now=now, ) except (ReportingCurrencyError, LedgerConflictError) as error: if isinstance(error, LedgerConflictError) and error.code != "OBSERVATION_CONFLICT": raise # Currency interpretation and competing observations are local # slice failures. Let later periods make progress; a later turn # can read the winning observation or retry the same reservation. logger.info( "reporting slice failed obligation=%s code=%s", obligation.reporting_obligation_id, error.code, ) turn.slices_failed.append(obligation.reporting_obligation_id) self._note_escalation(obligation, turn, now=now) async def _acquire_progress( self, progress: ReportingProducerProgress, configuration: ReportingConfiguration, turn: WorkerTurn, *, now: datetime, ) -> None: identifiers = await progress.next_producer_obligations( configuration, now=now, limit=self._max_periods_per_turn ) for identifier in identifiers: obligation = await self._store.get_obligation( account_id=configuration.account_id, reporting_obligation_id=identifier ) if obligation is None or obligation.generation_key != configuration.generation_key: raise LedgerConflictError("HISTORY_UNAVAILABLE", "producer history is unavailable") if _utc(now) < self._source_available_at(obligation): # Waiting for the declared source window is not completion. # Leave the durable work item available to a later turn. continue policy = self._settling_policy(configuration, obligation) finished = policy is None try: if policy is None: await self.acquire_obligation(configuration, obligation, turn=turn, now=now) else: finished = await self._acquire_with_settling_policy( configuration, obligation, policy=policy, turn=turn, now=now ) except (ReportingCurrencyError, LedgerConflictError) as error: if isinstance(error, LedgerConflictError) and error.code not in { "HISTORY_UNAVAILABLE", "EMPTY_DENOMINATOR", "OBSERVATION_CONFLICT", }: raise turn.slices_failed.append(identifier) self._note_escalation(obligation, turn, now=now) # Retain the existing corrupt-history parking check. A transient # currency failure or observation conflict must not retire work. finished = error.code != "OBSERVATION_CONFLICT" and ( policy is None or isinstance(error, LedgerConflictError) ) if finished: await progress.finish_producer_acquisition( configuration, reporting_obligation_id=identifier ) def _source_available_at( self, obligation: ReportingObligationRecord, *, target_finality: str | None = None, offering_id: str | None = None, worst_case: bool = False, ) -> datetime: end = _utc(obligation.period.end) offering_id = offering_id or self._offerings.offering_for( target_finality or obligation.required_finality ) if offering_id is None: # The acquisition path reports the existing missing-offering error. return end offering = self._source.capabilities.offering(offering_id) lag = ( offering.worst_case_availability_lag if worst_case else offering.expected_availability_lag ) ready_at = end + timedelta(milliseconds=iso_duration_milliseconds_v1(lag)) if isinstance(offering, AuthoritativeOfferingV1): zone = ZoneInfo(offering.source_timezone or obligation.period.source_timezone) local_day = end.astimezone(zone).date() + timedelta(days=offering.days_after_period_end) hour, minute = (int(part) for part in offering.source_local_ready_time.split(":")) local_ready = datetime.combine(local_day, datetime.min.time()).replace( hour=hour, minute=minute, tzinfo=zone ) # Pick the later instant across a clock fold or gap. Ambiguous # local readiness must not make the scheduler read the source early. ready_at = max( ready_at, _utc(local_ready.replace(fold=0)), _utc(local_ready.replace(fold=1)), ) return ready_at def _settling_policy( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord, ) -> _SettlingPolicy | None: """Resolve the source policy or SDK fallback for a snapshot obligation.""" offering_id = self._offerings.snapshot_offering_id if obligation.required_finality != "snapshot" or offering_id is None: return None offering = self._source.capabilities.offering(offering_id) if not isinstance(offering, ProvisionalSnapshotOfferingV1): return None if offering.restatement_cadence is not None: cadence = timedelta( milliseconds=iso_duration_milliseconds_v1(offering.restatement_cadence) ) else: cadence = max( iso_duration_to_timedelta(configuration.schedule.period_duration), timedelta(milliseconds=iso_duration_milliseconds_v1(offering.fastest_safe_cadence)), ) close_lag = ( timedelta(milliseconds=iso_duration_milliseconds_v1(offering.official_close_lag)) if offering.official_close_lag is not None else None ) return _SettlingPolicy( restatement_window=timedelta( milliseconds=iso_duration_milliseconds_v1(offering.restatement_window or "P3D") ), restatement_cadence=cadence, official_close_lag=close_lag, ) async def _acquire_with_settling_policy( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord, *, policy: _SettlingPolicy, turn: WorkerTurn, now: datetime, ) -> bool: """Return whether policy-controlled acquisition may leave the pending queue. A readable snapshot completes one acquisition, not the settling policy. The progress store retains its rotating work item until the policy ends. """ checkpoint_store = self._restatement_store() observation_store = self._observation_store() latest = await observation_store.get_provisional_observation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if latest is not None: frozen = latest.acquisition.policy policy = _SettlingPolicy(frozen.window, frozen.cadence, frozen.official_close_lag) explicit_close = ( policy.official_close_lag is not None and self._offerings.official_offering_id is not None ) revisions = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if any(item.finality == "official" for item in revisions): return True checkpoint = await checkpoint_store.get_restatement_checkpoint( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) ordinal = checkpoint.next_observation if checkpoint is not None else len(revisions) pending = await self._pending_acquisition(obligation, ordinal=ordinal) if pending is not None: # Finish frozen source work before selecting a new finality. A # snapshot retry may cross the official-close boundary while the # authoritative offering is still unavailable. request = pending.request() if _utc(now) < self._source_available_at(obligation, offering_id=request.offering_id): return False await self.acquire_obligation( configuration, obligation, restate=bool(revisions), turn=turn, now=now, target_finality=( "snapshot" if request.publication_class == "PROVISIONAL_SNAPSHOT" else "official" ), track_settling=True, ) revisions = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if any(item.finality == "official" for item in revisions): return True if not explicit_close: published = await observation_store.get_provisional_observation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) return ( published is not None and published.acquisition.ordinal == ordinal and published.next_due_at is None ) return False if not revisions: await self.acquire_obligation( configuration, obligation, turn=turn, now=now, target_finality="snapshot", track_settling=True, ) return False declared_until = _utc(obligation.period.end) + policy.restatement_window settles_at = ( _utc(checkpoint.provisional_until) if checkpoint is not None and checkpoint.provisional_until is not None else declared_until ) last_checked = ( checkpoint.checked_at if checkpoint is not None else max(revisions, key=lambda item: _utc(item.created_at)).created_at ) next_due = ( latest.next_due_at if latest is not None else min(_utc(last_checked) + policy.restatement_cadence, settles_at) ) # Preserve explicitly configured official-close boundary precedence. # Snapshot-only policies retain their final inclusive due read through # downtime; expiry alone is never an official publication. if not explicit_close or _utc(now) < settles_at: if next_due is None: return not explicit_close if _utc(now) >= _utc(next_due): await self.acquire_obligation( configuration, obligation, restate=True, turn=turn, now=now, target_finality="snapshot", track_settling=True, ) published = await observation_store.get_provisional_observation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if not explicit_close and published is not None and published.next_due_at is None: return True return False if policy.official_close_lag is None or self._offerings.official_offering_id is None: return True closes_at = max( settles_at, _utc(obligation.period.end) + policy.official_close_lag, self._source_available_at(obligation, target_finality="official"), ) if _utc(now) >= closes_at: await self.acquire_obligation( configuration, obligation, restate=True, turn=turn, now=now, target_finality="official", track_settling=True, ) # An attempted close can still return not-ready. Retire only after # the authoritative revision was actually committed. revisions = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) return any(item.finality == "official" for item in revisions) return False async def acquire_obligation( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord, *, restate: bool = False, turn: WorkerTurn | None = None, now: datetime | None = None, target_finality: str | None = None, track_settling: bool = False, manual_replay: bool = False, ) -> ReportingRevisionRecord | None: """Drive one obligation from its source and commit what comes back. Returns the committed revision, or ``None`` when the source is not ready or the obligation is already satisfied. ``restate`` asks for a *new observation* of an already-satisfied snapshot obligation. Without it a satisfied obligation is left alone, because re-reading a settled period on every worker turn would burn upstream quota to republish bytes nobody asked for. ``now`` freezes dispatch and the source read cutoff. Revision creation uses a fresh producer clock sample after the staged objects are read. """ if configuration.generation_key != obligation.generation_key: raise LedgerConflictError( "CONFIGURATION_GENERATION_MISMATCH", "the source configuration must belong to the obligation's account and generation", ) obligation = await self._stored_obligation(obligation) turn = turn or WorkerTurn() now = now or self._clock() finality = target_finality or obligation.required_finality if not manual_replay and await self._retry_not_before(obligation, turn, now=now): retry_offering_id: str | None = None if track_settling: checkpoint = await self._restatement_store().get_restatement_checkpoint( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) ordinal = ( checkpoint.next_observation if checkpoint is not None else len( await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) ) ) pending = await self._pending_acquisition(obligation, ordinal=ordinal) if pending is not None: retry_offering_id = pending.request().offering_id keys = turn._retry_keys_by_obligation[obligation.reporting_obligation_id] retryable = not any( turn._retry_entries[key].blocked for key in keys if key in turn._retry_entries ) self._note_escalation( obligation, turn, now=now, availability_retry=retryable, target_finality=finality, offering_id=retry_offering_id, ) return None revisions = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) selection = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) if selection.kind == "corrupt": raise LedgerConflictError("HISTORY_UNAVAILABLE", "the revision history requires repair") current = selection.revision if selection.kind == "selected" else None if current is not None and current.finality == "official": # An official close is terminal. A later source correction is an # adjustment, never another acquisition. return None satisfied = current is not None and current.readable if satisfied and not restate: return None # Everything below needs the frozen code: the slice request carries it, # the manifest is checked against it, and the revision is written under # it. Gate here rather than earlier so a settled legacy obligation stays # the no-op it already was instead of becoming an error on every turn. require_frozen_currency(obligation.currency) offering_id = self._offerings.offering_for(finality) if offering_id is None: raise LedgerConflictError( "NO_OFFERING_FOR_FINALITY", f"this producer declares no source offering for {finality} reporting", ) constituents = await self._admitted_source_constituents(configuration, obligation) checkpoint_store = self._restatement_store() if track_settling else None checkpoint = ( await checkpoint_store.get_restatement_checkpoint( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if checkpoint_store is not None else None ) observation = checkpoint.next_observation if checkpoint is not None else len(revisions) request = self._build_slice( configuration, obligation, offering_id, finality=finality, now=now, observation=observation, constituents=constituents, trigger="manual_replay" if manual_replay else "scheduled_poll", ) acquisition = None if track_settling: policy = self._settling_policy(configuration, obligation) assert policy is not None latest = await self._observation_store().get_provisional_observation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) frozen_policy = ( latest.acquisition.policy if latest is not None else ProvisionalPolicy( policy.restatement_window, policy.restatement_cadence, policy.official_close_lag ) ) leaf = self._current_snapshot(revisions) acquisition = await self._observation_store().reserve_provisional_acquisition( ProvisionalAcquisition( request.model_dump_json(), observation, frozen_policy, leaf.reporting_revision_id if leaf is not None else None, checkpoint.provisional_until if checkpoint is not None else None, ) ) request = acquisition.request(deadline_at=_utc(now) + self._offerings.slice_timeout) if manual_replay: request = request.model_copy(update={"trigger": "manual_replay"}) finality = ( "snapshot" if request.publication_class == "PROVISIONAL_SNAPSHOT" else "official" ) cancel = asyncio.Event() execution = asyncio.create_task( self._execute_source( configuration, request, admitted_constituents=constituents, cancel=cancel ) ) try: result = await asyncio.wait_for( asyncio.shield(execution), timeout=self._offerings.slice_timeout.total_seconds(), ) except asyncio.CancelledError: cancel.set() await cancel_and_settle(execution) raise except asyncio.TimeoutError: cancel.set() # wait_for's own cancellation join can be interrupted by a second # cancellation of this producer (notably on Python 3.10). Retain # the execution explicitly until even synchronous work has settled. await cancel_and_settle(execution) turn.slices_failed.append(obligation.reporting_obligation_id) await self._schedule_retry(obligation, turn, now=now, replayed=manual_replay) self._note_escalation( obligation, turn, now=now, availability_retry=True, offering_id=request.offering_id, ) return None if isinstance(result, _InlineStorageFailure): from adcp.reporting.inline_storage import InlineStorageError # Return closed data from the executor task: a raw driver exception # must not survive through a context manager or Task wakeup frame. raise InlineStorageError(result.code) if not result.ok: error = result.error assert error is not None logger.info( "reporting slice failed obligation=%s code=%s retry=%s", obligation.reporting_obligation_id, error.code, error.retry, ) turn.slices_failed.append(obligation.reporting_obligation_id) await self._schedule_retry( obligation, turn, now=now, scope=error.scope, retry_after_seconds=error.retry_after_seconds, blocked=error.retry == "terminal", replayed=manual_replay, ) self._note_escalation( obligation, turn, now=now, availability_retry=error.retry == "retryable", offering_id=request.offering_id, ) return None manifest = self._verified_manifest(result) if request.coverage.expected == "full" and manifest.coverage.status != "full": raise LedgerConflictError( "MANIFEST_MISMATCH", "a full-coverage request cannot complete with partial or missing coverage", ) self._validate_manifest_currency(obligation, manifest) rows = await self._read_rows(request, manifest) # ``now`` freezes dispatch/lease/cutoff decisions, not publication. # A conforming source can observe finality while acquisition is running. # Every successful scheduled read, even unchanged content, reaches the # atomic revision/observation/checkpoint commit with this fresh anchor. published_at = self._clock() if _utc(published_at) < _utc(now): raise LedgerConflictError( "PUBLICATION_TIME_INVALID", "producer clock regressed during acquisition; correct the clock before retrying", ) committed = await self.commit_revision_from_manifest( obligation, manifest, rows=rows, finality=finality, now=published_at, turn=turn, acquisition=acquisition, ) await self._clear_retry(obligation, turn=turn, now=published_at) return committed def _retry_keys(self, obligation: ReportingObligationRecord) -> tuple[str, str, str]: source = hashlib.sha256( canonical_json_utf8_v1( [self._offerings.publication_namespace, self._offerings.source_scope] ) ).hexdigest() return ( f"slice:{obligation.account_id}:{obligation.reporting_obligation_id}", f"account:{obligation.account_id}:{source}", f"source:{source}", ) async def _get_retry(self, key: str) -> RetryScheduleEntry | None: return await self._retry_store.get_retry_schedule(scope_key=key) async def _record_retry(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: return await self._retry_store.record_retry_schedule(entry) @staticmethod def _refresh_retry(turn: WorkerTurn, *, now: datetime) -> None: candidates = [] for keys in turn._retry_keys_by_obligation.values(): entries = [turn._retry_entries[key] for key in keys if key in turn._retry_entries] if any(entry.blocked for entry in entries): continue due = [entry.retry_not_before for entry in entries if entry.attempt > 0] if due: candidates.append(max(due, key=_utc)) turn.earliest_retry_at = min(candidates, key=_utc) if candidates else None async def _retry_not_before( self, obligation: ReportingObligationRecord, turn: WorkerTurn, *, now: datetime ) -> bool: keys = self._retry_keys(obligation) schedules = [await self._get_retry(key) for key in keys] deferred = [ entry for entry in schedules if entry is not None and entry.attempt > 0 and (entry.blocked or _utc(entry.retry_not_before) > _utc(now)) ] if not deferred: return False turn._retry_keys_by_obligation[obligation.reporting_obligation_id] = keys turn._retry_entries.update( (entry.scope_key, entry) for entry in schedules if entry is not None ) self._refresh_retry(turn, now=now) return True async def _schedule_retry( self, obligation: ReportingObligationRecord, turn: WorkerTurn, *, now: datetime, scope: str = "slice", retry_after_seconds: float | None = None, blocked: bool = False, replayed: bool = False, ) -> None: # Dispatch ``now`` can be synthetic in tests and integrations. A slow # source may finish long after it, so anchor the cooldown at completion. completed_at = max(_utc(now), _utc(self._clock())) keys = self._retry_keys(obligation) shared = keys[1 if scope == "account" else 2] affected = keys[:1] if scope == "slice" else ((shared,) if blocked else (keys[0], shared)) turn._retry_keys_by_obligation[obligation.reporting_obligation_id] = keys for key in keys: current = await self._get_retry(key) if current is not None: turn._retry_entries[key] = current for key in affected: previous = await self._get_retry(key) attempt = (previous.attempt if previous is not None else 0) + 1 # Stable per-scope jitter keeps retries spread across configurations # and gives identical decisions to workers sharing a store. jitter = ( 0.8 + 0.4 * int.from_bytes(hashlib.sha256(f"{key}:{attempt}".encode()).digest()[:2], "big") / 65535 ) exponential = self._retry_initial_delay * (2 ** min(attempt - 1, 30)) delay = min(self._retry_max_delay, exponential * jitter) if retry_after_seconds is not None: try: delay = max(delay, timedelta(seconds=retry_after_seconds)) except OverflowError: delay = timedelta.max if completed_at >= _utc(obligation.automated_recovery_deadline_at): delay = max(delay, self._post_deadline_retry_interval) try: retry_at = completed_at + delay except OverflowError: retry_at = datetime.max.replace(tzinfo=timezone.utc) stored = await self._record_retry( RetryScheduleEntry( key, completed_at if blocked else retry_at, attempt, blocked, recorded_at=completed_at, replayed=replayed, ) ) turn._retry_entries[key] = stored if replayed: # A manual probe supersedes an earlier terminal blast radius. The # new error's declared scope governs subsequent automatic work. for key in keys: if key in affected: continue prior = await self._get_retry(key) if prior is not None and prior.blocked: turn._retry_entries[key] = await self._record_retry( RetryScheduleEntry( key, completed_at, 0, recorded_at=completed_at, replayed=True ) ) self._refresh_retry(turn, now=completed_at) async def _clear_retry( self, obligation: ReportingObligationRecord, *, turn: WorkerTurn, now: datetime ) -> None: for key in self._retry_keys(obligation): prior = await self._get_retry(key) if prior is not None and prior.attempt: stored = await self._record_retry( RetryScheduleEntry(key, _utc(now), 0, recorded_at=_utc(now)) ) turn._retry_entries[key] = stored turn._retry_keys_by_obligation.pop(obligation.reporting_obligation_id, None) self._refresh_retry(turn, now=now) async def _admitted_source_constituents( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord ) -> tuple[ReportingConstituent, ...] | None: from adcp.reporting.ledger.producer_progress import ReportingProducerProgress from adcp.reporting.production.contracts import _SourceAuthorizationRevokedError if not isinstance(self._store, ReportingProducerProgress): return None try: return await self._store.producer_constituents(configuration, obligation) except _SourceAuthorizationRevokedError: source_revoked(configuration.account_id) def _check_source_authorization( self, configuration: ReportingConfiguration, offering_id: str, admitted_constituents: tuple[ReportingConstituent, ...] | None, ) -> None: from adcp.reporting.materializer.contracts import failure from adcp.reporting.production.contracts import ( ReportingProductionSource, ReportingProductionSourceBinding, _SourceAuthorizationRevokedError, ) require_account_work(configuration.account_id) if isinstance(self._source, ReportingProductionSource): try: binding = self._source.configuration_binding(configuration) if binding is None: source_revoked(configuration.account_id) if type(binding) is not ReportingProductionSourceBinding: raise failure("BINDING_MISMATCH") binding.check(configuration, self._source.capabilities, offering_id) if ( admitted_constituents is not None and binding.constituents() != admitted_constituents ): raise failure("BINDING_MISMATCH") except _SourceAuthorizationRevokedError: raise except Exception: raise failure("BINDING_MISMATCH") from None @asynccontextmanager async def _source_publication( self, configuration: ReportingConfiguration, offering_id: str, *, admitted_constituents: tuple[ReportingConstituent, ...] | None, seals: ReportingSealStore | None = None, ) -> AsyncIterator[InlineSealPublisher | None]: from adcp.reporting.production.contracts import ReportingProductionSource if not isinstance(self._source, ReportingProductionSource): yield None return publication = getattr(self._store, "_source_publication", None) if publication is None: raise TypeError("production source publication requires an SDK account lock") async with publication(configuration.account_id, seals=seals) as publish_seal: self._check_source_authorization(configuration, offering_id, admitted_constituents) yield publish_seal async def _execute_source( self, configuration: ReportingConfiguration, request: ReportingSourceSliceRequestV1, *, admitted_constituents: tuple[ReportingConstituent, ...] | None, cancel: asyncio.Event, ) -> ReportingSourceExecutorResult | _InlineStorageFailure: from adcp.reporting.inline_storage import InlineStorageError try: with bind_inline_publication( configuration.account_id, lambda seals: self._source_publication( configuration, request.offering_id, admitted_constituents=admitted_constituents, seals=seals, ), ): # Check inside the executing task: scheduling it is not dispatch. # Revocation never cancels a fetch that has already started. self._check_source_authorization( configuration, request.offering_id, admitted_constituents ) return await self._source.execute(request, cancel=cancel) except InlineStorageError as error: return _InlineStorageFailure(error.code) @asynccontextmanager async def _revision_publication( self, obligation: ReportingObligationRecord, offering_id: str ) -> AsyncIterator[None]: from adcp.reporting.production.contracts import ReportingProductionSource if not isinstance(self._source, ReportingProductionSource): yield return configuration = next( ( candidate for candidate in await self._store.list_configurations( caller=ReportingDeliveryPrincipal( obligation.account_id, obligation.consumer_id ), delivery_config_ids=[obligation.delivery_config_id], ) if candidate.generation_key == obligation.generation_key ), None, ) if configuration is None: raise LedgerConflictError("HISTORY_UNAVAILABLE", "source generation is unavailable") # Read immutable admission scope before taking the publication lock. It # is not an authorization grant: the callback is checked again inside # the lock against this exact mapping, including on replay. constituents = await self._admitted_source_constituents(configuration, obligation) async with self._source_publication( configuration, offering_id, admitted_constituents=constituents ): yield def _restatement_store(self) -> RestatementCheckpointStore: explicit = all( inspect.getattr_static(self._store, name, None) is not None for name in ("get_restatement_checkpoint", "record_restatement_checkpoint") ) if not explicit or not isinstance(self._store, RestatementCheckpointStore): raise LedgerConflictError( "RESTATEMENT_CHECKPOINTS_NOT_SUPPORTED", "a source settling window requires explicitly implemented durable " "restatement checkpoint methods", ) return self._store def _observation_store(self) -> ProvisionalObservationStore: explicit = all( inspect.getattr_static(self._store, name, None) is not None for name in ( "reserve_provisional_acquisition", "get_provisional_observation", "commit_provisional_observation", ) ) if not explicit or not isinstance(self._store, ProvisionalObservationStore): raise LedgerConflictError( "PROVISIONAL_OBSERVATIONS_NOT_SUPPORTED", "scheduled provisional reads require explicitly implemented atomic " "observation methods, including any decorating publisher's preparation", ) return self._store async def _pending_acquisition( self, obligation: ReportingObligationRecord, *, ordinal: int ) -> ProvisionalAcquisition | None: if inspect.getattr_static( self._store, "get_provisional_acquisition", None ) is None or not isinstance(self._store, PendingProvisionalAcquisitionStore): return None return await self._store.get_provisional_acquisition( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ordinal=ordinal, ) def _note_escalation( self, obligation: ReportingObligationRecord, turn: WorkerTurn, *, now: datetime, availability_retry: bool = False, target_finality: str | None = None, offering_id: str | None = None, ) -> None: """Record that this obligation has run out of automated recovery. The health projection derives ``delayed`` versus ``action_required`` from the clock, so the worker does not set a state -- it only surfaces that the boundary has passed so a supervisor can alert instead of letting a dead feed idle inside a retry loop forever. """ if availability_retry and _utc(now) < self._source_available_at( obligation, target_finality=target_finality, offering_id=offering_id, worst_case=True, ): return if _utc(now) >= _utc(obligation.automated_recovery_deadline_at): turn.escalated.append(obligation.reporting_obligation_id) def _verified_manifest(self, result: ReportingSourceExecutorResult) -> SourceBatchManifestV1: response = result.response manifest_bytes = result.manifest_bytes assert response is not None and manifest_bytes is not None return parse_verified_source_batch_manifest_v1(response.manifest, manifest_bytes) async def _read_rows( self, request: ReportingSourceSliceRequestV1, manifest: SourceBatchManifestV1 ) -> list[dict[str, Any]]: """Materialize the manifest's staged rows into the revision's frozen rows.""" if self._object_reader is None: return [] import json rows: list[dict[str, Any]] = [] cancel = asyncio.Event() for staged in manifest.objects: payload = await self._object_reader.read( object_ref=staged.object_ref, object_generation=staged.object_generation, account_id=request.identity.account_id, source_scope=request.identity.source_scope, cancel=cancel, ) if hashlib.sha256(payload).hexdigest() != staged.sha256: raise LedgerConflictError( "STAGED_OBJECT_MISMATCH", f"staged object {staged.ordinal} does not match its manifest digest", ) for line in payload.decode("utf-8").splitlines(): if line.strip(): rows.append(json.loads(line)) return rows async def commit_revision_from_manifest( self, obligation: ReportingObligationRecord, manifest: SourceBatchManifestV1, *, rows: Sequence[dict[str, Any]], finality: str, now: datetime | None = None, turn: WorkerTurn | None = None, acquisition: ProvisionalAcquisition | None = None, ) -> ReportingRevisionRecord: """Project a verified manifest into an immutable ledger revision. A snapshot restatement supersedes the current snapshot leaf; there is no edit path. An official close is terminal, so a later source correction must arrive as an adjustment instead. ``now`` is a trusted publication instant, unlike the dispatch instant accepted by ``acquire_obligation``. Replaying a publication retains its original creation time and parent. """ obligation = await self._stored_obligation(obligation) self._validate_manifest_currency(obligation, manifest) if any(cell.status == "unsupported" for cell in manifest.metric_availability): try: offering = self._source.capabilities.offering(manifest.offering_id) _validate_metric_applicability(offering.metrics, manifest.metric_availability) except (KeyError, ValueError): raise LedgerConflictError( "MANIFEST_MISMATCH", "unsupported cells require partial metric support with a reason", ) from None now = now or self._clock() turn = turn or WorkerTurn() existing = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) selection = select_reporting_revision( existing, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality="snapshot", ) # A retained official close coexists with the snapshot chain and wins # whole-history selection outright, so it is never the snapshot leaf. # Reading it as one would root a restatement at ``None`` and split the # obligation into two snapshot roots -- a permanently corrupt history # over immutable rows, with no repair path. leaf = select_reporting_revision( tuple(item for item in existing if item.finality == "snapshot"), account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality="snapshot", ) if selection.kind == "corrupt" or leaf.kind == "corrupt": raise LedgerConflictError("HISTORY_UNAVAILABLE", "the revision history requires repair") supersedes = ( leaf.revision.reporting_revision_id if finality == "snapshot" and leaf.kind == "selected" else None ) if acquisition is not None and finality == "snapshot": supersedes = acquisition.predecessor_revision_id control_totals = tuple((total.name, total.value) for total in manifest.control_totals) revision_id = f"rpr_{manifest.publication_id[4:44]}" prior = next((item for item in existing if item.reporting_revision_id == revision_id), None) created_at = prior.created_at if prior is not None else now if prior is not None: # Still reconstruct and verify the supplied content below. Merely # finding the ID must not bypass immutable-content validation. # On replay this retained parent wins over the acquisition's # predecessor, just as the retained creation time wins over now. supersedes = prior.supersedes_reporting_revision_id if ( _utc(manifest.acquired_at) > _utc(now) or _utc(manifest.observed_at) > _utc(created_at) or _utc(manifest.finality_evidence.observed_at) > _utc(created_at) or ( finality == "official" and _utc(manifest.finality_evidence.observed_at) < _utc(obligation.period.end) ) ): raise LedgerConflictError( "PUBLICATION_TIME_INVALID", "source observation or finality is outside the publication time bounds; " "check source evidence and the producer clock before retrying", ) revision = ReportingRevisionRecord( reporting_revision_id=revision_id, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, finality="official" if finality == "official" else "snapshot", revision_content_sha256=revision_content_sha256( reporting_revision_id=revision_id, row_count=manifest.row_count, control_totals=control_totals, reporting_rows=rows, ), row_count=manifest.row_count, control_totals=control_totals, observed_at=manifest.observed_at, data_through=manifest.data_through, created_at=created_at, supersedes_reporting_revision_id=supersedes, finality_basis=( ( "stabilized" if manifest.finality_evidence.basis == "elapsed_settlement_window" else "source_final" ) if finality == "official" else None ), finality_policy_id=( f"{obligation.report_definition_id}:{manifest.offering_id}" if finality == "official" else None ), finalized_at=manifest.finality_evidence.observed_at if finality == "official" else None, source_publication_id=manifest.publication_id, source_manifest_sha256=manifest.content_fingerprint.split(":", 1)[-1], ) async with self._revision_publication(obligation, manifest.offering_id): if self._revision_verifier is not None: from adcp.reporting.materializer.publication import verified_publication revision = verified_publication(self._revision_verifier, obligation, revision, rows) if acquisition is None: committed = await self._store.commit_revision(revision, rows) else: # ``now`` is the publication anchor, sampled after row acquisition. # The bounds check above already rejects acquired_at after now. checked_at = _utc(now) boundary = manifest.finality_evidence.provisional_until or ( _utc(obligation.period.end) + acquisition.policy.window ) next_due = ( min(checked_at + acquisition.policy.cadence, _utc(boundary)) if finality == "snapshot" and checked_at < _utc(boundary) else None ) committed = await self._observation_store().commit_provisional_observation( ProvisionalObservation( acquisition, revision_id, checked_at, _utc(boundary), next_due, manifest.model_dump_json(), ), revision, rows, ) turn.revisions_committed.append(committed.reporting_revision_id) return committed async def _stored_obligation( self, obligation: ReportingObligationRecord ) -> ReportingObligationRecord: """Always use the durable winner, including for public low-level calls.""" stored = await self._store.get_obligation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if stored is None: raise LedgerConflictError( "OBLIGATION_NOT_FOUND", "commit the obligation before source work" ) if stored.generation_key != obligation.generation_key: raise LedgerConflictError( "CONFIGURATION_GENERATION_MISMATCH", "the obligation's retained generation differs" ) return stored @staticmethod def _validate_manifest_currency( obligation: ReportingObligationRecord, manifest: SourceBatchManifestV1 ) -> None: currency = require_frozen_currency(obligation.currency) if manifest.currency != currency: raise ReportingCurrencyError( "CURRENCY_MISMATCH", "source manifest currency disagrees with the frozen obligation" ) identity = manifest.identity if ( identity.account_id != obligation.account_id or identity.reporting_obligation_id != obligation.reporting_obligation_id or identity.delivery_config_id != obligation.delivery_config_id or identity.delivery_config_version != obligation.delivery_config_version or identity.report_definition_id != obligation.report_definition_id ): raise LedgerConflictError("MANIFEST_MISMATCH", "manifest does not bind this obligation") definition = obligation.definition units = {"spend": currency} ReportingProducer._validate_definition_binding(obligation, manifest.contract) if definition is not None: units.update(definition.monetary_metric_units) units.update(definition.monetary_control_total_units) for total in manifest.control_totals: expected = units.get(total.name) if expected is not None and total.unit is not None and total.unit != expected: raise ReportingCurrencyError( "CURRENCY_MISMATCH", "source control total unit disagrees with the frozen currency", ) @staticmethod def _validate_definition_binding( obligation: ReportingObligationRecord, contract: ReportingContractIdentityV1 ) -> None: definition = obligation.definition if definition is not None and ( contract.report_definition_id != obligation.report_definition_id or contract.reporting_profile != obligation.reporting_profile or any(getattr(contract, name) != value for name, value in definition.to_wire().items()) ): raise LedgerConflictError( "REPORT_DEFINITION_MISMATCH", "the source definition differs from the obligation's pin", ) @staticmethod def _current_snapshot_leaf(revisions: Sequence[ReportingRevisionRecord]) -> str | None: current = ReportingProducer._current_snapshot(revisions) return current.reporting_revision_id if current is not None else None @staticmethod def _current_snapshot( revisions: Sequence[ReportingRevisionRecord], ) -> ReportingRevisionRecord | None: snapshots = [item for item in revisions if item.finality == "snapshot"] if not snapshots: return None superseded = { item.supersedes_reporting_revision_id for item in snapshots if item.supersedes_reporting_revision_id } leaves = [item for item in snapshots if item.reporting_revision_id not in superseded] if not leaves: return None return max(leaves, key=lambda item: (_utc(item.created_at), item.reporting_revision_id)) # -- slice construction ---------------------------------------------- def _build_slice( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord, offering_id: str, *, finality: str | None = None, now: datetime, observation: int = 0, constituents: tuple[ReportingConstituent, ...] | None = None, trigger: ReportingTriggerKind = "scheduled_poll", ) -> ReportingSourceSliceRequestV1: """Freeze one slice request from the obligation. ``source_execution_key`` is derived from the obligation, the offering, and the **observation ordinal**. Without a settling policy the ordinal is the number of revisions already committed. With one it comes from a durable checkpoint that also advances after an unchanged source read. That last term is what makes both behaviors correct at once. A *retry* of a failed acquisition commits nothing, so the ordinal is unchanged, the key is unchanged, and the source replays its sealed publication rather than minting a second one. A *restatement* follows a committed revision (or a successful unchanged check), so the ordinal advances and the source is genuinely re-read as a new immutable observation. The ordinal comes from durable state, not from the clock, so neither behavior depends on wall time. """ offering = self._source.capabilities.offering(offering_id) resolved_constituents: list[ReportingConstituent] = ( list(constituents) if constituents is not None else [ MediaBuyConstituentV1( constituent_id=media_buy_id, product_id=obligation.report_definition_id, media_buy_id=media_buy_id, ) for media_buy_id in obligation.media_buy_ids ] ) if not resolved_constituents: raise LedgerConflictError( "EMPTY_DENOMINATOR", "an obligation with no media buys has no source work; it is a platform-owned " "no-op, not a slice", ) source_execution_key = ( "rse-" + hashlib.sha256( canonical_json_utf8_v1( [obligation.reporting_obligation_id, offering_id, observation] ) ).hexdigest()[:40] ) resolved_finality = finality or obligation.required_finality publication_class: ReportingPublicationClass = ( "AUTHORITATIVE" if resolved_finality == "official" else "PROVISIONAL_SNAPSHOT" ) return ReportingSourceSliceRequestV1( identity=ReportingSourceIdentityV1( account_id=obligation.account_id, delivery_config_id=obligation.delivery_config_id, delivery_config_version=obligation.delivery_config_version, report_definition_id=obligation.report_definition_id, reporting_obligation_id=obligation.reporting_obligation_id, period_key=obligation.period.period_key, source_execution_key=source_execution_key, run_id=f"run-{_utc(now).strftime('%Y%m%dT%H%M%S')}", logical_slice_fingerprint=_logical_slice_fingerprint(obligation, offering_id), source_scope=dict(self._offerings.source_scope), ), adapter_build=self._source.capabilities.adapter_build, offering_id=offering_id, publication_namespace=self._offerings.publication_namespace, publication_class=publication_class, contract=offering.contract, period=ReportingSourcePeriodV1( period_key=obligation.period.period_key, source_local_date=_source_local_date( obligation.period.start, obligation.period.source_timezone ), start=obligation.period.start, end=obligation.period.end, source_timezone=obligation.period.source_timezone, # The read cutoff is "as late as the source could possibly know # about", floored at the period end: a closed period is always # readable through its own boundary, and reading past *now* # would ask the source about a future it cannot answer for. source_read_cutoff_at=max(_utc(now), _utc(obligation.period.end)), grain=self._source.capabilities.offering(offering_id).grain, windowing=self._source.capabilities.offering(offering_id).windowing, ), revision_kind="authoritative" if publication_class == "AUTHORITATIVE" else "snapshot", trigger=trigger, coverage=ReportingSourceCoverageRequestV1( expected="full", constituents=resolved_constituents, denominator_fingerprint=coverage_denominator_fingerprint_v1(resolved_constituents), ), requested_metrics=list(self._offerings.requested_metrics), requested_dimensions=list(self._offerings.requested_dimensions), currency=require_frozen_currency(obligation.currency), deadline_at=_utc(now) + self._offerings.slice_timeout, )Closes periods, drives the source, and commits immutable revisions.
Instance variables
prop escalation : ReportingDeliveryEscalation-
Expand source code
@property def escalation(self) -> ReportingDeliveryEscalation: """The advertised escalation commitment, for the status handler. Pass the same object to :class:`~adcp.reporting.ledger.status.ReportingStatusHandler` so the projection honours exactly the window the seller published. A handler with a different window than the capability block would escalate on a clock no buyer can see. """ return self._escalationThe advertised escalation commitment, for the status handler.
Pass the same object to :class:
~adcp.reporting.ledger.status.ReportingStatusHandlerso the projection honours exactly the window the seller published. A handler with a different window than the capability block would escalate on a clock no buyer can see. prop store : ReportingLedgerStore-
Expand source code
@property def store(self) -> ReportingLedgerStore: return self._store
Methods
async def acquire_obligation(self,
configuration: ReportingConfiguration,
obligation: ReportingObligationRecord,
*,
restate: bool = False,
turn: WorkerTurn | None = None,
now: datetime | None = None,
target_finality: str | None = None,
track_settling: bool = False,
manual_replay: bool = False) ‑> ReportingRevisionRecord | None-
Expand source code
async def acquire_obligation( self, configuration: ReportingConfiguration, obligation: ReportingObligationRecord, *, restate: bool = False, turn: WorkerTurn | None = None, now: datetime | None = None, target_finality: str | None = None, track_settling: bool = False, manual_replay: bool = False, ) -> ReportingRevisionRecord | None: """Drive one obligation from its source and commit what comes back. Returns the committed revision, or ``None`` when the source is not ready or the obligation is already satisfied. ``restate`` asks for a *new observation* of an already-satisfied snapshot obligation. Without it a satisfied obligation is left alone, because re-reading a settled period on every worker turn would burn upstream quota to republish bytes nobody asked for. ``now`` freezes dispatch and the source read cutoff. Revision creation uses a fresh producer clock sample after the staged objects are read. """ if configuration.generation_key != obligation.generation_key: raise LedgerConflictError( "CONFIGURATION_GENERATION_MISMATCH", "the source configuration must belong to the obligation's account and generation", ) obligation = await self._stored_obligation(obligation) turn = turn or WorkerTurn() now = now or self._clock() finality = target_finality or obligation.required_finality if not manual_replay and await self._retry_not_before(obligation, turn, now=now): retry_offering_id: str | None = None if track_settling: checkpoint = await self._restatement_store().get_restatement_checkpoint( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) ordinal = ( checkpoint.next_observation if checkpoint is not None else len( await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) ) ) pending = await self._pending_acquisition(obligation, ordinal=ordinal) if pending is not None: retry_offering_id = pending.request().offering_id keys = turn._retry_keys_by_obligation[obligation.reporting_obligation_id] retryable = not any( turn._retry_entries[key].blocked for key in keys if key in turn._retry_entries ) self._note_escalation( obligation, turn, now=now, availability_retry=retryable, target_finality=finality, offering_id=retry_offering_id, ) return None revisions = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) selection = select_reporting_revision( revisions, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality=obligation.required_finality, ) if selection.kind == "corrupt": raise LedgerConflictError("HISTORY_UNAVAILABLE", "the revision history requires repair") current = selection.revision if selection.kind == "selected" else None if current is not None and current.finality == "official": # An official close is terminal. A later source correction is an # adjustment, never another acquisition. return None satisfied = current is not None and current.readable if satisfied and not restate: return None # Everything below needs the frozen code: the slice request carries it, # the manifest is checked against it, and the revision is written under # it. Gate here rather than earlier so a settled legacy obligation stays # the no-op it already was instead of becoming an error on every turn. require_frozen_currency(obligation.currency) offering_id = self._offerings.offering_for(finality) if offering_id is None: raise LedgerConflictError( "NO_OFFERING_FOR_FINALITY", f"this producer declares no source offering for {finality} reporting", ) constituents = await self._admitted_source_constituents(configuration, obligation) checkpoint_store = self._restatement_store() if track_settling else None checkpoint = ( await checkpoint_store.get_restatement_checkpoint( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) if checkpoint_store is not None else None ) observation = checkpoint.next_observation if checkpoint is not None else len(revisions) request = self._build_slice( configuration, obligation, offering_id, finality=finality, now=now, observation=observation, constituents=constituents, trigger="manual_replay" if manual_replay else "scheduled_poll", ) acquisition = None if track_settling: policy = self._settling_policy(configuration, obligation) assert policy is not None latest = await self._observation_store().get_provisional_observation( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) frozen_policy = ( latest.acquisition.policy if latest is not None else ProvisionalPolicy( policy.restatement_window, policy.restatement_cadence, policy.official_close_lag ) ) leaf = self._current_snapshot(revisions) acquisition = await self._observation_store().reserve_provisional_acquisition( ProvisionalAcquisition( request.model_dump_json(), observation, frozen_policy, leaf.reporting_revision_id if leaf is not None else None, checkpoint.provisional_until if checkpoint is not None else None, ) ) request = acquisition.request(deadline_at=_utc(now) + self._offerings.slice_timeout) if manual_replay: request = request.model_copy(update={"trigger": "manual_replay"}) finality = ( "snapshot" if request.publication_class == "PROVISIONAL_SNAPSHOT" else "official" ) cancel = asyncio.Event() execution = asyncio.create_task( self._execute_source( configuration, request, admitted_constituents=constituents, cancel=cancel ) ) try: result = await asyncio.wait_for( asyncio.shield(execution), timeout=self._offerings.slice_timeout.total_seconds(), ) except asyncio.CancelledError: cancel.set() await cancel_and_settle(execution) raise except asyncio.TimeoutError: cancel.set() # wait_for's own cancellation join can be interrupted by a second # cancellation of this producer (notably on Python 3.10). Retain # the execution explicitly until even synchronous work has settled. await cancel_and_settle(execution) turn.slices_failed.append(obligation.reporting_obligation_id) await self._schedule_retry(obligation, turn, now=now, replayed=manual_replay) self._note_escalation( obligation, turn, now=now, availability_retry=True, offering_id=request.offering_id, ) return None if isinstance(result, _InlineStorageFailure): from adcp.reporting.inline_storage import InlineStorageError # Return closed data from the executor task: a raw driver exception # must not survive through a context manager or Task wakeup frame. raise InlineStorageError(result.code) if not result.ok: error = result.error assert error is not None logger.info( "reporting slice failed obligation=%s code=%s retry=%s", obligation.reporting_obligation_id, error.code, error.retry, ) turn.slices_failed.append(obligation.reporting_obligation_id) await self._schedule_retry( obligation, turn, now=now, scope=error.scope, retry_after_seconds=error.retry_after_seconds, blocked=error.retry == "terminal", replayed=manual_replay, ) self._note_escalation( obligation, turn, now=now, availability_retry=error.retry == "retryable", offering_id=request.offering_id, ) return None manifest = self._verified_manifest(result) if request.coverage.expected == "full" and manifest.coverage.status != "full": raise LedgerConflictError( "MANIFEST_MISMATCH", "a full-coverage request cannot complete with partial or missing coverage", ) self._validate_manifest_currency(obligation, manifest) rows = await self._read_rows(request, manifest) # ``now`` freezes dispatch/lease/cutoff decisions, not publication. # A conforming source can observe finality while acquisition is running. # Every successful scheduled read, even unchanged content, reaches the # atomic revision/observation/checkpoint commit with this fresh anchor. published_at = self._clock() if _utc(published_at) < _utc(now): raise LedgerConflictError( "PUBLICATION_TIME_INVALID", "producer clock regressed during acquisition; correct the clock before retrying", ) committed = await self.commit_revision_from_manifest( obligation, manifest, rows=rows, finality=finality, now=published_at, turn=turn, acquisition=acquisition, ) await self._clear_retry(obligation, turn=turn, now=published_at) return committedDrive one obligation from its source and commit what comes back.
Returns the committed revision, or
Nonewhen the source is not ready or the obligation is already satisfied.restateasks for a new observation of an already-satisfied snapshot obligation. Without it a satisfied obligation is left alone, because re-reading a settled period on every worker turn would burn upstream quota to republish bytes nobody asked for.nowfreezes dispatch and the source read cutoff. Revision creation uses a fresh producer clock sample after the staged objects are read. def advertised_reporting_delivery(self,
*,
consumer_status_task: bool,
offerings: Sequence[Mapping[str, Any]],
automated_recovery_window: timedelta,
status_retention_days: int,
extra: Mapping[str, Any] | None = None) ‑> dict[str, typing.Any]-
Expand source code
def advertised_reporting_delivery( self, *, consumer_status_task: bool, offerings: Sequence[Mapping[str, Any]], automated_recovery_window: timedelta, status_retention_days: int, extra: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """The complete ``media_buy.reporting_delivery`` block for this producer. Returns a *whole* capability document, not a fragment, so the result can be validated against ``core/reporting-delivery-capabilities.json`` before it is published. A fragment would push six required fields onto the caller to remember, and an under-filled capability block is exactly the kind of thing that passes review and fails a buyer's validator. The seller supplies what only it knows -- its ``offerings``, its seller-wide recovery window and retention. This method supplies the task names and the Reliable Reporting declarations, because those follow from the producer actually running rather than from configuration. ``consumer_status_task`` is an explicit argument rather than inferred: advertising it while the ingest is disabled is the half-implemented loop this module's docstring warns about, and a buyer that can file statements nobody reads believes it has told you. """ return _advertised_reporting_delivery( escalation=self._escalation, consumer_status_task=consumer_status_task, offerings=offerings, automated_recovery_window=automated_recovery_window, status_retention_days=status_retention_days, extra=extra, )The complete
media_buy.reporting_deliveryblock for this producer.Returns a whole capability document, not a fragment, so the result can be validated against
core/reporting-delivery-capabilities.jsonbefore it is published. A fragment would push six required fields onto the caller to remember, and an under-filled capability block is exactly the kind of thing that passes review and fails a buyer's validator.The seller supplies what only it knows – its
offerings, its seller-wide recovery window and retention. This method supplies the task names and the Reliable Reporting declarations, because those follow from the producer actually running rather than from configuration.consumer_status_taskis an explicit argument rather than inferred: advertising it while the ingest is disabled is the half-implemented loop this module's docstring warns about, and a buyer that can file statements nobody reads believes it has told you. async def close_elapsed_periods(self,
configuration: ReportingConfiguration,
*,
now: datetime | None = None) ‑> list[ReportingObligationRecord]-
Expand source code
async def close_elapsed_periods( self, configuration: ReportingConfiguration, *, now: datetime | None = None ) -> list[ReportingObligationRecord]: """Commit an obligation for every elapsed eligible period. Public because a seller often wants to run this on its own cadence -- the obligation must land in the first ledger snapshot strictly after the period boundary, independent of whether the source is healthy. """ if configuration.quarantined: raise LedgerConflictError( "REPORTING_GENERATION_QUARANTINED", "legacy generations cannot run" ) turn = WorkerTurn() return await self._close_elapsed_periods(configuration, turn, now=now or self._clock())Commit an obligation for every elapsed eligible period.
Public because a seller often wants to run this on its own cadence – the obligation must land in the first ledger snapshot strictly after the period boundary, independent of whether the source is healthy.
async def commit_revision_from_manifest(self,
obligation: ReportingObligationRecord,
manifest: SourceBatchManifestV1,
*,
rows: Sequence[dict[str, Any]],
finality: str,
now: datetime | None = None,
turn: WorkerTurn | None = None,
acquisition: ProvisionalAcquisition | None = None) ‑> ReportingRevisionRecord-
Expand source code
async def commit_revision_from_manifest( self, obligation: ReportingObligationRecord, manifest: SourceBatchManifestV1, *, rows: Sequence[dict[str, Any]], finality: str, now: datetime | None = None, turn: WorkerTurn | None = None, acquisition: ProvisionalAcquisition | None = None, ) -> ReportingRevisionRecord: """Project a verified manifest into an immutable ledger revision. A snapshot restatement supersedes the current snapshot leaf; there is no edit path. An official close is terminal, so a later source correction must arrive as an adjustment instead. ``now`` is a trusted publication instant, unlike the dispatch instant accepted by ``acquire_obligation``. Replaying a publication retains its original creation time and parent. """ obligation = await self._stored_obligation(obligation) self._validate_manifest_currency(obligation, manifest) if any(cell.status == "unsupported" for cell in manifest.metric_availability): try: offering = self._source.capabilities.offering(manifest.offering_id) _validate_metric_applicability(offering.metrics, manifest.metric_availability) except (KeyError, ValueError): raise LedgerConflictError( "MANIFEST_MISMATCH", "unsupported cells require partial metric support with a reason", ) from None now = now or self._clock() turn = turn or WorkerTurn() existing = await self._store.list_revisions( account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, ) selection = select_reporting_revision( existing, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality="snapshot", ) # A retained official close coexists with the snapshot chain and wins # whole-history selection outright, so it is never the snapshot leaf. # Reading it as one would root a restatement at ``None`` and split the # obligation into two snapshot roots -- a permanently corrupt history # over immutable rows, with no repair path. leaf = select_reporting_revision( tuple(item for item in existing if item.finality == "snapshot"), account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, required_finality="snapshot", ) if selection.kind == "corrupt" or leaf.kind == "corrupt": raise LedgerConflictError("HISTORY_UNAVAILABLE", "the revision history requires repair") supersedes = ( leaf.revision.reporting_revision_id if finality == "snapshot" and leaf.kind == "selected" else None ) if acquisition is not None and finality == "snapshot": supersedes = acquisition.predecessor_revision_id control_totals = tuple((total.name, total.value) for total in manifest.control_totals) revision_id = f"rpr_{manifest.publication_id[4:44]}" prior = next((item for item in existing if item.reporting_revision_id == revision_id), None) created_at = prior.created_at if prior is not None else now if prior is not None: # Still reconstruct and verify the supplied content below. Merely # finding the ID must not bypass immutable-content validation. # On replay this retained parent wins over the acquisition's # predecessor, just as the retained creation time wins over now. supersedes = prior.supersedes_reporting_revision_id if ( _utc(manifest.acquired_at) > _utc(now) or _utc(manifest.observed_at) > _utc(created_at) or _utc(manifest.finality_evidence.observed_at) > _utc(created_at) or ( finality == "official" and _utc(manifest.finality_evidence.observed_at) < _utc(obligation.period.end) ) ): raise LedgerConflictError( "PUBLICATION_TIME_INVALID", "source observation or finality is outside the publication time bounds; " "check source evidence and the producer clock before retrying", ) revision = ReportingRevisionRecord( reporting_revision_id=revision_id, account_id=obligation.account_id, reporting_obligation_id=obligation.reporting_obligation_id, finality="official" if finality == "official" else "snapshot", revision_content_sha256=revision_content_sha256( reporting_revision_id=revision_id, row_count=manifest.row_count, control_totals=control_totals, reporting_rows=rows, ), row_count=manifest.row_count, control_totals=control_totals, observed_at=manifest.observed_at, data_through=manifest.data_through, created_at=created_at, supersedes_reporting_revision_id=supersedes, finality_basis=( ( "stabilized" if manifest.finality_evidence.basis == "elapsed_settlement_window" else "source_final" ) if finality == "official" else None ), finality_policy_id=( f"{obligation.report_definition_id}:{manifest.offering_id}" if finality == "official" else None ), finalized_at=manifest.finality_evidence.observed_at if finality == "official" else None, source_publication_id=manifest.publication_id, source_manifest_sha256=manifest.content_fingerprint.split(":", 1)[-1], ) async with self._revision_publication(obligation, manifest.offering_id): if self._revision_verifier is not None: from adcp.reporting.materializer.publication import verified_publication revision = verified_publication(self._revision_verifier, obligation, revision, rows) if acquisition is None: committed = await self._store.commit_revision(revision, rows) else: # ``now`` is the publication anchor, sampled after row acquisition. # The bounds check above already rejects acquired_at after now. checked_at = _utc(now) boundary = manifest.finality_evidence.provisional_until or ( _utc(obligation.period.end) + acquisition.policy.window ) next_due = ( min(checked_at + acquisition.policy.cadence, _utc(boundary)) if finality == "snapshot" and checked_at < _utc(boundary) else None ) committed = await self._observation_store().commit_provisional_observation( ProvisionalObservation( acquisition, revision_id, checked_at, _utc(boundary), next_due, manifest.model_dump_json(), ), revision, rows, ) turn.revisions_committed.append(committed.reporting_revision_id) return committedProject a verified manifest into an immutable ledger revision.
A snapshot restatement supersedes the current snapshot leaf; there is no edit path. An official close is terminal, so a later source correction must arrive as an adjustment instead.
nowis a trusted publication instant, unlike the dispatch instant accepted byacquire_obligation. Replaying a publication retains its original creation time and parent. async def run_configuration(self,
configuration: ReportingConfiguration,
*,
now: datetime | None = None) ‑> WorkerTurn-
Expand source code
async def run_configuration( self, configuration: ReportingConfiguration, *, now: datetime | None = None, ) -> WorkerTurn: """Run one turn for an already-routed configuration generation. High-level orchestrators use this entry point after freezing adapter, currency, and source scope for a specific generation. The orchestrator owns cross-process scheduling; obligation and revision writes remain convergent and immutable in the ledger store. Most adopters should continue using :meth:`run_worker`, whose store lease chooses a configuration automatically. """ from adcp.reporting.production.contracts import _SourceAuthorizationRevokedError boundary = now or self._clock() turn = WorkerTurn() with source_turn(): try: require_account_work(configuration.account_id) await self._close_elapsed_periods(configuration, turn, now=boundary) await self._acquire_pending(configuration, turn, now=boundary) except _SourceAuthorizationRevokedError: return turn return turnRun one turn for an already-routed configuration generation.
High-level orchestrators use this entry point after freezing adapter, currency, and source scope for a specific generation. The orchestrator owns cross-process scheduling; obligation and revision writes remain convergent and immutable in the ledger store.
Most adopters should continue using :meth:
run_worker, whose store lease chooses a configuration automatically. async def run_worker(self) ‑> WorkerTurn-
Expand source code
async def run_worker(self) -> WorkerTurn: """Run one leased turn. Safe to call from cron, a loop, or a supervisor. Returns immediately with an empty turn when nothing is leasable, so a caller can back off rather than spin. Standalone PostgreSQL turns retry deadlocks and lock timeouts at most twice, after rollback and fenced release. Each retry must acquire a new lease; other failures propagate. """ from adcp.reporting.ledger.pg import PgReportingLedgerStore retryable = ( self._store._worker_lock_errors() if isinstance(self._store, PgReportingLedgerStore) else () ) for attempt in range(3): leased = None with source_turn(): try: now = self._clock() leased = await self._store.lease_period_close( worker_id=self._worker_id, now=now, lease_seconds=self._lease_seconds ) turn = WorkerTurn(leased=leased) if leased is None: return turn await self._work_leased_configuration(leased, turn, now=now) except retryable: # Store transactions have already exited/rolled back. A # committed lease is released only under its original fence; # a competitor may win before the next acquisition. if leased is not None: await self._release_worker_lease(leased, retryable) if attempt == 2: raise except BaseException as original: if leased is not None: try: await self._release_worker_lease(leased, retryable) except retryable: # Never turn an immutable-content conflict or # cancellation into a retryable cleanup failure. raise original from None raise else: # Exhausted release retries escape from here, rather than # starting another turn or reporting a false empty success. await self._release_worker_lease(leased, retryable) return turn await asyncio.sleep(0.05 * (2**attempt)) raise AssertionError("unreachable worker retry") # pragma: no coverRun one leased turn. Safe to call from cron, a loop, or a supervisor.
Returns immediately with an empty turn when nothing is leasable, so a caller can back off rather than spin. Standalone PostgreSQL turns retry deadlocks and lock timeouts at most twice, after rollback and fenced release. Each retry must acquire a new lease; other failures propagate.
class ReportingReceiptKey (principal: ReportingDeliveryPrincipal,
reporting_receipt_id: str)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReceiptKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_receipt_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_receipt_id, maximum=255) if len(self.reporting_receipt_id) < 16: raise ValueError("a reporting receipt identifier requires at least 16 characters")ReportingReceiptKey(principal: 'ReportingDeliveryPrincipal', reporting_receipt_id: 'str')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var principal : ReportingDeliveryPrincipal-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReceiptKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_receipt_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_receipt_id, maximum=255) if len(self.reporting_receipt_id) < 16: raise ValueError("a reporting receipt identifier requires at least 16 characters") var reporting_receipt_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReceiptKey(_ClosedValue): principal: ReportingDeliveryPrincipal reporting_receipt_id: str def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.reporting_receipt_id, maximum=255) if len(self.reporting_receipt_id) < 16: raise ValueError("a reporting receipt identifier requires at least 16 characters")
class ReportingReceiptStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingReceiptStore(Protocol): """Transport-derived scope is mandatory. Exact retries precede temporal checks. ``received_at`` is assigned by the store; callers retry the immutable input or the returned record. Both receipt kinds share the same scoped ID namespace. """ async def record_revision_receipt( self, record: ReportingRevisionReceiptRecord ) -> tuple[ReportingRevisionReceiptRecord, bool]: ... async def record_adjustment_receipt( self, record: ReportingAdjustmentReceiptRecord ) -> tuple[ReportingAdjustmentReceiptRecord, bool]: ... async def get_receipt(self, key: ReportingReceiptKey) -> ReportingReceiptRecord | None: ...Transport-derived scope is mandatory. Exact retries precede temporal checks.
received_atis assigned by the store; callers retry the immutable input or the returned record. Both receipt kinds share the same scoped ID namespace.Ancestors
- typing.Protocol
- typing.Generic
Subclasses
Methods
async def get_receipt(self,
key: ReportingReceiptKey) ‑> ReportingRevisionReceiptRecord | ReportingAdjustmentReceiptRecord | None-
Expand source code
async def get_receipt(self, key: ReportingReceiptKey) -> ReportingReceiptRecord | None: ... async def record_adjustment_receipt(self,
record: ReportingAdjustmentReceiptRecord) ‑> tuple[ReportingAdjustmentReceiptRecord, bool]-
Expand source code
async def record_adjustment_receipt( self, record: ReportingAdjustmentReceiptRecord ) -> tuple[ReportingAdjustmentReceiptRecord, bool]: ... async def record_revision_receipt(self,
record: ReportingRevisionReceiptRecord) ‑> tuple[ReportingRevisionReceiptRecord, bool]-
Expand source code
async def record_revision_receipt( self, record: ReportingRevisionReceiptRecord ) -> tuple[ReportingRevisionReceiptRecord, bool]: ...
class ReportingReconciliationChange (sequence: int, record: ReportingDeliveryRecord)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationChange: sequence: int record: ReportingDeliveryRecordReportingReconciliationChange(sequence: 'int', record: 'ReportingDeliveryRecord')
Instance variables
var record : ReportingDestinationBinding | ReportingObligationDeliveryRecord | ReportingMaterializationAttempt | ReportingMaterializationRecord | ReportingMaterializationCheck | ReportingRevisionReceiptRecord | ReportingAdjustmentReceiptRecord-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationChange: sequence: int record: ReportingDeliveryRecord var sequence : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationChange: sequence: int record: ReportingDeliveryRecord
class ReportingReconciliationChangeStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingReconciliationFeedStore(Protocol): """An optional seam; existing Core and reconciliation protocols are unchanged. Persist partial pages with their cursor. Only the final page issues a checkpoint, which opens a new walk via ``changes_after``. Tokens bind caller, version, filters, bounds and the last emitted key, and survive reader restarts. Neither token is an authorization grant or a Core status checkpoint. """ async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: ...An optional seam; existing Core and reconciliation protocols are unchanged.
Persist partial pages with their cursor. Only the final page issues a checkpoint, which opens a new walk via
changes_after. Tokens bind caller, version, filters, bounds and the last emitted key, and survive reader restarts. Neither token is an authorization grant or a Core status checkpoint.Ancestors
- typing.Protocol
- typing.Generic
Methods
async def read_reconciliation_changes(self,
*,
caller: ReportingDeliveryPrincipal,
changes_after: ReportingReconciliationCheckpoint | None = None,
cursor: ReportingReconciliationCursor | None = None,
limit: int = 100,
filters: ReportingReconciliationFilter = ReportingReconciliationFilter(record_kinds=(), reporting_obligation_id=None)) ‑> ReportingReconciliationPage-
Expand source code
async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: ...
class ReportingReconciliationFeedStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingReconciliationFeedStore(Protocol): """An optional seam; existing Core and reconciliation protocols are unchanged. Persist partial pages with their cursor. Only the final page issues a checkpoint, which opens a new walk via ``changes_after``. Tokens bind caller, version, filters, bounds and the last emitted key, and survive reader restarts. Neither token is an authorization grant or a Core status checkpoint. """ async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: ...An optional seam; existing Core and reconciliation protocols are unchanged.
Persist partial pages with their cursor. Only the final page issues a checkpoint, which opens a new walk via
changes_after. Tokens bind caller, version, filters, bounds and the last emitted key, and survive reader restarts. Neither token is an authorization grant or a Core status checkpoint.Ancestors
- typing.Protocol
- typing.Generic
Methods
async def read_reconciliation_changes(self,
*,
caller: ReportingDeliveryPrincipal,
changes_after: ReportingReconciliationCheckpoint | None = None,
cursor: ReportingReconciliationCursor | None = None,
limit: int = 100,
filters: ReportingReconciliationFilter = ReportingReconciliationFilter(record_kinds=(), reporting_obligation_id=None)) ‑> ReportingReconciliationPage-
Expand source code
async def read_reconciliation_changes( self, *, caller: ReportingDeliveryPrincipal, changes_after: ReportingReconciliationCheckpoint | None = None, cursor: ReportingReconciliationCursor | None = None, limit: int = 100, filters: ReportingReconciliationFilter = ReportingReconciliationFilter(), ) -> ReportingReconciliationPage: ...
class ReportingReconciliationFilter (record_kinds: tuple[ReportingReconciliationRecordKind, ...] = (),
reporting_obligation_id: str | None = None)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationFilter: record_kinds: tuple[ReportingReconciliationRecordKind, ...] = () reporting_obligation_id: str | None = None def __post_init__(self) -> None: if type(self.record_kinds) is not tuple or any( not _is_record_kind(kind) for kind in self.record_kinds ): raise ValueError("invalid reconciliation record filter") object.__setattr__(self, "record_kinds", tuple(sorted(set(self.record_kinds)))) if self.reporting_obligation_id is not None: reporting_identifier(self.reporting_obligation_id) @property def fingerprint(self) -> str: return hashlib.sha256(canonical_json_utf8_v1(asdict(self))).hexdigest() def matches(self, record: ReportingDeliveryRecord) -> bool: return (not self.record_kinds or record.kind in self.record_kinds) and ( self.reporting_obligation_id is None or ( not isinstance(record, ReportingDestinationBinding) and record.scope.reporting_obligation_id == self.reporting_obligation_id ) )ReportingReconciliationFilter(record_kinds: 'tuple[ReportingReconciliationRecordKind, …]' = (), reporting_obligation_id: 'str | None' = None)
Instance variables
prop fingerprint : str-
Expand source code
@property def fingerprint(self) -> str: return hashlib.sha256(canonical_json_utf8_v1(asdict(self))).hexdigest() var record_kinds : tuple[typing.Literal['destination_binding', 'obligation_delivery', 'materialization_attempt', 'materialization', 'materialization_check', 'revision_receipt', 'adjustment_receipt'], ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationFilter: record_kinds: tuple[ReportingReconciliationRecordKind, ...] = () reporting_obligation_id: str | None = None def __post_init__(self) -> None: if type(self.record_kinds) is not tuple or any( not _is_record_kind(kind) for kind in self.record_kinds ): raise ValueError("invalid reconciliation record filter") object.__setattr__(self, "record_kinds", tuple(sorted(set(self.record_kinds)))) if self.reporting_obligation_id is not None: reporting_identifier(self.reporting_obligation_id) @property def fingerprint(self) -> str: return hashlib.sha256(canonical_json_utf8_v1(asdict(self))).hexdigest() def matches(self, record: ReportingDeliveryRecord) -> bool: return (not self.record_kinds or record.kind in self.record_kinds) and ( self.reporting_obligation_id is None or ( not isinstance(record, ReportingDestinationBinding) and record.scope.reporting_obligation_id == self.reporting_obligation_id ) ) var reporting_obligation_id : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationFilter: record_kinds: tuple[ReportingReconciliationRecordKind, ...] = () reporting_obligation_id: str | None = None def __post_init__(self) -> None: if type(self.record_kinds) is not tuple or any( not _is_record_kind(kind) for kind in self.record_kinds ): raise ValueError("invalid reconciliation record filter") object.__setattr__(self, "record_kinds", tuple(sorted(set(self.record_kinds)))) if self.reporting_obligation_id is not None: reporting_identifier(self.reporting_obligation_id) @property def fingerprint(self) -> str: return hashlib.sha256(canonical_json_utf8_v1(asdict(self))).hexdigest() def matches(self, record: ReportingDeliveryRecord) -> bool: return (not self.record_kinds or record.kind in self.record_kinds) and ( self.reporting_obligation_id is None or ( not isinstance(record, ReportingDestinationBinding) and record.scope.reporting_obligation_id == self.reporting_obligation_id ) )
Methods
def matches(self, record: ReportingDeliveryRecord) ‑> bool-
Expand source code
def matches(self, record: ReportingDeliveryRecord) -> bool: return (not self.record_kinds or record.kind in self.record_kinds) and ( self.reporting_obligation_id is None or ( not isinstance(record, ReportingDestinationBinding) and record.scope.reporting_obligation_id == self.reporting_obligation_id ) )
class ReportingReconciliationPage (caller: ReportingDeliveryPrincipal,
boundary: ReportingReconciliationSnapshotToken,
changes: tuple[ReportingReconciliationChange, ...],
has_more: bool,
cursor: ReportingReconciliationCursor | None,
changes_checkpoint: ReportingReconciliationCheckpoint | None)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_countReportingReconciliationPage(caller: 'ReportingDeliveryPrincipal', boundary: 'ReportingReconciliationSnapshotToken', changes: 'tuple[ReportingReconciliationChange, …]', has_more: 'bool', cursor: 'ReportingReconciliationCursor | None', changes_checkpoint: 'ReportingReconciliationCheckpoint | None')
Instance variables
var boundary : ReportingReconciliationSnapshotToken-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count var caller : ReportingDeliveryPrincipal-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count var changes : tuple[ReportingReconciliationChange, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count var changes_checkpoint : adcp.reporting.ledger.delivery_changes.ReportingReconciliationCheckpoint | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count var cursor : adcp.reporting.ledger.delivery_changes.ReportingReconciliationCursor | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count var has_more : bool-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationPage: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken changes: tuple[ReportingReconciliationChange, ...] has_more: bool cursor: ReportingReconciliationCursor | None changes_checkpoint: ReportingReconciliationCheckpoint | None @property def total_count(self) -> int: return self.boundary.total_count prop total_count : int-
Expand source code
@property def total_count(self) -> int: return self.boundary.total_count
class ReportingReconciliationSnapshot (caller: ReportingDeliveryPrincipal,
boundary: ReportingReconciliationSnapshotToken,
records: tuple[ReportingDeliveryRecord, ...])-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshot: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken records: tuple[ReportingDeliveryRecord, ...] def materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: if key.principal != self.caller: return None attempt = next( ( item for item in self.records if isinstance(item, ReportingMaterializationAttempt) and item.key == key ), None, ) if attempt is None: return None binding = next( ( item for item in self.records if isinstance(item, ReportingDestinationBinding) and item.generation_key == attempt.scope.generation_key ), None, ) if binding is None: unavailable() outcome = next( ( item for item in self.records if isinstance(item, ReportingMaterializationRecord) and item.key == key ), None, ) return ReportingMaterializationView( attempt, binding, outcome, tuple( item for item in self.records if isinstance(item, ReportingMaterializationCheck) and item.reporting_materialization_id == key.reporting_materialization_id ), ) @property def current_receipts(self) -> tuple[ReportingReceiptRecord, ...]: receipts = tuple( item for item in self.records if isinstance(item, (ReportingRevisionReceiptRecord, ReportingAdjustmentReceiptRecord)) ) return tuple(item for item in receipts if current_receipt(self.records, item) == item) @property def terminal_acceptances(self) -> tuple[ReportingReceiptKey, ...]: return tuple(item.key for item in self.current_receipts if item.status == "accepted")ReportingReconciliationSnapshot(caller: 'ReportingDeliveryPrincipal', boundary: 'ReportingReconciliationSnapshotToken', records: 'tuple[ReportingDeliveryRecord, …]')
Instance variables
var boundary : ReportingReconciliationSnapshotToken-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshot: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken records: tuple[ReportingDeliveryRecord, ...] def materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: if key.principal != self.caller: return None attempt = next( ( item for item in self.records if isinstance(item, ReportingMaterializationAttempt) and item.key == key ), None, ) if attempt is None: return None binding = next( ( item for item in self.records if isinstance(item, ReportingDestinationBinding) and item.generation_key == attempt.scope.generation_key ), None, ) if binding is None: unavailable() outcome = next( ( item for item in self.records if isinstance(item, ReportingMaterializationRecord) and item.key == key ), None, ) return ReportingMaterializationView( attempt, binding, outcome, tuple( item for item in self.records if isinstance(item, ReportingMaterializationCheck) and item.reporting_materialization_id == key.reporting_materialization_id ), ) @property def current_receipts(self) -> tuple[ReportingReceiptRecord, ...]: receipts = tuple( item for item in self.records if isinstance(item, (ReportingRevisionReceiptRecord, ReportingAdjustmentReceiptRecord)) ) return tuple(item for item in receipts if current_receipt(self.records, item) == item) @property def terminal_acceptances(self) -> tuple[ReportingReceiptKey, ...]: return tuple(item.key for item in self.current_receipts if item.status == "accepted") var caller : ReportingDeliveryPrincipal-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshot: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken records: tuple[ReportingDeliveryRecord, ...] def materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: if key.principal != self.caller: return None attempt = next( ( item for item in self.records if isinstance(item, ReportingMaterializationAttempt) and item.key == key ), None, ) if attempt is None: return None binding = next( ( item for item in self.records if isinstance(item, ReportingDestinationBinding) and item.generation_key == attempt.scope.generation_key ), None, ) if binding is None: unavailable() outcome = next( ( item for item in self.records if isinstance(item, ReportingMaterializationRecord) and item.key == key ), None, ) return ReportingMaterializationView( attempt, binding, outcome, tuple( item for item in self.records if isinstance(item, ReportingMaterializationCheck) and item.reporting_materialization_id == key.reporting_materialization_id ), ) @property def current_receipts(self) -> tuple[ReportingReceiptRecord, ...]: receipts = tuple( item for item in self.records if isinstance(item, (ReportingRevisionReceiptRecord, ReportingAdjustmentReceiptRecord)) ) return tuple(item for item in receipts if current_receipt(self.records, item) == item) @property def terminal_acceptances(self) -> tuple[ReportingReceiptKey, ...]: return tuple(item.key for item in self.current_receipts if item.status == "accepted") prop current_receipts : tuple[ReportingReceiptRecord, ...]-
Expand source code
@property def current_receipts(self) -> tuple[ReportingReceiptRecord, ...]: receipts = tuple( item for item in self.records if isinstance(item, (ReportingRevisionReceiptRecord, ReportingAdjustmentReceiptRecord)) ) return tuple(item for item in receipts if current_receipt(self.records, item) == item) var records : tuple[ReportingDestinationBinding | ReportingObligationDeliveryRecord | ReportingMaterializationAttempt | ReportingMaterializationRecord | ReportingMaterializationCheck | ReportingRevisionReceiptRecord | ReportingAdjustmentReceiptRecord, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshot: caller: ReportingDeliveryPrincipal boundary: ReportingReconciliationSnapshotToken records: tuple[ReportingDeliveryRecord, ...] def materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: if key.principal != self.caller: return None attempt = next( ( item for item in self.records if isinstance(item, ReportingMaterializationAttempt) and item.key == key ), None, ) if attempt is None: return None binding = next( ( item for item in self.records if isinstance(item, ReportingDestinationBinding) and item.generation_key == attempt.scope.generation_key ), None, ) if binding is None: unavailable() outcome = next( ( item for item in self.records if isinstance(item, ReportingMaterializationRecord) and item.key == key ), None, ) return ReportingMaterializationView( attempt, binding, outcome, tuple( item for item in self.records if isinstance(item, ReportingMaterializationCheck) and item.reporting_materialization_id == key.reporting_materialization_id ), ) @property def current_receipts(self) -> tuple[ReportingReceiptRecord, ...]: receipts = tuple( item for item in self.records if isinstance(item, (ReportingRevisionReceiptRecord, ReportingAdjustmentReceiptRecord)) ) return tuple(item for item in receipts if current_receipt(self.records, item) == item) @property def terminal_acceptances(self) -> tuple[ReportingReceiptKey, ...]: return tuple(item.key for item in self.current_receipts if item.status == "accepted") prop terminal_acceptances : tuple[ReportingReceiptKey, ...]-
Expand source code
@property def terminal_acceptances(self) -> tuple[ReportingReceiptKey, ...]: return tuple(item.key for item in self.current_receipts if item.status == "accepted")
Methods
def materialization(self,
key: ReportingMaterializationKey) ‑> ReportingMaterializationView | None-
Expand source code
def materialization( self, key: ReportingMaterializationKey ) -> ReportingMaterializationView | None: if key.principal != self.caller: return None attempt = next( ( item for item in self.records if isinstance(item, ReportingMaterializationAttempt) and item.key == key ), None, ) if attempt is None: return None binding = next( ( item for item in self.records if isinstance(item, ReportingDestinationBinding) and item.generation_key == attempt.scope.generation_key ), None, ) if binding is None: unavailable() outcome = next( ( item for item in self.records if isinstance(item, ReportingMaterializationRecord) and item.key == key ), None, ) return ReportingMaterializationView( attempt, binding, outcome, tuple( item for item in self.records if isinstance(item, ReportingMaterializationCheck) and item.reporting_materialization_id == key.reporting_materialization_id ), )
class ReportingReconciliationSnapshotToken (caller: ReportingDeliveryPrincipal,
snapshot_id: str,
ledger_as_of: datetime,
min_sequence: int,
max_sequence: int,
total_count: int,
filters: ReportingReconciliationFilter)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_idA frozen boundary in one principal's feed, never a Core ledger sequence.
Instance variables
prop account_id : str-
Expand source code
@property def account_id(self) -> str: return self.caller.account_id var caller : ReportingDeliveryPrincipal-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id prop consumer_id : str-
Expand source code
@property def consumer_id(self) -> str: return self.caller.consumer_id var filters : ReportingReconciliationFilter-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id var ledger_as_of : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id var max_sequence : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id var min_sequence : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id var snapshot_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id var total_count : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingReconciliationSnapshotToken: """A frozen boundary in one principal's feed, never a Core ledger sequence.""" caller: ReportingDeliveryPrincipal snapshot_id: str ledger_as_of: datetime min_sequence: int max_sequence: int total_count: int filters: ReportingReconciliationFilter @property def account_id(self) -> str: return self.caller.account_id @property def consumer_id(self) -> str: return self.caller.consumer_id
class ReportingReconciliationStore (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingReconciliationStore( ReportingDestinationStore, ReportingMaterializationStore, ReportingReceiptStore, Protocol ): async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: ...Trusted configuration ingestion. Never accept a destination from a receipt body.
Ancestors
- ReportingDestinationStore
- ReportingMaterializationStore
- ReportingReceiptStore
- typing.Protocol
- typing.Generic
Methods
async def read_reconciliation_snapshot(self,
*,
caller: ReportingDeliveryPrincipal,
boundary: ReportingReconciliationSnapshotToken | None = None) ‑> ReportingReconciliationSnapshot-
Expand source code
async def read_reconciliation_snapshot( self, *, caller: ReportingDeliveryPrincipal, boundary: ReportingReconciliationSnapshotToken | None = None, ) -> ReportingReconciliationSnapshot: ...
class ReportingResourceRecord (resource_ref: str,
kind: "Literal['manifest', 'dataset', 'warehouse_relation']",
location: str,
immutability: "Literal['immutable_location', 'native_version']",
expires_at: datetime,
native_version_ref: str | None = None,
manifest_sha256: str | None = None,
object_refs: tuple[str, ...] = (),
reader_compatibility: tuple[str, ...] = ())-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique")ReportingResourceRecord(resource_ref: 'str', kind: "Literal['manifest', 'dataset', 'warehouse_relation']", location: 'str', immutability: "Literal['immutable_location', 'native_version']", expires_at: 'datetime', native_version_ref: 'str | None' = None, manifest_sha256: 'str | None' = None, object_refs: 'tuple[str, …]' = (), reader_compatibility: 'tuple[str, …]' = ())
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var expires_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var immutability : Literal['immutable_location', 'native_version']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var kind : Literal['manifest', 'dataset', 'warehouse_relation']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var location : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var manifest_sha256 : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var native_version_ref : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var object_refs : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var reader_compatibility : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique") var resource_ref : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingResourceRecord(_ClosedValue): resource_ref: str kind: Literal["manifest", "dataset", "warehouse_relation"] location: str immutability: Literal["immutable_location", "native_version"] expires_at: datetime native_version_ref: str | None = None manifest_sha256: str | None = None object_refs: tuple[str, ...] = () reader_compatibility: tuple[str, ...] = () def __post_init__(self) -> None: _freeze_fields(self) reporting_identifier(self.resource_ref, maximum=255) resource_location(self.location) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.manifest_sha256 is not None: sha256_value(self.manifest_sha256) if self.kind == "manifest" and self.manifest_sha256 is None: raise ValueError("manifest resources require the exact manifest digest") if self.immutability == "native_version" and self.native_version_ref is None: raise ValueError("native-version resources require an immutable version") object.__setattr__(self, "expires_at", aware_utc(self.expires_at)) object.__setattr__( self, "object_refs", tuple(file_object_reference(value) for value in self.object_refs), ) object.__setattr__( self, "reader_compatibility", tuple(reader_feature_reference(value) for value in self.reader_compatibility), ) if len(set(self.object_refs)) != len(self.object_refs): raise ValueError("resource object references must be unique") if len(set(self.reader_compatibility)) != len(self.reader_compatibility): raise ValueError("reader compatibility requirements must be unique")
class ReportingRevisionCorrupt (reason: "Literal['ownership_mismatch', 'duplicate_revision_id', 'invalid_revision_identity', 'invalid_finality', 'missing_predecessor', 'cross_finality_edge', 'official_predecessor', 'forked_snapshot_history', 'disconnected_snapshot_history', 'revision_cycle', 'multiple_officials']")-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionCorrupt: # Closed diagnostics: no row data, credentials or provider errors. reason: Literal[ "ownership_mismatch", "duplicate_revision_id", "invalid_revision_identity", "invalid_finality", "missing_predecessor", "cross_finality_edge", "official_predecessor", "forked_snapshot_history", "disconnected_snapshot_history", "revision_cycle", "multiple_officials", ] kind: Literal["corrupt"] = field(default="corrupt", init=False)ReportingRevisionCorrupt(reason: "Literal['ownership_mismatch', 'duplicate_revision_id', 'invalid_revision_identity', 'invalid_finality', 'missing_predecessor', 'cross_finality_edge', 'official_predecessor', 'forked_snapshot_history', 'disconnected_snapshot_history', 'revision_cycle', 'multiple_officials']")
Instance variables
var kind : Literal['corrupt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionCorrupt: # Closed diagnostics: no row data, credentials or provider errors. reason: Literal[ "ownership_mismatch", "duplicate_revision_id", "invalid_revision_identity", "invalid_finality", "missing_predecessor", "cross_finality_edge", "official_predecessor", "forked_snapshot_history", "disconnected_snapshot_history", "revision_cycle", "multiple_officials", ] kind: Literal["corrupt"] = field(default="corrupt", init=False) var reason : Literal['ownership_mismatch', 'duplicate_revision_id', 'invalid_revision_identity', 'invalid_finality', 'missing_predecessor', 'cross_finality_edge', 'official_predecessor', 'forked_snapshot_history', 'disconnected_snapshot_history', 'revision_cycle', 'multiple_officials']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionCorrupt: # Closed diagnostics: no row data, credentials or provider errors. reason: Literal[ "ownership_mismatch", "duplicate_revision_id", "invalid_revision_identity", "invalid_finality", "missing_predecessor", "cross_finality_edge", "official_predecessor", "forked_snapshot_history", "disconnected_snapshot_history", "revision_cycle", "multiple_officials", ] kind: Literal["corrupt"] = field(default="corrupt", init=False)
class ReportingRevisionNotReady (reason: "Literal['empty_history', 'official_required']")-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionNotReady: reason: Literal["empty_history", "official_required"] kind: Literal["not_ready"] = field(default="not_ready", init=False)ReportingRevisionNotReady(reason: "Literal['empty_history', 'official_required']")
Instance variables
var kind : Literal['not_ready']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionNotReady: reason: Literal["empty_history", "official_required"] kind: Literal["not_ready"] = field(default="not_ready", init=False) var reason : Literal['empty_history', 'official_required']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionNotReady: reason: Literal["empty_history", "official_required"] kind: Literal["not_ready"] = field(default="not_ready", init=False)
class ReportingRevisionReceiptRecord (scope: ReportingDeliveryScope,
reporting_receipt_id: str,
reporting_revision_id: str,
reporting_materialization_id: str,
status: ReceiptStatus,
verification_profile: VerificationProfile,
observed_row_count: int,
observed_control_totals: tuple[ReportingControlTotalRecord, ...],
observed_at: datetime,
supersedes_reporting_receipt_id: str | None = None,
observed_canonical_content_digest: ReportingCanonicalDigest | None = None,
observed_manifest_sha256: str | None = None,
observed_native_version_ref: str | None = None,
consumer_commit_ref: str | None = None,
rejection_codes: tuple[str, ...] = (),
received_at: datetime | None = None,
*,
kind: "Literal['revision_receipt']" = 'revision_receipt')-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id)ReportingRevisionReceiptRecord(scope: 'ReportingDeliveryScope', reporting_receipt_id: 'str', reporting_revision_id: 'str', reporting_materialization_id: 'str', status: 'ReceiptStatus', verification_profile: 'VerificationProfile', observed_row_count: 'int', observed_control_totals: 'tuple[ReportingControlTotalRecord, …]', observed_at: 'datetime', supersedes_reporting_receipt_id: 'str | None' = None, observed_canonical_content_digest: 'ReportingCanonicalDigest | None' = None, observed_manifest_sha256: 'str | None' = None, observed_native_version_ref: 'str | None' = None, consumer_commit_ref: 'str | None' = None, rejection_codes: 'tuple[str, …]' = (), received_at: 'datetime | None' = None, *, kind: "Literal['revision_receipt']" = 'revision_receipt')
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var consumer_commit_ref : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) prop key : ReportingReceiptKey-
Expand source code
@property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var kind : Literal['revision_receipt']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_canonical_content_digest : ReportingCanonicalDigest | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_control_totals : tuple[ReportingControlTotalRecord, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_manifest_sha256 : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_native_version_ref : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var observed_row_count : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var received_at : datetime.datetime | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var rejection_codes : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var reporting_materialization_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var reporting_receipt_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var reporting_revision_id : str-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var scope : ReportingDeliveryScope-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var status : Literal['accepted', 'rejected']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var supersedes_reporting_receipt_id : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id) var verification_profile : Literal['canonical_digest', 'manifest_checksums', 'native_commit']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionReceiptRecord(_ClosedValue): scope: ReportingDeliveryScope reporting_receipt_id: str reporting_revision_id: str reporting_materialization_id: str status: ReceiptStatus verification_profile: VerificationProfile observed_row_count: int observed_control_totals: tuple[ReportingControlTotalRecord, ...] observed_at: datetime supersedes_reporting_receipt_id: str | None = None observed_canonical_content_digest: ReportingCanonicalDigest | None = None observed_manifest_sha256: str | None = None observed_native_version_ref: str | None = None consumer_commit_ref: str | None = None rejection_codes: tuple[str, ...] = () received_at: datetime | None = None kind: Literal["revision_receipt"] = field(default="revision_receipt", kw_only=True) def __post_init__(self) -> None: _freeze_fields(self) self.key reporting_identifier(self.reporting_revision_id, maximum=255) reporting_identifier(self.reporting_materialization_id, maximum=255) _positive(self.observed_row_count, zero=True) _unique_totals(self.observed_control_totals) if self.observed_manifest_sha256 is not None: sha256_value(self.observed_manifest_sha256) if self.observed_native_version_ref is not None: native_version_reference(self.observed_native_version_ref) if self.consumer_commit_ref is not None: consumer_commit_reference(self.consumer_commit_ref) _receipt_fields(self) @property def key(self) -> ReportingReceiptKey: return ReportingReceiptKey(self.scope.principal, self.reporting_receipt_id)
class ReportingRevisionRecord (reporting_revision_id: str,
account_id: str,
reporting_obligation_id: str,
finality: ReportingFinality,
revision_content_sha256: str,
row_count: int,
control_totals: tuple[tuple[str, str], ...],
observed_at: datetime,
data_through: datetime | None,
created_at: datetime,
supersedes_reporting_revision_id: str | None = None,
finality_basis: "Literal['source_final', 'contractual_cutoff', 'stabilized'] | None" = None,
finality_policy_id: str | None = None,
finalized_at: datetime | None = None,
readable: bool = True,
readable_at_commit: bool = True,
source_publication_id: str | None = None,
source_manifest_sha256: str | None = None,
canonical_content_digest: ReportingCanonicalDigest | None = None,
managed_control_totals: tuple[ReportingControlTotalRecord, ...] | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingRevisionRecord: """One immutable publication of a period's content. ``revision_content_sha256`` is the Core binding: JCS over ``{reporting_revision_id, row_count, control_totals, reporting_rows}``, SHA-256 of those bytes. A consumer recomputes it from what it actually read, which is what makes an exact read verifiable rather than trusted. ``readable`` is not cosmetic. Core's promise is that a committed revision stays readable for ``status_retention_days``; an obligation whose only qualifying revision has become unreadable is ``action_required``, not ``complete``. """ reporting_revision_id: str account_id: str reporting_obligation_id: str finality: ReportingFinality revision_content_sha256: str row_count: int control_totals: tuple[tuple[str, str], ...] observed_at: datetime data_through: datetime | None created_at: datetime supersedes_reporting_revision_id: str | None = None finality_basis: Literal["source_final", "contractual_cutoff", "stabilized"] | None = None finality_policy_id: str | None = None finalized_at: datetime | None = None readable: bool = True readable_at_commit: bool = True source_publication_id: str | None = None source_manifest_sha256: str | None = None # Optional managed evidence supplied by a trusted publisher before any # destination work. Core neither computes nor requires this contract. canonical_content_digest: ReportingCanonicalDigest | None = None managed_control_totals: tuple[ReportingControlTotalRecord, ...] | None = None def __post_init__(self) -> None: if ( self.canonical_content_digest is not None and type(self.canonical_content_digest) is not ReportingCanonicalDigest ): raise ValueError("managed revision evidence requires an immutable canonical digest") object.__setattr__( self, "control_totals", tuple(tuple(item) for item in self.control_totals) ) if self.managed_control_totals is not None: object.__setattr__( self, "managed_control_totals", freeze_control_totals(self.managed_control_totals, self.control_totals), ) if self.finality == "official": if not (self.finality_basis and self.finality_policy_id and self.finalized_at): raise ValueError( "an official revision needs finality basis, policy, and finalized_at; " "without them a consumer cannot tell contractual close from a guess" ) elif self.finality_basis or self.finality_policy_id or self.finalized_at: raise ValueError("finality evidence belongs only to an official revision") if self.finality == "official" and self.supersedes_reporting_revision_id: raise ValueError( "an official revision is terminal and cannot supersede another revision; " "publish a later source correction as an adjustment" )One immutable publication of a period's content.
revision_content_sha256()is the Core binding: JCS over{reporting_revision_id, row_count, control_totals, reporting_rows}, SHA-256 of those bytes. A consumer recomputes it from what it actually read, which is what makes an exact read verifiable rather than trusted.readableis not cosmetic. Core's promise is that a committed revision stays readable forstatus_retention_days; an obligation whose only qualifying revision has become unreadable isaction_required, notcomplete.Instance variables
var account_id : strvar canonical_content_digest : ReportingCanonicalDigest | Nonevar control_totals : tuple[tuple[str, str], ...]var created_at : datetime.datetimevar data_through : datetime.datetime | Nonevar finality : Literal['snapshot', 'official']var finality_basis : Literal['source_final', 'contractual_cutoff', 'stabilized'] | Nonevar finality_policy_id : str | Nonevar finalized_at : datetime.datetime | Nonevar managed_control_totals : tuple[ReportingControlTotalRecord, ...] | Nonevar observed_at : datetime.datetimevar readable : boolvar readable_at_commit : boolvar reporting_obligation_id : strvar reporting_revision_id : strvar revision_content_sha256 : strvar row_count : intvar source_manifest_sha256 : str | Nonevar source_publication_id : str | Nonevar supersedes_reporting_revision_id : str | None
class ReportingRevisionSelected (revision: R)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionSelected(Generic[R]): revision: R kind: Literal["selected"] = field(default="selected", init=False)ReportingRevisionSelected(revision: 'R')
Ancestors
- typing.Generic
Instance variables
var kind : Literal['selected']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionSelected(Generic[R]): revision: R kind: Literal["selected"] = field(default="selected", init=False) var revision : ~R-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingRevisionSelected(Generic[R]): revision: R kind: Literal["selected"] = field(default="selected", init=False)
class ReportingRowPage (rows: tuple[dict[str, Any], ...],
total_count: int,
has_more: bool,
cursor: str | None,
reporting_revision_id: str | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingRowPage: """One page of an exact revision read. Every page repeats identical revision metadata and binding; a consumer hashes the concatenated rows in cursor order only after exhausting the frozen walk. Do not substitute a fresh date-range pull for this read -- it may have changed since the immutable revision was published. """ rows: tuple[dict[str, Any], ...] total_count: int has_more: bool cursor: str | None reporting_revision_id: str | None = NoneOne page of an exact revision read.
Every page repeats identical revision metadata and binding; a consumer hashes the concatenated rows in cursor order only after exhausting the frozen walk. Do not substitute a fresh date-range pull for this read – it may have changed since the immutable revision was published.
Instance variables
var cursor : str | Nonevar has_more : boolvar reporting_revision_id : str | Nonevar rows : tuple[dict[str, typing.Any], ...]var total_count : int
class ReportingScheduleSpec (period_duration: str,
delivery_sla: str,
alignment: "Literal['utc', 'account_timezone', 'custom_timezone']" = 'utc',
period_timezone: str | None = None,
period_anchor: datetime | None = None)-
Expand source code
@dataclass(frozen=True) class ReportingScheduleSpec: """The clock a reporting configuration creates. ``expected_at`` is always ``period.end + delivery_sla``. Accepting a media buy does not start a separate SLA; the configuration is the only clock. """ period_duration: str delivery_sla: str alignment: Literal["utc", "account_timezone", "custom_timezone"] = "utc" period_timezone: str | None = None period_anchor: datetime | None = None def timezone_name(self, account_timezone: str) -> str: if self.alignment == "utc": return "UTC" if self.alignment == "account_timezone": return account_timezone if not self.period_timezone: raise ValueError("custom_timezone alignment requires period_timezone") return self.period_timezoneThe clock a reporting configuration creates.
expected_atis alwaysperiod.end + delivery_sla. Accepting a media buy does not start a separate SLA; the configuration is the only clock.Instance variables
var alignment : Literal['utc', 'account_timezone', 'custom_timezone']var delivery_sla : strvar period_anchor : datetime.datetime | Nonevar period_duration : strvar period_timezone : str | None
Methods
def timezone_name(self, account_timezone: str) ‑> str-
Expand source code
def timezone_name(self, account_timezone: str) -> str: if self.alignment == "utc": return "UTC" if self.alignment == "account_timezone": return account_timezone if not self.period_timezone: raise ValueError("custom_timezone alignment requires period_timezone") return self.period_timezone
class ReportingStatusCaller (account_id: str, consumer_id: str)-
Expand source code
@dataclass(frozen=True) class ReportingStatusCaller: """The authenticated caller, resolved from transport, never from the body. ``consumer_id`` is what scopes consumer-status visibility. A request body that asserts a buyer principal is ignored: identity comes from the authenticated transport or it does not exist. """ account_id: str consumer_id: str def __post_init__(self) -> None: from adcp.reporting.evidence import consumer_reference, principal_reference principal_reference(self.account_id) consumer_reference(self.consumer_id)The authenticated caller, resolved from transport, never from the body.
consumer_idis what scopes consumer-status visibility. A request body that asserts a buyer principal is ignored: identity comes from the authenticated transport or it does not exist.Instance variables
var account_id : strvar consumer_id : str
class ReportingStatusHandler (store: ReportingLedgerStore,
*,
page_size: int = 100,
consumer_status_enabled: bool = False,
escalation: ReportingDeliveryEscalation | None = None)-
Expand source code
class ReportingStatusHandler: """Render one captured status snapshot using the same pure projection as push.""" def __init__( self, store: ReportingLedgerStore, *, page_size: int = _DEFAULT_PAGE_SIZE, consumer_status_enabled: bool = False, escalation: ReportingDeliveryEscalation | None = None, ) -> None: self._store = store self._page_size = page_size self._consumer_status_enabled = consumer_status_enabled self._escalation = escalation async def handle( self, request: dict[str, Any], *, caller: ReportingStatusCaller ) -> dict[str, Any]: snapshot = await self._load(caller) return self.render_snapshot(request, caller=caller, snapshot=snapshot) async def _load(self, caller: ReportingStatusCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.status_snapshot import ReportingStatusParticipant if isinstance(self._store, ReportingStatusParticipant): return await self._store.read_status_snapshot(caller=caller) # Optional upgrade: custom stores keep their original structural API. # This fallback cannot establish durable status-notification readiness. boundary = await self._store.open_snapshot( caller=caller, filters_fingerprint="status-projection-v1" ) configurations = await self._store.list_configurations(caller=caller) obligations: list[ReportingObligationRecord] = [] revisions: list[ReportingRevisionRecord] = [] adjustments: list[ReportingAdjustmentRecord] = [] statuses: list[ConsumerStatusRecord] = [] offset = 0 changes: list[tuple[int, str, str, str]] = [] while True: page = await self._store.read_page( snapshot=boundary, consumer_id=caller.consumer_id, delivery_config_ids=None, media_buy_ids=None, offset=offset, limit=self._page_size, changes_after_sequence=None, ) obligations.extend(page.obligations) revisions.extend(page.revisions) adjustments.extend(page.adjustments) statuses.extend(page.consumer_statuses) if not page.has_more: break offset += self._page_size lifecycles = [] for key in sorted({mismatch_key(s) for s in statuses}): seen: set[str] = set() while ( issue := await self._store.get_issue(account_id=caller.account_id, issue_key=key) ) is not None: if issue.issue_id in seen or issue.issue_key != key: raise LedgerConflictError( "STATUS_PROJECTION_UNAVAILABLE", "invalid waiver chain" ) seen.add(issue.issue_id) lifecycles.append(issue) if issue.issue_state != "waived": break key = condition_after_waiver(issue) for kind, records, attribute in ( ("obligation", obligations, "reporting_obligation_id"), ("revision", revisions, "reporting_revision_id"), ("adjustment", adjustments, "reporting_adjustment_id"), ("consumer_status", statuses, "reporting_status_id"), ): # The legacy page API has a durable boundary but no per-record # ordinals. Replay its retained records when that boundary advances; # incremental_repair permits identity-deduplicated over-inclusion. # Positional ordinals would instead omit later records after rebuilds. changes.extend( (boundary.max_sequence, kind, getattr(record, attribute), caller.consumer_id) for record in records ) snapshot = ReportingStatusSnapshot( caller.account_id, boundary.ledger_as_of, tuple(configurations), tuple(obligations), tuple(revisions), tuple(statuses), tuple(lifecycles), adjustments=tuple(adjustments), changes=tuple(changes), ) for _ in range(3): intents = lifecycle_intents(snapshot) if not intents: return snapshot for intent in intents: issue = intent.lifecycle if intent.action == "ensure_mismatch": await self._store.ensure_issue_opened( issue_key=issue.issue_key, account_id=issue.account_id, consumer_id=issue.consumer_id, observed_at=issue.opened_at, ) elif intent.action == "retire_mismatch": await self._store.retire_issue( issue_key=issue.issue_key, account_id=issue.account_id, at=snapshot.as_of ) snapshot = apply_intents_to_snapshot(snapshot, intents) raise LedgerConflictError( "STATUS_PROJECTION_UNAVAILABLE", "status lifecycle did not converge" ) def render_snapshot( self, request: dict[str, Any], *, caller: ReportingStatusCaller, snapshot: ReportingStatusSnapshot, reconciliation: tuple[ReportingDeliveryRecord, ...] | None = None, revision_ownership: bool = False, ) -> dict[str, Any]: """Render captured database evidence without any store calls or clock reads.""" view = request.get("view", "summary") if view not in {"summary", "periods", "revision"}: raise LedgerConflictError("INVALID_VIEW", "unsupported reporting status view") if snapshot.account_id != caller.account_id: raise LedgerConflictError("LOOKUP_UNAVAILABLE", "status is unavailable to this caller") filters = _filters(request) version = normalize_to_release_precision( request.get("adcp_version") or resolve_adcp_version(None) ) complete_start_forecast = is_adcp_version_at_least(version, "3.2-rc.6") if complete_start_forecast: # Bind the new wire contract without relabelling explicit rc.3 snapshots. filters["adcp_version"] = version scope = ReportingStatusScope( caller.account_id, consumer_id=caller.consumer_id, ) from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.materializer.capture import private_snapshot snapshot = private_snapshot( snapshot, ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id) ) snapshot_id = ( "rpls_" + _fingerprint( [ caller.account_id, caller.consumer_id, filters, snapshot.max_sequence, ] )[:32] ) if reconciliation is not None: from adcp.reporting.ledger._delivery_state import fingerprint, principal reconciliation = tuple( r for r in reconciliation if principal(r).account_id == caller.account_id and principal(r).consumer_id == caller.consumer_id ) snapshot_id = ( "rpls_" + _fingerprint( [snapshot_id, [fingerprint(r) for r in reconciliation], _iso(snapshot.as_of)] )[:32] ) offset = 0 lower = _checkpoint_sequence(request.get("changes_after"), caller=caller) or 0 cursor = (request.get("pagination") or {}).get("cursor") if cursor: decoded = decode_cursor(cursor) _require_owned_cursor(decoded, caller=caller) if decoded.get("snapshot") != snapshot_id: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor belongs to a different snapshot, caller or filter set;" " restart the walk", ) cursor_lower = decoded.get("lower") if not isinstance(cursor_lower, int): raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor does not retain its incremental lower bound; restart the walk", ) if "changes_after" in request and lower != cursor_lower: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor belongs to a different incremental lower bound; restart the walk", ) lower = cursor_lower offset = int(decoded.get("offset", 0)) if "as_of" in decoded: snapshot = replace(snapshot, as_of=datetime.fromisoformat(decoded["as_of"])) value = StatusProjectionInput( snapshot, scope, self._escalation, tuple(filters["delivery_config_ids"] or ()), tuple(filters["media_buy_ids"] or ()), tuple(filters["feed_purposes"] or ()), _parse(filters["period_start"]), _parse(filters["period_end"]), reconciliation=reconciliation, consumer_status_enabled=self._consumer_status_enabled, ) result = project_status_scope(value) if result.intents: raise LedgerConflictError( "STATUS_PROJECTION_UNAVAILABLE", "status lifecycle is pending" ) common: dict[str, Any] = { "status": "completed", "view": view, "ledger_snapshot_id": snapshot_id, "ledger_as_of": _iso(snapshot.as_of), "account_id": caller.account_id, } if view == "revision": revision_id = request.get("reporting_revision_id") if not revision_id: raise LedgerConflictError( "MISSING_REVISION_ID", "a revision view requires reporting_revision_id" ) revision = next( (r for r in snapshot.revisions if r.reporting_revision_id == revision_id), None ) if revision is None: raise LedgerConflictError( "LOOKUP_UNAVAILABLE", "no such revision is available to this caller" ) owner = next( ( o for o in snapshot.obligations if o.reporting_obligation_id == revision.reporting_obligation_id ), None, ) response = { **common, "revision": _revision_to_wire(revision, owner), "adjustments": [ _adjustment_to_wire(a) for a in snapshot.adjustments if a.adjusts_reporting_revision_id == revision_id ], "materializations": [], "receipts": [], "pagination": {"total_count": 1, "has_more": False}, } if reconciliation is not None: from adcp.reporting.projection.wire import exact_revision_evidence response.update( exact_revision_evidence(snapshot, revision, owner, reconciliation, caller) ) if revision_ownership: from adcp.reporting.ownership import with_revision_ownership response = with_revision_ownership( response, {revision.reporting_revision_id: revision.reporting_obligation_id} ) return response common["scope"] = _scope_to_wire( result.configurations, ledger_as_of=snapshot.as_of, request=request, obligations=tuple(p.obligation for p in result.obligations), ) obligations = tuple(p.obligation for p in result.obligations) if view == "summary": states: list[str] = [p.projection.health for p in result.obligations] counts = { "total": len(obligations), **{ state: states.count(state) for state in ("waiting", "healthy", "delayed", "action_required", "complete") }, } if self._consumer_status_enabled: counts["consumer_status_pending"] = result.pending_count watermark = _scope_data_through(p.projection for p in result.obligations) configurations = tuple( c for c in result.configurations if (not request.get("finality") or c.required_finality in request["finality"]) and ( not request.get("health") or project_status_scope( replace( value, scope=ReportingStatusScope( c.account_id, c.generation_key, consumer_id=scope.consumer_id ), ) ).health in request["health"] ) ) selected_obligations = tuple( p.obligation for p in result.obligations if ( not request.get("finality") or p.obligation.required_finality in request["finality"] ) and (not request.get("health") or p.projection.health in request["health"]) ) next_expected = ( next_reporting_period_start(configurations, as_of=snapshot.as_of) if complete_start_forecast and result.health == "complete" else next_reporting_expectation( configurations, selected_obligations, as_of=snapshot.as_of, period_start=value.period_start, period_end=value.period_end, ) ) return { **common, "health": result.health, "coverage": _coverage_roll_up(obligations, as_of=snapshot.as_of), "data_through": _iso(watermark) if watermark else None, **({"next_expected_at": _iso(next_expected)} if next_expected is not None else {}), "obligation_counts": counts, "issues": [i.to_wire() for i in result.issues], } owners = {o.reporting_obligation_id: o for o in obligations} records: dict[tuple[str, str], Any] = { ("obligation", o.reporting_obligation_id): o for o in obligations } for r in snapshot.revisions: if r.reporting_obligation_id in owners: records[("revision", r.reporting_revision_id)] = r for a in snapshot.adjustments: if ("revision", a.adjusts_reporting_revision_id) in records: records[("adjustment", a.reporting_adjustment_id)] = a generation_keys = {c.generation_key for c in result.configurations} if self._consumer_status_enabled: for s in snapshot.statuses: if s.consumer_id == caller.consumer_id and s.generation_key in generation_keys: if (value.period_start is not None and s.period_end <= value.period_start) or ( value.period_end is not None and s.period_start >= value.period_end ): continue records[("consumer_status", s.reporting_status_id)] = s selected = [ (kind, records[(kind, record_id)]) for seq, kind, record_id, _ in sorted(snapshot.changes) if seq > lower and (kind, record_id) in records ] window = selected[offset : offset + self._page_size] has_more = offset + self._page_size < len(selected) periods = [] for kind, record in window: if kind != "obligation": continue scoped = project_status_scope( replace(value, scope=ReportingStatusScope.for_obligation(record, scope.consumer_id)) ) item = scoped.obligations[0] periods.append( _obligation_to_wire( record, revisions=item.revisions, health=scoped.health, production_status=item.projection.production_status, issues=scoped.issues, statuses=item.statuses, ) ) payload = { **common, "health": result.health, "issues": [issue.to_wire() for issue in result.issues], "changes_checkpoint": _encode_checkpoint(snapshot.max_sequence, caller=caller), "periods": periods, "revisions": [ _revision_to_wire(r, owners.get(r.reporting_obligation_id)) for kind, r in window if kind == "revision" ], "adjustments": [_adjustment_to_wire(a) for kind, a in window if kind == "adjustment"], "materializations": [], "receipts": [], "pagination": { "total_count": len(selected), "has_more": has_more, **( { "cursor": encode_cursor( { "ownership": 2, "account": caller.account_id, "consumer": caller.consumer_id, "snapshot": snapshot_id, "offset": offset + self._page_size, "lower": lower, "as_of": snapshot.as_of.isoformat(), } ) } if has_more else {} ), }, } if self._consumer_status_enabled: payload["consumer_statuses"] = [ _consumer_status_to_wire(s) for kind, s in window if kind == "consumer_status" ] return payloadRender one captured status snapshot using the same pure projection as push.
Methods
async def handle(self,
request: dict[str, Any],
*,
caller: ReportingStatusCaller) ‑> dict[str, typing.Any]-
Expand source code
async def handle( self, request: dict[str, Any], *, caller: ReportingStatusCaller ) -> dict[str, Any]: snapshot = await self._load(caller) return self.render_snapshot(request, caller=caller, snapshot=snapshot) def render_snapshot(self,
request: dict[str, Any],
*,
caller: ReportingStatusCaller,
snapshot: ReportingStatusSnapshot,
reconciliation: tuple[ReportingDeliveryRecord, ...] | None = None,
revision_ownership: bool = False) ‑> dict[str, typing.Any]-
Expand source code
def render_snapshot( self, request: dict[str, Any], *, caller: ReportingStatusCaller, snapshot: ReportingStatusSnapshot, reconciliation: tuple[ReportingDeliveryRecord, ...] | None = None, revision_ownership: bool = False, ) -> dict[str, Any]: """Render captured database evidence without any store calls or clock reads.""" view = request.get("view", "summary") if view not in {"summary", "periods", "revision"}: raise LedgerConflictError("INVALID_VIEW", "unsupported reporting status view") if snapshot.account_id != caller.account_id: raise LedgerConflictError("LOOKUP_UNAVAILABLE", "status is unavailable to this caller") filters = _filters(request) version = normalize_to_release_precision( request.get("adcp_version") or resolve_adcp_version(None) ) complete_start_forecast = is_adcp_version_at_least(version, "3.2-rc.6") if complete_start_forecast: # Bind the new wire contract without relabelling explicit rc.3 snapshots. filters["adcp_version"] = version scope = ReportingStatusScope( caller.account_id, consumer_id=caller.consumer_id, ) from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.materializer.capture import private_snapshot snapshot = private_snapshot( snapshot, ReportingDeliveryPrincipal(caller.account_id, caller.consumer_id) ) snapshot_id = ( "rpls_" + _fingerprint( [ caller.account_id, caller.consumer_id, filters, snapshot.max_sequence, ] )[:32] ) if reconciliation is not None: from adcp.reporting.ledger._delivery_state import fingerprint, principal reconciliation = tuple( r for r in reconciliation if principal(r).account_id == caller.account_id and principal(r).consumer_id == caller.consumer_id ) snapshot_id = ( "rpls_" + _fingerprint( [snapshot_id, [fingerprint(r) for r in reconciliation], _iso(snapshot.as_of)] )[:32] ) offset = 0 lower = _checkpoint_sequence(request.get("changes_after"), caller=caller) or 0 cursor = (request.get("pagination") or {}).get("cursor") if cursor: decoded = decode_cursor(cursor) _require_owned_cursor(decoded, caller=caller) if decoded.get("snapshot") != snapshot_id: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor belongs to a different snapshot, caller or filter set;" " restart the walk", ) cursor_lower = decoded.get("lower") if not isinstance(cursor_lower, int): raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor does not retain its incremental lower bound; restart the walk", ) if "changes_after" in request and lower != cursor_lower: raise LedgerConflictError( "CURSOR_SNAPSHOT_MISMATCH", "this cursor belongs to a different incremental lower bound; restart the walk", ) lower = cursor_lower offset = int(decoded.get("offset", 0)) if "as_of" in decoded: snapshot = replace(snapshot, as_of=datetime.fromisoformat(decoded["as_of"])) value = StatusProjectionInput( snapshot, scope, self._escalation, tuple(filters["delivery_config_ids"] or ()), tuple(filters["media_buy_ids"] or ()), tuple(filters["feed_purposes"] or ()), _parse(filters["period_start"]), _parse(filters["period_end"]), reconciliation=reconciliation, consumer_status_enabled=self._consumer_status_enabled, ) result = project_status_scope(value) if result.intents: raise LedgerConflictError( "STATUS_PROJECTION_UNAVAILABLE", "status lifecycle is pending" ) common: dict[str, Any] = { "status": "completed", "view": view, "ledger_snapshot_id": snapshot_id, "ledger_as_of": _iso(snapshot.as_of), "account_id": caller.account_id, } if view == "revision": revision_id = request.get("reporting_revision_id") if not revision_id: raise LedgerConflictError( "MISSING_REVISION_ID", "a revision view requires reporting_revision_id" ) revision = next( (r for r in snapshot.revisions if r.reporting_revision_id == revision_id), None ) if revision is None: raise LedgerConflictError( "LOOKUP_UNAVAILABLE", "no such revision is available to this caller" ) owner = next( ( o for o in snapshot.obligations if o.reporting_obligation_id == revision.reporting_obligation_id ), None, ) response = { **common, "revision": _revision_to_wire(revision, owner), "adjustments": [ _adjustment_to_wire(a) for a in snapshot.adjustments if a.adjusts_reporting_revision_id == revision_id ], "materializations": [], "receipts": [], "pagination": {"total_count": 1, "has_more": False}, } if reconciliation is not None: from adcp.reporting.projection.wire import exact_revision_evidence response.update( exact_revision_evidence(snapshot, revision, owner, reconciliation, caller) ) if revision_ownership: from adcp.reporting.ownership import with_revision_ownership response = with_revision_ownership( response, {revision.reporting_revision_id: revision.reporting_obligation_id} ) return response common["scope"] = _scope_to_wire( result.configurations, ledger_as_of=snapshot.as_of, request=request, obligations=tuple(p.obligation for p in result.obligations), ) obligations = tuple(p.obligation for p in result.obligations) if view == "summary": states: list[str] = [p.projection.health for p in result.obligations] counts = { "total": len(obligations), **{ state: states.count(state) for state in ("waiting", "healthy", "delayed", "action_required", "complete") }, } if self._consumer_status_enabled: counts["consumer_status_pending"] = result.pending_count watermark = _scope_data_through(p.projection for p in result.obligations) configurations = tuple( c for c in result.configurations if (not request.get("finality") or c.required_finality in request["finality"]) and ( not request.get("health") or project_status_scope( replace( value, scope=ReportingStatusScope( c.account_id, c.generation_key, consumer_id=scope.consumer_id ), ) ).health in request["health"] ) ) selected_obligations = tuple( p.obligation for p in result.obligations if ( not request.get("finality") or p.obligation.required_finality in request["finality"] ) and (not request.get("health") or p.projection.health in request["health"]) ) next_expected = ( next_reporting_period_start(configurations, as_of=snapshot.as_of) if complete_start_forecast and result.health == "complete" else next_reporting_expectation( configurations, selected_obligations, as_of=snapshot.as_of, period_start=value.period_start, period_end=value.period_end, ) ) return { **common, "health": result.health, "coverage": _coverage_roll_up(obligations, as_of=snapshot.as_of), "data_through": _iso(watermark) if watermark else None, **({"next_expected_at": _iso(next_expected)} if next_expected is not None else {}), "obligation_counts": counts, "issues": [i.to_wire() for i in result.issues], } owners = {o.reporting_obligation_id: o for o in obligations} records: dict[tuple[str, str], Any] = { ("obligation", o.reporting_obligation_id): o for o in obligations } for r in snapshot.revisions: if r.reporting_obligation_id in owners: records[("revision", r.reporting_revision_id)] = r for a in snapshot.adjustments: if ("revision", a.adjusts_reporting_revision_id) in records: records[("adjustment", a.reporting_adjustment_id)] = a generation_keys = {c.generation_key for c in result.configurations} if self._consumer_status_enabled: for s in snapshot.statuses: if s.consumer_id == caller.consumer_id and s.generation_key in generation_keys: if (value.period_start is not None and s.period_end <= value.period_start) or ( value.period_end is not None and s.period_start >= value.period_end ): continue records[("consumer_status", s.reporting_status_id)] = s selected = [ (kind, records[(kind, record_id)]) for seq, kind, record_id, _ in sorted(snapshot.changes) if seq > lower and (kind, record_id) in records ] window = selected[offset : offset + self._page_size] has_more = offset + self._page_size < len(selected) periods = [] for kind, record in window: if kind != "obligation": continue scoped = project_status_scope( replace(value, scope=ReportingStatusScope.for_obligation(record, scope.consumer_id)) ) item = scoped.obligations[0] periods.append( _obligation_to_wire( record, revisions=item.revisions, health=scoped.health, production_status=item.projection.production_status, issues=scoped.issues, statuses=item.statuses, ) ) payload = { **common, "health": result.health, "issues": [issue.to_wire() for issue in result.issues], "changes_checkpoint": _encode_checkpoint(snapshot.max_sequence, caller=caller), "periods": periods, "revisions": [ _revision_to_wire(r, owners.get(r.reporting_obligation_id)) for kind, r in window if kind == "revision" ], "adjustments": [_adjustment_to_wire(a) for kind, a in window if kind == "adjustment"], "materializations": [], "receipts": [], "pagination": { "total_count": len(selected), "has_more": has_more, **( { "cursor": encode_cursor( { "ownership": 2, "account": caller.account_id, "consumer": caller.consumer_id, "snapshot": snapshot_id, "offset": offset + self._page_size, "lower": lower, "as_of": snapshot.as_of.isoformat(), } ) } if has_more else {} ), }, } if self._consumer_status_enabled: payload["consumer_statuses"] = [ _consumer_status_to_wire(s) for kind, s in window if kind == "consumer_status" ] return payloadRender captured database evidence without any store calls or clock reads.
class ReportingStatusNotificationHandler (status: ReportingStatusHandler,
*,
resolve_caller: ReportingStatusCallerResolver,
adcp_version: str | None = None)-
Expand source code
class ReportingStatusNotificationHandler(ADCPHandler[ToolContext]): """Compose with adopter task overrides; mounts only the authoritative read. ``resolve_caller`` resolves the requested account using trusted transport authentication and account authorization. Request consumer/principal fields are never identity. Registration and other reporting tasks remain explicit adopter overrides, and do not appear merely because this handler exists. """ def __init__( self, status: ReportingStatusHandler, *, resolve_caller: ReportingStatusCallerResolver, adcp_version: str | None = None, ) -> None: super().__init__() self.reporting_status_handler = status self._resolve_status_caller = resolve_caller self._adcp_version = resolve_adcp_version(adcp_version) if not is_adcp_version_at_least(self._adcp_version, "3.2-rc.6"): raise ConfigurationError( "reporting mounts require a supported AdCP 3.2 reporting contract; " "use 3.2 or omit the pin for the packaged default" ) def get_adcp_version(self) -> str: """Public per-mount protocol pin, shared by schema and rendering.""" return self._adcp_version async def get_reporting_status( self, params: GetReportingStatusRequest | dict[str, Any], context: ToolContext | None = None ) -> dict[str, Any]: request = ( dict(params) if isinstance(params, dict) else params.model_dump(mode="json", exclude_unset=True) ) request["adcp_version"] = ( context.resolved_adcp_version if context is not None and context.resolved_adcp_version is not None else request.get("adcp_version") or self.get_adcp_version() ) caller = await self._resolve_status_caller(request, context) return await self.reporting_status_handler.handle(request, caller=caller)Compose with adopter task overrides; mounts only the authoritative read.
resolve_callerresolves the requested account using trusted transport authentication and account authorization. Request consumer/principal fields are never identity. Registration and other reporting tasks remain explicit adopter overrides, and do not appear merely because this handler exists.Ancestors
- ADCPHandler
- abc.ABC
- typing.Generic
Methods
def get_adcp_version(self) ‑> str-
Expand source code
def get_adcp_version(self) -> str: """Public per-mount protocol pin, shared by schema and rendering.""" return self._adcp_versionPublic per-mount protocol pin, shared by schema and rendering.
Inherited members
ADCPHandler:accept_proposalacquire_rightsactivate_signalbuild_creative_legacybuy_productscalibrate_contentcheck_governancecomply_test_controllercontext_matchcontrol_media_buycreate_collection_listcreate_content_standardscreate_media_buycreate_property_listdecline_proposalsdelete_collection_listdelete_property_listget_account_financialsget_adcp_capabilitiesget_brand_identityget_collection_listget_content_standardsget_creative_deliveryget_creative_featuresget_media_buy_artifactsget_media_buy_deliveryget_media_buysget_plan_audit_logsget_principalget_productsget_property_listget_reporting_statusget_rightsget_signalsget_task_statusidentity_matchlist_account_changeslist_accountslist_collection_listslist_content_standardslist_creative_formats_legacylist_creativeslist_productslist_property_listslist_taskslist_transformerslog_eventpreview_creative_legacyprovide_performance_feedbackrefine_proposalsreport_plan_adjustmentreport_plan_outcomereport_usagerequest_proposalssi_get_offeringsi_initiate_sessionsi_send_messagesi_terminate_sessionsync_accountssync_agent_notification_configssync_audiencessync_catalogssync_creativessync_event_sourcessync_governancesync_planssync_principalsync_reporting_receiptssync_reporting_statusupdate_collection_listupdate_content_standardsupdate_media_buyupdate_property_listupdate_rightsvalidate_content_deliveryvalidate_inputverify_brand_claimverify_brand_claims
class ReportingStatusParticipant (*args, **kwargs)-
Expand source code
@runtime_checkable class ReportingStatusParticipant(Protocol): """Optional upgrade; existing ReportingLedgerStore implementations stay valid.""" async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: ... async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: ...Optional upgrade; existing ReportingLedgerStore implementations stay valid.
Ancestors
- typing.Protocol
- typing.Generic
Methods
async def read_status_snapshot(self,
*,
caller: ReportingCaller) ‑> ReportingStatusSnapshot-
Expand source code
async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: ... async def record_consumer_status_with_lifecycle(self,
status: ConsumerStatusRecord) ‑> tuple[ConsumerStatusRecord, bool]-
Expand source code
async def record_consumer_status_with_lifecycle( self, status: ConsumerStatusRecord ) -> tuple[ConsumerStatusRecord, bool]: ...
class ReportingStatusSnapshot (account_id: str,
as_of: datetime,
configurations: tuple[ReportingConfiguration, ...] = (),
obligations: tuple[ReportingObligationRecord, ...] = (),
revisions: tuple[ReportingRevisionRecord, ...] = (),
statuses: tuple[ConsumerStatusRecord, ...] = (),
lifecycles: tuple[ReportingIssueLifecycle, ...] = (),
issue_scopes: tuple[tuple[str, ReportingStatusScope], ...] = (),
consumer_ids: tuple[str, ...] = (),
adjustments: tuple[ReportingAdjustmentRecord, ...] = (),
changes: tuple[tuple[int, str, str, str], ...] = ())-
Expand source code
@dataclass(frozen=True) class ReportingStatusSnapshot: account_id: str as_of: datetime configurations: tuple[ReportingConfiguration, ...] = () obligations: tuple[ReportingObligationRecord, ...] = () revisions: tuple[ReportingRevisionRecord, ...] = () statuses: tuple[ConsumerStatusRecord, ...] = () lifecycles: tuple[ReportingIssueLifecycle, ...] = () issue_scopes: tuple[tuple[str, ReportingStatusScope], ...] = () consumer_ids: tuple[str, ...] = () adjustments: tuple[ReportingAdjustmentRecord, ...] = () # Sequence, record kind, ID, authenticated generation/status owner. changes: tuple[tuple[int, str, str, str], ...] = () @property def max_sequence(self) -> int: return max((row[0] for row in self.changes), default=0)ReportingStatusSnapshot(account_id: 'str', as_of: 'datetime', configurations: 'tuple[ReportingConfiguration, …]' = (), obligations: 'tuple[ReportingObligationRecord, …]' = (), revisions: 'tuple[ReportingRevisionRecord, …]' = (), statuses: 'tuple[ConsumerStatusRecord, …]' = (), lifecycles: 'tuple[ReportingIssueLifecycle, …]' = (), issue_scopes: 'tuple[tuple[str, ReportingStatusScope], …]' = (), consumer_ids: 'tuple[str, …]' = (), adjustments: 'tuple[ReportingAdjustmentRecord, …]' = (), changes: 'tuple[tuple[int, str, str, str], …]' = ())
Instance variables
var account_id : strvar adjustments : tuple[ReportingAdjustmentRecord, ...]var as_of : datetime.datetimevar changes : tuple[tuple[int, str, str, str], ...]var configurations : tuple[ReportingConfiguration, ...]var consumer_ids : tuple[str, ...]var issue_scopes : tuple[tuple[str, ReportingStatusScope], ...]var lifecycles : tuple[ReportingIssueLifecycle, ...]prop max_sequence : int-
Expand source code
@property def max_sequence(self) -> int: return max((row[0] for row in self.changes), default=0) var obligations : tuple[ReportingObligationRecord, ...]var revisions : tuple[ReportingRevisionRecord, ...]var statuses : tuple[ConsumerStatusRecord, ...]
class ReportingVerificationRecord (verified_at: datetime,
verification_path: VerificationPath,
verification_profile: VerificationProfile,
row_count: int,
control_totals: tuple[ReportingControlTotalRecord, ...],
canonical_content_digest: ReportingCanonicalDigest | None = None,
physical_checksums: tuple[ReportingPhysicalChecksum, ...] = (),
native_version_ref: str | None = None,
native_observed_through: "Literal['representative_consumer', 'destination'] | None" = None,
verified_format: ReportingFormat | None = None)-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence")ReportingVerificationRecord(verified_at: 'datetime', verification_path: 'VerificationPath', verification_profile: 'VerificationProfile', row_count: 'int', control_totals: 'tuple[ReportingControlTotalRecord, …]', canonical_content_digest: 'ReportingCanonicalDigest | None' = None, physical_checksums: 'tuple[ReportingPhysicalChecksum, …]' = (), native_version_ref: 'str | None' = None, native_observed_through: "Literal['representative_consumer', 'destination'] | None" = None, verified_format: 'ReportingFormat | None' = None)
Ancestors
- adcp.reporting.ledger.delivery_models._ClosedValue
Instance variables
var canonical_content_digest : ReportingCanonicalDigest | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var control_totals : tuple[ReportingControlTotalRecord, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var native_observed_through : Literal['representative_consumer', 'destination'] | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var native_version_ref : str | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var physical_checksums : tuple[ReportingPhysicalChecksum, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var row_count : int-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var verification_path : Literal['adcp.reporting.ledger.producer', 'representative_consumer', 'destination']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var verification_profile : Literal['canonical_digest', 'manifest_checksums', 'native_commit']-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var verified_at : datetime.datetime-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence") var verified_format : Literal['jsonl', 'csv', 'parquet', 'avro', 'orc'] | None-
Expand source code
@dataclass(frozen=True, slots=True) class ReportingVerificationRecord(_ClosedValue): verified_at: datetime verification_path: VerificationPath verification_profile: VerificationProfile row_count: int control_totals: tuple[ReportingControlTotalRecord, ...] canonical_content_digest: ReportingCanonicalDigest | None = None physical_checksums: tuple[ReportingPhysicalChecksum, ...] = () native_version_ref: str | None = None native_observed_through: Literal["representative_consumer", "destination"] | None = None verified_format: ReportingFormat | None = None def __post_init__(self) -> None: _freeze_fields(self) object.__setattr__(self, "verified_at", aware_utc(self.verified_at)) _positive(self.row_count, zero=True) _unique_totals(self.control_totals) object.__setattr__(self, "physical_checksums", tuple(self.physical_checksums)) if self.native_version_ref is not None: native_version_reference(self.native_version_ref) if self.native_observed_through is not None and self.native_version_ref is None: raise ValueError("native observation paths require version evidence")
class RestatementCheckpoint (account_id: str,
reporting_obligation_id: str,
checked_at: datetime,
next_observation: int,
provisional_until: datetime | None = None)-
Expand source code
@dataclass(frozen=True) class RestatementCheckpoint: """Durable scheduling state for successful source observations. ``next_observation`` advances even when the source content is unchanged. Without that durable ordinal the next scheduled refresh would replay the same sealed source execution forever instead of making a new observation. """ account_id: str reporting_obligation_id: str checked_at: datetime next_observation: int provisional_until: datetime | None = None def __post_init__(self) -> None: if self.next_observation < 1: raise ValueError("next_observation must be at least one") _utc(self.checked_at) if self.provisional_until is not None: _utc(self.provisional_until)Durable scheduling state for successful source observations.
next_observationadvances even when the source content is unchanged. Without that durable ordinal the next scheduled refresh would replay the same sealed source execution forever instead of making a new observation.Instance variables
var account_id : strvar checked_at : datetime.datetimevar next_observation : intvar provisional_until : datetime.datetime | Nonevar reporting_obligation_id : str
class RestatementCheckpointStore (*args, **kwargs)-
Expand source code
@runtime_checkable class RestatementCheckpointStore(Protocol): """Optional store extension used only by source settling policies.""" async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: ... async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: """Persist the next source observation ordinal after a successful read.""" ...Optional store extension used only by source settling policies.
Ancestors
- typing.Protocol
- typing.Generic
Methods
async def get_restatement_checkpoint(self, *, account_id: str, reporting_obligation_id: str) ‑> RestatementCheckpoint | None-
Expand source code
async def get_restatement_checkpoint( self, *, account_id: str, reporting_obligation_id: str ) -> RestatementCheckpoint | None: ... async def record_restatement_checkpoint(self,
checkpoint: RestatementCheckpoint) ‑> RestatementCheckpoint-
Expand source code
async def record_restatement_checkpoint( self, checkpoint: RestatementCheckpoint ) -> RestatementCheckpoint: """Persist the next source observation ordinal after a successful read.""" ...Persist the next source observation ordinal after a successful read.
class RetryScheduleEntry (scope_key: str,
retry_not_before: datetime,
attempt: int,
blocked: bool = False,
recorded_at: datetime | None = None,
replayed: bool = False)-
Expand source code
@dataclass(frozen=True) class RetryScheduleEntry: """The next permitted source read for one slice, account, or source scope. ``attempt`` counts consecutive failures. Zero clears the schedule after a successful read. Scope keys are generated by the producer, not callers. """ scope_key: str retry_not_before: datetime attempt: int blocked: bool = False recorded_at: datetime | None = None replayed: bool = field(default=False, compare=False, repr=False) def __post_init__(self) -> None: if not self.scope_key or self.attempt < 0: raise ValueError("retry schedule requires a scope key and nonnegative attempt") _utc(self.retry_not_before) if self.recorded_at is None: object.__setattr__(self, "recorded_at", self.retry_not_before) assert self.recorded_at is not None _utc(self.recorded_at)The next permitted source read for one slice, account, or source scope.
attemptcounts consecutive failures. Zero clears the schedule after a successful read. Scope keys are generated by the producer, not callers.Instance variables
var attempt : intvar blocked : boolvar recorded_at : datetime.datetime | Nonevar replayed : boolvar retry_not_before : datetime.datetimevar scope_key : str
class RetryScheduleStore (*args, **kwargs)-
Expand source code
@runtime_checkable class RetryScheduleStore(Protocol): """Optional shared retry state for producer source acquisitions.""" async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: pass async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: passOptional shared retry state for producer source acquisitions.
Ancestors
- typing.Protocol
- typing.Generic
Methods
async def get_retry_schedule(self, *, scope_key: str) ‑> RetryScheduleEntry | None-
Expand source code
async def get_retry_schedule(self, *, scope_key: str) -> RetryScheduleEntry | None: pass async def record_retry_schedule(self,
entry: RetryScheduleEntry) ‑> RetryScheduleEntry-
Expand source code
async def record_retry_schedule(self, entry: RetryScheduleEntry) -> RetryScheduleEntry: pass
class StatusLifecycleIntent (action: "Literal['ensure_mismatch', 'retire_mismatch', 'refine_scope']",
lifecycle: ReportingIssueLifecycle,
scope: ReportingStatusScope)-
Expand source code
@dataclass(frozen=True) class StatusLifecycleIntent: action: Literal["ensure_mismatch", "retire_mismatch", "refine_scope"] lifecycle: ReportingIssueLifecycle scope: ReportingStatusScopeStatusLifecycleIntent(action: "Literal['ensure_mismatch', 'retire_mismatch', 'refine_scope']", lifecycle: 'ReportingIssueLifecycle', scope: 'ReportingStatusScope')
Instance variables
var action : Literal['ensure_mismatch', 'retire_mismatch', 'refine_scope']var lifecycle : ReportingIssueLifecyclevar scope : ReportingStatusScope
class StatusProjectionInput (snapshot: ReportingStatusSnapshot,
scope: ReportingStatusScope,
escalation: ReportingDeliveryEscalation | None = None,
delivery_config_ids: tuple[str, ...] = (),
media_buy_ids: tuple[str, ...] = (),
feed_purposes: tuple[str, ...] = (),
period_start: datetime | None = None,
period_end: datetime | None = None,
reconciliation: tuple[ReportingDeliveryRecord, ...] | None = None,
consumer_status_enabled: bool = True)-
Expand source code
@dataclass(frozen=True) class StatusProjectionInput: snapshot: ReportingStatusSnapshot scope: ReportingStatusScope escalation: ReportingDeliveryEscalation | None = None delivery_config_ids: tuple[str, ...] = () media_buy_ids: tuple[str, ...] = () feed_purposes: tuple[str, ...] = () period_start: datetime | None = None period_end: datetime | None = None # None selects the immutable legacy C representation. Versioned callers # pass the complete captured account history, even when it is empty. reconciliation: tuple[ReportingDeliveryRecord, ...] | None = None consumer_status_enabled: bool = TrueStatusProjectionInput(snapshot: 'ReportingStatusSnapshot', scope: 'ReportingStatusScope', escalation: 'ReportingDeliveryEscalation | None' = None, delivery_config_ids: 'tuple[str, …]' = (), media_buy_ids: 'tuple[str, …]' = (), feed_purposes: 'tuple[str, …]' = (), period_start: 'datetime | None' = None, period_end: 'datetime | None' = None, reconciliation: 'tuple[ReportingDeliveryRecord, …] | None' = None, consumer_status_enabled: 'bool' = True)
Instance variables
var consumer_status_enabled : boolvar delivery_config_ids : tuple[str, ...]var escalation : ReportingDeliveryEscalation | Nonevar feed_purposes : tuple[str, ...]var media_buy_ids : tuple[str, ...]var period_end : datetime.datetime | Nonevar period_start : datetime.datetime | Nonevar reconciliation : tuple[ReportingDestinationBinding | ReportingObligationDeliveryRecord | ReportingMaterializationAttempt | ReportingMaterializationRecord | ReportingMaterializationCheck | ReportingRevisionReceiptRecord | ReportingAdjustmentReceiptRecord, ...] | Nonevar scope : ReportingStatusScopevar snapshot : ReportingStatusSnapshot
class StatusProjectionResult (scope: ReportingStatusScope,
health: ReportingHealth,
issues: tuple[ReportingIssue, ...],
obligations: tuple[StatusObligationProjection, ...],
configurations: tuple[ReportingConfiguration, ...],
pending_count: int,
next_due_at: datetime | None,
intents: tuple[StatusLifecycleIntent, ...],
fingerprint: str)-
Expand source code
@dataclass(frozen=True) class StatusProjectionResult: scope: ReportingStatusScope health: ReportingHealth issues: tuple[ReportingIssue, ...] obligations: tuple[StatusObligationProjection, ...] configurations: tuple[ReportingConfiguration, ...] pending_count: int next_due_at: datetime | None intents: tuple[StatusLifecycleIntent, ...] fingerprint: str @property def publishable(self) -> bool: return not self.intents and ( any(i.severity == self.health for i in self.issues) if self.health in {"delayed", "action_required"} else not self.issues ) @property def issue_ids(self) -> tuple[str, ...]: return tuple(sorted({issue.issue_id for issue in self.issues}))[:16] def canonical(self) -> dict[str, Any]: return { "health": self.health, "issues": [issue.to_wire() for issue in self.issues], }StatusProjectionResult(scope: 'ReportingStatusScope', health: 'ReportingHealth', issues: 'tuple[ReportingIssue, …]', obligations: 'tuple[StatusObligationProjection, …]', configurations: 'tuple[ReportingConfiguration, …]', pending_count: 'int', next_due_at: 'datetime | None', intents: 'tuple[StatusLifecycleIntent, …]', fingerprint: 'str')
Instance variables
var configurations : tuple[ReportingConfiguration, ...]var fingerprint : strvar health : Literal['healthy', 'waiting', 'delayed', 'action_required', 'complete']var intents : tuple[StatusLifecycleIntent, ...]prop issue_ids : tuple[str, ...]-
Expand source code
@property def issue_ids(self) -> tuple[str, ...]: return tuple(sorted({issue.issue_id for issue in self.issues}))[:16] var issues : tuple[ReportingIssue, ...]var next_due_at : datetime.datetime | Nonevar obligations : tuple[StatusObligationProjection, ...]var pending_count : intprop publishable : bool-
Expand source code
@property def publishable(self) -> bool: return not self.intents and ( any(i.severity == self.health for i in self.issues) if self.health in {"delayed", "action_required"} else not self.issues ) var scope : ReportingStatusScope
Methods
def canonical(self) ‑> dict[str, typing.Any]-
Expand source code
def canonical(self) -> dict[str, Any]: return { "health": self.health, "issues": [issue.to_wire() for issue in self.issues], }
class WorkerTurn (leased: LeasedConfiguration | None = None,
obligations_committed: list[str] = <factory>,
revisions_committed: list[str] = <factory>,
slices_failed: list[str] = <factory>,
escalated: list[str] = <factory>,
earliest_retry_at: datetime | None = None)-
Expand source code
@dataclass class WorkerTurn: """What one turn of the worker actually did. Returned rather than logged-and-forgotten so a supervisor can decide whether to run again immediately or back off, and so tests can assert on the loop without scraping logs. """ leased: LeasedConfiguration | None = None obligations_committed: list[str] = field(default_factory=list) revisions_committed: list[str] = field(default_factory=list) slices_failed: list[str] = field(default_factory=list) escalated: list[str] = field(default_factory=list) earliest_retry_at: datetime | None = None _retry_keys_by_obligation: dict[str, tuple[str, str, str]] = field( default_factory=dict, repr=False, compare=False ) _retry_entries: dict[str, RetryScheduleEntry] = field( default_factory=dict, repr=False, compare=False ) @property def did_work(self) -> bool: return bool( self.obligations_committed or self.revisions_committed or self.slices_failed or self.escalated )What one turn of the worker actually did.
Returned rather than logged-and-forgotten so a supervisor can decide whether to run again immediately or back off, and so tests can assert on the loop without scraping logs.
Instance variables
prop did_work : bool-
Expand source code
@property def did_work(self) -> bool: return bool( self.obligations_committed or self.revisions_committed or self.slices_failed or self.escalated ) var earliest_retry_at : datetime.datetime | Nonevar escalated : list[str]var leased : LeasedConfiguration | Nonevar obligations_committed : list[str]var revisions_committed : list[str]var slices_failed : list[str]