diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py index a8fa46d38..8d202bfe4 100644 --- a/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py @@ -4,7 +4,7 @@ from collections.abc import Callable from datetime import datetime -from typing import Any +from typing import Any, TypeVar, overload from adcp.reporting.service import ( ReliableReportingService, @@ -15,6 +15,34 @@ __all__ = ["postgres_reliable_reporting_service"] +_ServiceT = TypeVar("_ServiceT", bound=ReliableReportingService) + + +@overload +def postgres_reliable_reporting_service( + *, + pool: Any, + account_context: ReportingContextResolver, + caller_resolver: ReportingCallerResolver | None = ..., + clock: Callable[[], datetime] | None = ..., + production: ReportingProductionOptions | None = ..., + service_class: None = ..., + **kwargs: Any, +) -> ReliableReportingService: ... + + +@overload +def postgres_reliable_reporting_service( + *, + pool: Any, + account_context: ReportingContextResolver, + caller_resolver: ReportingCallerResolver | None = ..., + clock: Callable[[], datetime] | None = ..., + production: ReportingProductionOptions | None = ..., + service_class: type[_ServiceT], + **kwargs: Any, +) -> _ServiceT: ... + def postgres_reliable_reporting_service( *, @@ -23,6 +51,7 @@ def postgres_reliable_reporting_service( caller_resolver: ReportingCallerResolver | None = None, clock: Callable[[], datetime] | None = None, production: ReportingProductionOptions | None = None, + service_class: type[ReliableReportingService] | None = None, **kwargs: Any, ) -> ReliableReportingService: """Create durable ledger, staging and seal stores over a borrowed pool. @@ -32,6 +61,10 @@ def postgres_reliable_reporting_service( Startup installs all SDK-owned schemas. Explicit source-store overrides remain the adopter's responsibility. No pool is opened or closed here. + ``service_class`` builds an adopter's ``ReliableReportingService`` + subclass, as ``SubClass.memory(...)`` does, so a host can override the + installed tool methods without copying this composition. + This is what ``ReliableReportingService.postgres(...)`` was before the storage implementations moved to this distribution. """ @@ -45,7 +78,8 @@ def postgres_reliable_reporting_service( else PgReportingLedgerStore(pool=pool, clock=clock) ) staging = PgReportingStagingStore(pool=pool) - return ReliableReportingService( + cls = ReliableReportingService if service_class is None else service_class + return cls( store=store, account_context=account_context, caller_resolver=caller_resolver, diff --git a/tests/test_reporting_storage_service_class.py b/tests/test_reporting_storage_service_class.py new file mode 100644 index 000000000..1e79b502a --- /dev/null +++ b/tests/test_reporting_storage_service_class.py @@ -0,0 +1,35 @@ +"""``postgres_reliable_reporting_service`` builds an adopter's service subclass (#1588).""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("psycopg_pool") + +from adcp_reporting_storage import postgres_reliable_reporting_service # noqa: E402 +from psycopg_pool import AsyncConnectionPool # noqa: E402 + +from adcp.reporting import ReliableReportingService # noqa: E402 +from tests.test_reliable_reporting_service import _account_context # noqa: E402 + + +class HostReportingService(ReliableReportingService): + """A host subclass overriding an installed tool method.""" + + +def _pool() -> AsyncConnectionPool: + return AsyncConnectionPool("postgresql://unused", open=False) + + +def test_default_builds_the_base_service() -> None: + service = postgres_reliable_reporting_service(pool=_pool(), account_context=_account_context) + assert type(service) is ReliableReportingService + + +def test_service_class_builds_the_subclass_like_memory() -> None: + pg = postgres_reliable_reporting_service( + pool=_pool(), account_context=_account_context, service_class=HostReportingService + ) + memory = HostReportingService.memory(account_context=_account_context) + assert type(pg) is HostReportingService + assert type(memory) is HostReportingService diff --git a/tests/type_checks/reporting_storage_service_class.py b/tests/type_checks/reporting_storage_service_class.py new file mode 100644 index 000000000..03ee37f82 --- /dev/null +++ b/tests/type_checks/reporting_storage_service_class.py @@ -0,0 +1,21 @@ +"""Adopter-facing type contract: the Postgres factory returns the requested subclass.""" + +from typing import Any + +from adcp_reporting_storage import postgres_reliable_reporting_service + +from adcp.reporting import ReliableReportingService + + +class HostReportingService(ReliableReportingService): + pass + + +def build(pool: Any, account_context: Any) -> HostReportingService: + return postgres_reliable_reporting_service( + pool=pool, account_context=account_context, service_class=HostReportingService + ) + + +def build_default(pool: Any, account_context: Any) -> ReliableReportingService: + return postgres_reliable_reporting_service(pool=pool, account_context=account_context)