From 268789ba5d7e0ed73d6fd46286f929fbbdb5c692 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 30 Oct 2026 20:24:17 +0000 Subject: [PATCH] feat(reporting): use owed and list_subscription_principals, as the TypeScript SDK does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The naming freeze for the Python low-level surface (DX plan §3.1), matching adcp-client#3546: - expected_subscription_periods is renamed owed_subscription_periods and ExpectedSubscriptionPeriod is renamed OwedSubscriptionPeriod. "Owed" is the one word for what a seller must deliver on both sides; "expected" already means something else in Reliable Reporting. The old names stay for one beta: the function warns with DeprecationWarning and delegates, and the type is an alias. - ReportingSubscriptionEngine.list_principals is renamed list_subscription_principals; the old name warns and delegates. The state-store port keeps list_principals, as §2.4 specifies. Internal callers, tests and docs use the new names. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/reliable-reporting-service.md | 4 +-- docs/reporting-subscriptions-buyer.md | 2 +- docs/reporting-subscriptions-coverage.md | 2 +- llms.txt | 2 +- src/adcp/reporting/_reconcile.py | 1 - src/adcp/reporting/conformance.py | 3 +- src/adcp/reporting/feed/projection.py | 10 +++--- src/adcp/reporting/ledger/consumer_status.py | 2 +- .../ledger/reconciliation_projection.py | 4 ++- src/adcp/reporting/outbox/worker.py | 7 +++-- src/adcp/reporting/service.py | 10 +++--- src/adcp/reporting/subscriptions/__init__.py | 6 +++- src/adcp/reporting/subscriptions/consumer.py | 10 +++--- src/adcp/reporting/subscriptions/coverage.py | 29 ++++++++++++++--- src/adcp/reporting/subscriptions/engine.py | 27 +++++++++++----- src/adcp/reporting/subscriptions/ledger.py | 4 +-- src/adcp/reporting/subscriptions/managed.py | 14 ++++----- .../test_reporting_storage_migrations_pg.py | 2 +- tests/test_reporting_subscription_coverage.py | 31 ++++++++++++++++--- tests/test_reporting_subscription_engine.py | 10 +++--- ...st_reporting_subscription_grant_journal.py | 2 +- tests/test_reporting_subscription_parity.py | 6 ++-- 22 files changed, 123 insertions(+), 65 deletions(-) diff --git a/docs/reliable-reporting-service.md b/docs/reliable-reporting-service.md index 227d3a2dd..66ef43bc4 100644 --- a/docs/reliable-reporting-service.md +++ b/docs/reliable-reporting-service.md @@ -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. diff --git a/docs/reporting-subscriptions-buyer.md b/docs/reporting-subscriptions-buyer.md index fa0ee3e72..8d8649cc7 100644 --- a/docs/reporting-subscriptions-buyer.md +++ b/docs/reporting-subscriptions-buyer.md @@ -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 diff --git a/docs/reporting-subscriptions-coverage.md b/docs/reporting-subscriptions-coverage.md index 7980023d0..7cfe02d9d 100644 --- a/docs/reporting-subscriptions-coverage.md +++ b/docs/reporting-subscriptions-coverage.md @@ -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 diff --git a/llms.txt b/llms.txt index 885f98f25..322e72590 100644 --- a/llms.txt +++ b/llms.txt @@ -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 diff --git a/src/adcp/reporting/_reconcile.py b/src/adcp/reporting/_reconcile.py index b6fbbe775..5ee9257d2 100644 --- a/src/adcp/reporting/_reconcile.py +++ b/src/adcp/reporting/_reconcile.py @@ -74,7 +74,6 @@ async def get_reporting_status( class ReportingReconciliationClient(ReportingStatusClient, Protocol): - async def sync_reporting_receipts( self, request: SyncReportingReceiptsRequest ) -> TaskResult[SyncReportingReceiptsResponse]: diff --git a/src/adcp/reporting/conformance.py b/src/adcp/reporting/conformance.py index b240f8ebe..c7b000ee4 100644 --- a/src/adcp/reporting/conformance.py +++ b/src/adcp/reporting/conformance.py @@ -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 diff --git a/src/adcp/reporting/feed/projection.py b/src/adcp/reporting/feed/projection.py index 6083ff2fd..de58dcfbe 100644 --- a/src/adcp/reporting/feed/projection.py +++ b/src/adcp/reporting/feed/projection.py @@ -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: @@ -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 diff --git a/src/adcp/reporting/ledger/consumer_status.py b/src/adcp/reporting/ledger/consumer_status.py index f7e016ec0..2e790a7bc 100644 --- a/src/adcp/reporting/ledger/consumer_status.py +++ b/src/adcp/reporting/ledger/consumer_status.py @@ -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, diff --git a/src/adcp/reporting/ledger/reconciliation_projection.py b/src/adcp/reporting/ledger/reconciliation_projection.py index 4aefa6681..fd56c2e84 100644 --- a/src/adcp/reporting/ledger/reconciliation_projection.py +++ b/src/adcp/reporting/ledger/reconciliation_projection.py @@ -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" diff --git a/src/adcp/reporting/outbox/worker.py b/src/adcp/reporting/outbox/worker.py index 27d95ae16..6e1ebcb7a 100644 --- a/src/adcp/reporting/outbox/worker.py +++ b/src/adcp/reporting/outbox/worker.py @@ -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): diff --git a/src/adcp/reporting/service.py b/src/adcp/reporting/service.py index 0b745c8a4..d1afcbc5f 100644 --- a/src/adcp/reporting/service.py +++ b/src/adcp/reporting/service.py @@ -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, @@ -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 diff --git a/src/adcp/reporting/subscriptions/__init__.py b/src/adcp/reporting/subscriptions/__init__.py index 8847af538..0fe20282a 100644 --- a/src/adcp/reporting/subscriptions/__init__.py +++ b/src/adcp/reporting/subscriptions/__init__.py @@ -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. @@ -48,6 +48,7 @@ from adcp.reporting.subscriptions.coverage import ( AccountTimezoneChange, ExpectedSubscriptionPeriod, + OwedSubscriptionPeriod, SubscriptionAuthorization, SubscriptionBuy, SubscriptionBuyFlight, @@ -56,6 +57,7 @@ SubscriptionVersionWindow, authorization_chain_start, expected_subscription_periods, + owed_subscription_periods, subscription_covers_account, subscription_exceptions, subscription_version_windows, @@ -164,6 +166,7 @@ "STANDARD_REPORTING_DIMENSIONS", "AccountTimezoneChange", "ExpectedSubscriptionPeriod", + "OwedSubscriptionPeriod", "ReportingSubscriptionSupport", "SubscriptionAuthorization", "SubscriptionBuy", @@ -178,6 +181,7 @@ "SubscriptionVersionWindow", "advertise_reporting_subscriptions", "expected_subscription_periods", + "owed_subscription_periods", "reporting_configuration_tasks", "resolve_subscription_offerings", "subscription_capabilities", diff --git a/src/adcp/reporting/subscriptions/consumer.py b/src/adcp/reporting/subscriptions/consumer.py index 84912ada7..770bd68c2 100644 --- a/src/adcp/reporting/subscriptions/consumer.py +++ b/src/adcp/reporting/subscriptions/consumer.py @@ -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 @@ -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 @@ -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 @@ -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, diff --git a/src/adcp/reporting/subscriptions/coverage.py b/src/adcp/reporting/subscriptions/coverage.py index 6664cede5..a20870555 100644 --- a/src/adcp/reporting/subscriptions/coverage.py +++ b/src/adcp/reporting/subscriptions/coverage.py @@ -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 @@ -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, @@ -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`` @@ -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, @@ -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", @@ -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", diff --git a/src/adcp/reporting/subscriptions/engine.py b/src/adcp/reporting/subscriptions/engine.py index 63e461eec..806b68bba 100644 --- a/src/adcp/reporting/subscriptions/engine.py +++ b/src/adcp/reporting/subscriptions/engine.py @@ -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 @@ -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, ) @@ -177,7 +177,7 @@ async def is_period_delivered( self, principal: PrincipalLike, account_id: str, - period: ExpectedSubscriptionPeriod, + period: OwedSubscriptionPeriod, *, by: datetime, ) -> bool: ... @@ -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 @@ -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. """ @@ -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 @@ -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, diff --git a/src/adcp/reporting/subscriptions/ledger.py b/src/adcp/reporting/subscriptions/ledger.py index cad0841de..e2e16a209 100644 --- a/src/adcp/reporting/subscriptions/ledger.py +++ b/src/adcp/reporting/subscriptions/ledger.py @@ -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. @@ -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) diff --git a/src/adcp/reporting/subscriptions/managed.py b/src/adcp/reporting/subscriptions/managed.py index 256ca964c..8883d561c 100644 --- a/src/adcp/reporting/subscriptions/managed.py +++ b/src/adcp/reporting/subscriptions/managed.py @@ -58,7 +58,7 @@ ReportingSubscriptionServiceOptions, SubscriptionPlanningFailure, ) - from adcp.reporting.subscriptions.coverage import ExpectedSubscriptionPeriod + from adcp.reporting.subscriptions.coverage import OwedSubscriptionPeriod from adcp.reporting.subscriptions.engine import ReportingSubscriptionEngine from adcp.reporting.subscriptions.resolution import SubscriptionOffering from adcp.server.principal import ( @@ -398,7 +398,7 @@ async def _is_period_delivered( self, principal: Any, account_id: str, - period: ExpectedSubscriptionPeriod, + period: OwedSubscriptionPeriod, *, by: datetime, ) -> bool: @@ -580,7 +580,7 @@ def compose( self._engine = engine self._service_options = ReportingSubscriptionServiceOptions( engine=engine, - list_principals=self.list_principals, + list_principals=self.list_subscription_principals, resolve_principal=self.resolve, planning_error_handler=options.planning_error_handler, definitions=options.definitions, @@ -667,9 +667,9 @@ def bind_handler(self, handler: Any) -> None: # -- principals ------------------------------------------------------------ - async def list_principals(self) -> list[PrincipalRef]: + async def list_subscription_principals(self) -> list[PrincipalRef]: """Every principal with subscription state, from the engine store's index.""" - return await self.engine.list_principals() + return await self.engine.list_subscription_principals() async def resolve(self, request: Any, context: Any) -> PrincipalRef: """The authenticated principal of a request; never derived from request content.""" @@ -819,7 +819,7 @@ async def _reconcile_concerned( concerned[caller.subject] = caller if account_id is not None: try: - principals = await self.list_principals() + principals = await self.list_subscription_principals() except Exception as error: # noqa: BLE001 - reported, never raised to the buyer logger.warning("reporting subscription principals could not be listed: %s", error) principals = [] @@ -834,7 +834,7 @@ async def sweep(self, *, now: datetime) -> list[SubscriptionPlanningFailure]: from adcp.reporting.service_subscriptions import SubscriptionPlanningFailure failures: list[SubscriptionPlanningFailure] = [] - for principal in await self.list_principals(): + for principal in await self.list_subscription_principals(): try: if not await self._subscribed(principal): continue diff --git a/tests/conformance/reporting/test_reporting_storage_migrations_pg.py b/tests/conformance/reporting/test_reporting_storage_migrations_pg.py index 785269eaf..67e9c9a81 100644 --- a/tests/conformance/reporting/test_reporting_storage_migrations_pg.py +++ b/tests/conformance/reporting/test_reporting_storage_migrations_pg.py @@ -143,6 +143,6 @@ async def test_a_managed_service_runs_on_postgres_storage() -> None: assert turn.subscription_errors == {} assert turn.subscription_obligations_written == 2 engine = service._managed.engine - assert await engine.list_principals() == [PrincipalRef(None, AGENCY)] + assert await engine.list_subscription_principals() == [PrincipalRef(None, AGENCY)] finally: await service.close() diff --git a/tests/test_reporting_subscription_coverage.py b/tests/test_reporting_subscription_coverage.py index 8d24b3273..9dbc5bb19 100644 --- a/tests/test_reporting_subscription_coverage.py +++ b/tests/test_reporting_subscription_coverage.py @@ -3,7 +3,7 @@ Every expected period below is written out by hand from the rule in ``core/reporting-subscription.json`` (coverage, cutover) and ``core/reporting-subscription-coverage.json``. The seller runtime owes exactly -what :func:`expected_subscription_periods` returns, so these tables are the +what :func:`owed_subscription_periods` returns, so these tables are the billing contract: change one only with a spec citation. Unless a test says otherwise the account is in ``America/New_York`` in @@ -22,7 +22,7 @@ from adcp.reporting.subscriptions import ( AccountTimezoneChange, CoverageInputError, - ExpectedSubscriptionPeriod, + OwedSubscriptionPeriod, SubscriptionAuthorization, SubscriptionBuy, SubscriptionBuyFlight, @@ -31,7 +31,7 @@ SubscriptionException, SubscriptionOffering, SubscriptionVersionWindow, - expected_subscription_periods, + owed_subscription_periods, resolve_subscription_offerings, subscription_exceptions, subscription_version_windows, @@ -194,13 +194,13 @@ def starts(**kwargs) -> list[str]: return [item.period_start for item in run(**kwargs)] -def run(**kwargs) -> list[ExpectedSubscriptionPeriod]: +def run(**kwargs) -> list[OwedSubscriptionPeriod]: kwargs.setdefault("subscription", state("2026-10-01T00:00:00Z")) kwargs.setdefault("account_id", "acct-1") kwargs.setdefault("offerings", [DAILY, HOURLY_UTC]) kwargs.setdefault("account_timezone", NY) kwargs.setdefault("now", at("2026-12-31T00:00:00Z")) - return expected_subscription_periods(**kwargs) + return owed_subscription_periods(**kwargs) def days(first: str, count: int) -> list[str]: @@ -1785,3 +1785,24 @@ def test_more_boundaries_than_max_iterations_raise(self) -> None: def test_a_budget_must_be_positive(self, name: str) -> None: with pytest.raises(CoverageInputError, match=name): run(buys=[buy()], **{name: 0}) + + +def test_the_expected_names_are_deprecated_aliases() -> None: + from adcp.reporting.subscriptions import ( + ExpectedSubscriptionPeriod, + OwedSubscriptionPeriod, + expected_subscription_periods, + ) + + assert ExpectedSubscriptionPeriod is OwedSubscriptionPeriod + kwargs = { + "subscription": state("2026-10-01T00:00:00Z"), + "account_id": "acct-1", + "buys": [buy(end_time="2026-10-04T04:00:00Z")], + "offerings": [DAILY, HOURLY_UTC], + "account_timezone": NY, + "now": at("2026-12-31T00:00:00Z"), + } + with pytest.warns(DeprecationWarning, match="owed_subscription_periods"): + legacy = expected_subscription_periods(**kwargs) + assert legacy == owed_subscription_periods(**kwargs) diff --git a/tests/test_reporting_subscription_engine.py b/tests/test_reporting_subscription_engine.py index 334a67373..f9c67b1b9 100644 --- a/tests/test_reporting_subscription_engine.py +++ b/tests/test_reporting_subscription_engine.py @@ -1,7 +1,7 @@ """Seller runtime for reporting subscriptions, and buyer/seller conformance. The conformance cases follow adcontextprotocol/adcp-client#3504: the buyer -computes its expected periods with ``expected_subscription_periods`` from what +computes its expected periods with ``owed_subscription_periods`` from what it can see (the ``get_principal`` readback, ``subscription_coverage[]``, its own buys and grant history), and the seller's owed periods must equal them period by period. @@ -26,7 +26,7 @@ SubscriptionBuyFlight, SubscriptionBuyPackage, SubscriptionOffering, - expected_subscription_periods, + owed_subscription_periods, ) from adcp.server import PrincipalChange, PrincipalIdentity, PrincipalService @@ -223,7 +223,7 @@ async def buyer_expects(self, subscription_id: str, account_id: str, now: str, c state = await self.state(subscription_id) excluded = coverage.excluded if coverage is not None else () gaps = (coverage.gaps or ()) if coverage is not None else () - return expected_subscription_periods( + return owed_subscription_periods( subscription=state, version_configurations=self.sent[subscription_id], account_id=account_id, @@ -685,7 +685,7 @@ async def test_timezone_cutovers_carry_previous_timezone() -> None: # The buyer needs no timezone history of its own: list_accounts shows the # current zone, and previous_timezone gives the zone before the cutover. seller = await world.seller_owes("analytics", "acct-1", "2026-10-08T00:00:00Z") - buyer = expected_subscription_periods( + buyer = owed_subscription_periods( subscription=await world.state("analytics"), account_id="acct-1", buys=world.host.buys["acct-1"], @@ -1065,7 +1065,7 @@ async def test_timezone_change_then_new_version_in_one_period_matches_the_wire() await world.sync(subscription(subscription_version=2)) coverage = await world.coverage("acct-1", "analytics") seller = await world.seller_owes("analytics", "acct-1", "2026-10-07T05:00:00Z") - buyer = expected_subscription_periods( + buyer = owed_subscription_periods( subscription=await world.state("analytics"), account_id="acct-1", buys=world.host.buys["acct-1"], diff --git a/tests/test_reporting_subscription_grant_journal.py b/tests/test_reporting_subscription_grant_journal.py index c7f30d833..5b814c564 100644 --- a/tests/test_reporting_subscription_grant_journal.py +++ b/tests/test_reporting_subscription_grant_journal.py @@ -291,7 +291,7 @@ async def test_callers_without_subscriptions_are_served_unchanged() -> None: world = _World() listed = await world.platform.list_accounts({}, _ctx("https://stranger.example/agent")) assert listed["accounts"][0]["authorization"]["authorized_at"] == "2026-10-01T00:00:00Z" - assert await world.service._managed.engine.list_principals() == [] + assert await world.service._managed.engine.list_subscription_principals() == [] @pytest.mark.asyncio diff --git a/tests/test_reporting_subscription_parity.py b/tests/test_reporting_subscription_parity.py index 1036353dc..b5e3f3ab1 100644 --- a/tests/test_reporting_subscription_parity.py +++ b/tests/test_reporting_subscription_parity.py @@ -6,7 +6,7 @@ case gives the buyer-side input and either the exact owed rows, derived from the spec text its ``spec_refs`` cite and reproduced by an independent reference model, or the input error. Both SDKs must produce identical periods, -so this test maps each input onto :func:`expected_subscription_periods` and +so this test maps each input onto :func:`owed_subscription_periods` and compares instants row by row. The rows are derived with IANA tzdata 2026f. The test pins zoneinfo to the @@ -35,7 +35,7 @@ SubscriptionDestination, SubscriptionOffering, SubscriptionVersionWindow, - expected_subscription_periods, + owed_subscription_periods, ) SPEC = Path(__file__).parent / "fixtures" / "reporting_subscriptions" / "spec" @@ -179,7 +179,7 @@ def buys(input_: dict[str, Any]) -> list[SubscriptionBuy]: def periods(input_: dict[str, Any]): account = input_["account"] - return expected_subscription_periods( + return owed_subscription_periods( subscription=windows(input_["versions"], input_.get("inactive_intervals", [])), account_id=account["account_id"], buys=buys(input_),