Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/reliable-reporting-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,13 @@ platform = reporting.install(MySeller()) # then serve the platform as usual
The rest of this section describes the building blocks the managed path
composes, for sellers who wire them by hand.

**Buyer.** `expected_subscription_periods(...)` is the coverage rule as a pure
**Buyer.** `owed_subscription_periods(...)` is the coverage rule as a pure
function over plain data. Pass it the `get_principal` subscription readback,
the buys you can see (`SubscriptionBuy`, with earlier flights when a buy was
extended or reinstated), the offerings with the seller's dimension mapping
(`SubscriptionOffering`), the account timezone, the account's grant history,
and the `subscription_coverage[]` exceptions and gaps. It returns
`ExpectedSubscriptionPeriod` values, which are `ExpectedReportingPeriod`s, so
`OwedSubscriptionPeriod` values, which are `ExpectedReportingPeriod`s, so
they feed `evaluate_reporting_ledger` and the consumer-status planner as they
are.

Expand Down
2 changes: 1 addition & 1 deletion docs/reporting-subscriptions-buyer.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ An `all_authorized_accounts` subscription follows every grant, including account

## Reconcile

`client.reporting.reconcile()` and `explain()` arrive in a later beta. Until then, `expected_subscription_periods(...)` and `ReportingSubscriptionTracker` compute owed periods from your own records (see [Coverage, explained](reporting-subscriptions-coverage.md#the-owed-rules)).
`client.reporting.reconcile()` and `explain()` arrive in a later beta. Until then, `owed_subscription_periods(...)` and `ReportingSubscriptionTracker` compute owed periods from your own records (see [Coverage, explained](reporting-subscriptions-coverage.md#the-owed-rules)).

## Events

Expand Down
2 changes: 1 addition & 1 deletion docs/reporting-subscriptions-coverage.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Coverage, explained

Both sides compute which periods a subscription owes with one function, `expected_subscription_periods`, over the same facts. Never compute periods yourself.
Both sides compute which periods a subscription owes with one function, `owed_subscription_periods`, over the same facts. Never compute periods yourself.

## The owed rules

Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ docs/reporting-subscriptions-buyer.md.
- DON'T pick `subscription_version` yourself: `subscribe` chooses it.
- DO send an explicit scope: `{"all_authorized_accounts": True}` or `{"account_ids": [...]}`.
- DON'T compute reporting periods with local time; use
`expected_subscription_periods` (and, later, `client.reporting.reconcile`).
`owed_subscription_periods` (and, later, `client.reporting.reconcile`).
- DO switch on `next_step(outcome)` for accounts. DON'T fetch
`pending_action.url`: show it to a person.
- Sellers: DON'T call `engine.reconcile` from request handlers; the service
Expand Down
1 change: 0 additions & 1 deletion src/adcp/reporting/_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ async def get_reporting_status(


class ReportingReconciliationClient(ReportingStatusClient, Protocol):

async def sync_reporting_receipts(
self, request: SyncReportingReceiptsRequest
) -> TaskResult[SyncReportingReceiptsResponse]:
Expand Down
3 changes: 1 addition & 2 deletions src/adcp/reporting/conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,8 +777,7 @@ def _assert_cells_do_not_regress(
if item.status in _AVAILABLE and (later is None or later.status not in _AVAILABLE):
raise _fail(
"REVISION_SEQUENCE_INVALID",
f"{label} must not remove or degrade constituent availability from its "
"predecessor",
f"{label} must not remove or degrade constituent availability from its predecessor",
)
if item.data_through is not None and (
later is None
Expand Down
10 changes: 6 additions & 4 deletions src/adcp/reporting/feed/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ def capture_feed(
and owners[owner_id].generation_key in bindings
and record.canonical_content_digest is not None
):
wires[identity][
"canonical_content_digest"
] = record.canonical_content_digest.to_wire()
wires[identity]["canonical_content_digest"] = (
record.canonical_content_digest.to_wire()
)
if record.supersedes_reporting_revision_id is not None:
predecessor = revisions.get(record.supersedes_reporting_revision_id)
if predecessor is None or predecessor.reporting_obligation_id != owner_id:
Expand Down Expand Up @@ -481,7 +481,9 @@ def capture_feed(
finality = (
revisions[revision_id].finality
if revision_id in revisions
else owners[owner_id].required_finality if owner_id in owners else None
else owners[owner_id].required_finality
if owner_id in owners
else None
)
if finality not in finalities:
matches = False
Expand Down
2 changes: 1 addition & 1 deletion src/adcp/reporting/ledger/consumer_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ async def _find_period_obligation(
and item.delivery_config_version == record.delivery_config_version
and item.report_definition_id == record.report_definition_id
),
key=lambda item: (item.subscription.offering_id if item.subscription else ""),
key=lambda item: item.subscription.offering_id if item.subscription else "",
):
candidate = await self.store.find_obligation(
account_id=record.account_id,
Expand Down
4 changes: 3 additions & 1 deletion src/adcp/reporting/ledger/reconciliation_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ def project_reconciliation(
wire["reconciliation_status"] = (
"rejected"
if rejected
else "accepted" if accepted and not pending_adjustments else "pending"
else "accepted"
if accepted and not pending_adjustments
else "pending"
)
else:
wire["reconciliation_status"] = "not_required"
Expand Down
7 changes: 4 additions & 3 deletions src/adcp/reporting/outbox/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,10 @@ async def deliver_one(self, *, account_id: str) -> bool:
return False
observation = _HttpObservation()
if self.delivery_window is not None:
observation.retry_deadline, observation.retry_window_expired = (
await self.delivery_window.inspect(lease, now=self._clock())
)
(
observation.retry_deadline,
observation.retry_window_expired,
) = await self.delivery_window.inspect(lease, now=self._clock())
try:
opened = self.cipher.open(lease.delivery)
except (ReportingNotificationError, ValueError, TypeError):
Expand Down
10 changes: 4 additions & 6 deletions src/adcp/reporting/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,9 +912,9 @@ def _compose_production_subscriptions(self) -> None:
offering_id: item.verification_key.definition
for offering_id, item in offerings.items()
},
currency=lambda configuration: self._production_offering(
configuration
).producer._offerings.currency,
currency=lambda configuration: (
self._production_offering(configuration).producer._offerings.currency
),
automated_recovery_window=support.automated_recovery_window,
status_retention_days=support.status_retention_days,
install=self._install_production_generation,
Expand Down Expand Up @@ -2056,9 +2056,7 @@ async def write(instance: Any, params: Any, context: Any | None = None) -> Any:
if name in {"sync_accounts", "list_accounts"}:
result = await service._project_accounts(result, context)
if name != "list_accounts":
await service._subscription_write(
_written_account(params, context), context
)
await service._subscription_write(_written_account(params, context), context)
return result

write.__name__ = name
Expand Down
6 changes: 5 additions & 1 deletion src/adcp/reporting/subscriptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
its principal through ``sync_principal`` instead of once per account through
``sync_accounts``. This package holds the parts both sides share:

* :func:`expected_subscription_periods` -- the pure coverage rule. A buyer runs
* :func:`owed_subscription_periods` -- the pure coverage rule. A buyer runs
it from its own records to learn exactly which periods it is owed; the
seller runtime runs it over the same facts to decide which obligations it
owes.
Expand Down Expand Up @@ -48,6 +48,7 @@
from adcp.reporting.subscriptions.coverage import (
AccountTimezoneChange,
ExpectedSubscriptionPeriod,
OwedSubscriptionPeriod,
SubscriptionAuthorization,
SubscriptionBuy,
SubscriptionBuyFlight,
Expand All @@ -56,6 +57,7 @@
SubscriptionVersionWindow,
authorization_chain_start,
expected_subscription_periods,
owed_subscription_periods,
subscription_covers_account,
subscription_exceptions,
subscription_version_windows,
Expand Down Expand Up @@ -164,6 +166,7 @@
"STANDARD_REPORTING_DIMENSIONS",
"AccountTimezoneChange",
"ExpectedSubscriptionPeriod",
"OwedSubscriptionPeriod",
"ReportingSubscriptionSupport",
"SubscriptionAuthorization",
"SubscriptionBuy",
Expand All @@ -178,6 +181,7 @@
"SubscriptionVersionWindow",
"advertise_reporting_subscriptions",
"expected_subscription_periods",
"owed_subscription_periods",
"reporting_configuration_tasks",
"resolve_subscription_offerings",
"subscription_capabilities",
Expand Down
10 changes: 5 additions & 5 deletions src/adcp/reporting/subscriptions/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
the buyer registered (the readback keeps only the current configuration),
tracks lost authority from ``accounts_unauthorized[]``, and computes the
buyer's expected periods with
:func:`~adcp.reporting.subscriptions.expected_subscription_periods`.
:func:`~adcp.reporting.subscriptions.owed_subscription_periods`.

The expected periods are :class:`~adcp.reporting.ExpectedReportingPeriod`
values, so they go straight into
Expand All @@ -32,10 +32,10 @@
from adcp.reporting.subscriptions._clock import require_zone, utc_instant
from adcp.reporting.subscriptions.coverage import (
AccountTimezoneChange,
ExpectedSubscriptionPeriod,
OwedSubscriptionPeriod,
SubscriptionAuthorization,
SubscriptionBuy,
expected_subscription_periods,
owed_subscription_periods,
subscription_version_windows,
)
from adcp.reporting.subscriptions.resolution import SubscriptionOffering
Expand Down Expand Up @@ -534,7 +534,7 @@ def expected_periods(
coverage: ReportingSubscriptionCoverage | None = None,
include_unresolved: bool = False,
require_prior_configurations: bool = True,
) -> list[ExpectedSubscriptionPeriod]:
) -> list[OwedSubscriptionPeriod]:
"""What the seller owes this account under the subscription, from buyer records.

``coverage`` is the subscription's ``subscription_coverage[]`` entry
Expand Down Expand Up @@ -564,7 +564,7 @@ def expected_periods(
version_configurations=self._sent.get(subscription_id),
require_prior_configurations=require_prior_configurations,
)
return expected_subscription_periods(
return owed_subscription_periods(
subscription=windows,
account_id=account_id,
buys=buys,
Expand Down
29 changes: 25 additions & 4 deletions src/adcp/reporting/subscriptions/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ def subscription_covers_account(


@dataclass(frozen=True, kw_only=True)
class ExpectedSubscriptionPeriod(ExpectedReportingPeriod):
class OwedSubscriptionPeriod(ExpectedReportingPeriod):
"""One owed period of one (subscription version, resolved offering).

An :class:`~adcp.reporting.ExpectedReportingPeriod`, so it feeds
Expand Down Expand Up @@ -711,7 +711,7 @@ def _windows(
return subscription_version_windows(subscription, version_configurations=version_configurations)


def expected_subscription_periods(
def owed_subscription_periods(
*,
subscription: Any,
account_id: str,
Expand All @@ -732,7 +732,7 @@ def expected_subscription_periods(
include_unresolved: bool = False,
max_periods: int = DEFAULT_MAX_PERIODS,
max_iterations: int = DEFAULT_MAX_ITERATIONS,
) -> list[ExpectedSubscriptionPeriod]:
) -> list[OwedSubscriptionPeriod]:
"""Every period one subscription owes one account, from plain data.

Fails closed rather than return a partial set: more than ``max_periods``
Expand Down Expand Up @@ -930,7 +930,7 @@ def expected_subscription_periods(
)

periods = [
ExpectedSubscriptionPeriod(
OwedSubscriptionPeriod(
delivery_config_id=subscription_id,
delivery_config_version=item.version,
report_definition_id=item.offering.report_definition_id,
Expand Down Expand Up @@ -1079,11 +1079,31 @@ def _subscription_id(subscription: Any, windows: Sequence[SubscriptionVersionWin
return _text(value)


#: Deprecated alias of :class:`OwedSubscriptionPeriod`, kept for one beta.
ExpectedSubscriptionPeriod = OwedSubscriptionPeriod


def expected_subscription_periods(**kwargs: Any) -> list[OwedSubscriptionPeriod]:
"""Deprecated: renamed :func:`owed_subscription_periods` ("owed" on both sides).

Kept for one beta; it warns and delegates.
"""
import warnings

warnings.warn(
"expected_subscription_periods is renamed owed_subscription_periods",
DeprecationWarning,
stacklevel=2,
)
return owed_subscription_periods(**kwargs)


__all__ = [
"AccountTimezoneChange",
"DEFAULT_MAX_ITERATIONS",
"DEFAULT_MAX_PERIODS",
"ExpectedSubscriptionPeriod",
"OwedSubscriptionPeriod",
"SubscriptionAuthorization",
"SubscriptionBuy",
"SubscriptionBuyFlight",
Expand All @@ -1092,6 +1112,7 @@ def _subscription_id(subscription: Any, windows: Sequence[SubscriptionVersionWin
"SubscriptionVersionWindow",
"authorization_chain_start",
"expected_subscription_periods",
"owed_subscription_periods",
"subscription_covers_account",
"subscription_exceptions",
"subscription_version_windows",
Expand Down
27 changes: 19 additions & 8 deletions src/adcp/reporting/subscriptions/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
``get_reporting_status``, keeps ``accounts_with_exceptions[]`` and
``accounts_unauthorized[]`` current in the ``get_principal`` readback, and
computes owed periods with the same
:func:`~adcp.reporting.subscriptions.expected_subscription_periods` a buyer
:func:`~adcp.reporting.subscriptions.owed_subscription_periods` a buyer
runs, so both sides agree period by period.

``max_accounts`` order
Expand All @@ -47,13 +47,13 @@
from adcp.reporting.subscriptions.capabilities import ReportingSubscriptionSupport
from adcp.reporting.subscriptions.coverage import (
AccountTimezoneChange,
ExpectedSubscriptionPeriod,
OwedSubscriptionPeriod,
SubscriptionAuthorization,
SubscriptionBuy,
SubscriptionException,
SubscriptionVersionWindow,
authorization_chain_start,
expected_subscription_periods,
owed_subscription_periods,
subscription_covers_account,
subscription_version_windows,
)
Expand Down Expand Up @@ -177,7 +177,7 @@ async def is_period_delivered(
self,
principal: PrincipalLike,
account_id: str,
period: ExpectedSubscriptionPeriod,
period: OwedSubscriptionPeriod,
*,
by: datetime,
) -> bool: ...
Expand Down Expand Up @@ -746,6 +746,17 @@ def store(self) -> ReportingSubscriptionStateStore:
return self._store

async def list_principals(self) -> list[PrincipalRef]:
"""Deprecated: renamed :meth:`list_subscription_principals`; kept for one beta."""
import warnings

warnings.warn(
"list_principals is renamed list_subscription_principals",
DeprecationWarning,
stacklevel=2,
)
return await self.list_subscription_principals()

async def list_subscription_principals(self) -> list[PrincipalRef]:
"""Every principal the store holds engine state for, in subject order.

Requires a store that implements ``list_principals()`` (both SDK stores
Expand Down Expand Up @@ -1802,10 +1813,10 @@ async def owed_periods(
account_id: str,
*,
now: datetime | None = None,
) -> list[ExpectedSubscriptionPeriod]:
) -> list[OwedSubscriptionPeriod]:
"""Every period this subscription owes the account: the seller's obligations.

The same :func:`expected_subscription_periods` a buyer runs, over the
The same :func:`owed_subscription_periods` a buyer runs, over the
engine's records: every version's configuration, the grant history,
timezone changes and every recorded exclusion.
"""
Expand Down Expand Up @@ -1837,7 +1848,7 @@ def _owed(
buys: Sequence[SubscriptionBuy],
destinations: Mapping[str, Any],
now: datetime,
) -> list[ExpectedSubscriptionPeriod]:
) -> list[OwedSubscriptionPeriod]:
subscription_id = str(state["subscription_id"])
versions = {
version: configuration
Expand All @@ -1862,7 +1873,7 @@ def _owed(
state, version_configurations=versions, require_prior_configurations=True
)
)
return expected_subscription_periods(
return owed_subscription_periods(
subscription=windows,
account_id=account_id,
buys=buys,
Expand Down
4 changes: 2 additions & 2 deletions src/adcp/reporting/subscriptions/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
)
from adcp.reporting.ledger.store import ReportingLedgerStore, ReportingSubscriptionLedger
from adcp.reporting.subscriptions._clock import parse_delivery_sla, utc_instant
from adcp.reporting.subscriptions.coverage import ExpectedSubscriptionPeriod
from adcp.reporting.subscriptions.coverage import OwedSubscriptionPeriod
from adcp.reporting.subscriptions.resolution import SubscriptionOffering, _text, _value

#: The frozen reporting currency of one generation, as its account context gives it.
Expand Down Expand Up @@ -78,7 +78,7 @@ class FrozenOwedSet:
account_id: str
subscription_id: str
through: datetime
periods: tuple[ExpectedSubscriptionPeriod, ...]
periods: tuple[OwedSubscriptionPeriod, ...]
versions: Mapping[int, Any] = field(default_factory=dict)


Expand Down
Loading
Loading