diff --git a/repositories/postgres_unit_of_work.py b/repositories/postgres_unit_of_work.py index 3a10b087..89db8539 100644 --- a/repositories/postgres_unit_of_work.py +++ b/repositories/postgres_unit_of_work.py @@ -21,6 +21,7 @@ from repositories.scenario.scenario_postgres_repository import ( ScenarioPostgresRepository, ) from repositories.solar.solar_postgres_repository import SolarPostgresRepository +from repositories.spatial.spatial_postgres_repository import SpatialPostgresRepository from repositories.unit_of_work import UnitOfWork @@ -42,6 +43,7 @@ class PostgresUnitOfWork(UnitOfWork): self.property = PropertyPostgresRepository(self._session, epc_repo) self.epc = epc_repo self.solar = SolarPostgresRepository(self._session) + self.spatial = SpatialPostgresRepository(self._session) self.property_baseline = PropertyBaselinePostgresRepository(self._session) self.scenario = ScenarioPostgresRepository(self._session) self.product = ProductPostgresRepository(self._session) diff --git a/repositories/unit_of_work.py b/repositories/unit_of_work.py index a8a27cdb..1478855c 100644 --- a/repositories/unit_of_work.py +++ b/repositories/unit_of_work.py @@ -11,6 +11,7 @@ from repositories.product.product_repository import ProductRepository from repositories.property.property_repository import PropertyRepository from repositories.scenario.scenario_repository import ScenarioRepository from repositories.solar.solar_repository import SolarRepository +from repositories.spatial.spatial_repository import SpatialRepository class UnitOfWork(ABC): @@ -28,6 +29,9 @@ class UnitOfWork(ABC): property: PropertyRepository epc: EpcRepository solar: SolarRepository + # Per-UPRN cache of the OS spatial reference data, written in Ingestion + # alongside the EPC/solar enrichments (ADR-0020). + spatial: SpatialRepository property_baseline: PropertyBaselineRepository # Modelling-stage repos (ADR-0017): read the Scenario, read the Product # catalogue, write the Plan + its Plan Measures — all on the one session. diff --git a/tests/orchestration/fakes.py b/tests/orchestration/fakes.py index f8ec1734..b0abd38c 100644 --- a/tests/orchestration/fakes.py +++ b/tests/orchestration/fakes.py @@ -10,6 +10,8 @@ from types import TracebackType from typing import Any, Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData +from domain.geospatial.planning_restrictions import PlanningRestrictions +from domain.geospatial.spatial_reference import SpatialReference from domain.modelling.plan import Plan from domain.modelling.scenario import Scenario from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance @@ -22,6 +24,7 @@ from repositories.epc.epc_repository import EpcRepository from repositories.property.property_repository import PropertyRepository from repositories.scenario.scenario_repository import ScenarioRepository from repositories.solar.solar_repository import SolarRepository +from repositories.spatial.spatial_repository import SpatialRepository from repositories.unit_of_work import UnitOfWork from domain.modelling.product import Product @@ -101,6 +104,24 @@ class FakeSolarRepo(SolarRepository): raise NotImplementedError +class FakeSpatialRepo(SpatialRepository): + """In-memory per-UPRN spatial cache. Seed `by_uprn` to hydrate Properties in + a read test; `saved` records writes for an Ingestion-side assertion.""" + + def __init__( + self, by_uprn: Optional[dict[int, PlanningRestrictions]] = None + ) -> None: + self._by_uprn: dict[int, PlanningRestrictions] = dict(by_uprn or {}) + self.saved: list[tuple[int, SpatialReference]] = [] + + def save(self, uprn: int, reference: SpatialReference) -> None: + self.saved.append((uprn, reference)) + self._by_uprn[uprn] = reference.restrictions + + def get_for_uprns(self, uprns: list[int]) -> dict[int, PlanningRestrictions]: + return {uprn: self._by_uprn[uprn] for uprn in uprns if uprn in self._by_uprn} + + class FakePropertyBaselineRepo(PropertyBaselineRepository): def __init__(self) -> None: self.saved: list[tuple[PropertyBaselinePerformance, int]] = [] @@ -170,6 +191,7 @@ class FakeUnitOfWork(UnitOfWork): property: FakePropertyRepo, epc: Optional[FakeEpcRepo] = None, solar: Optional[FakeSolarRepo] = None, + spatial: Optional[FakeSpatialRepo] = None, property_baseline: Optional[FakePropertyBaselineRepo] = None, scenario: Optional[FakeScenarioRepository] = None, product: Optional[ProductRepository] = None, @@ -178,6 +200,7 @@ class FakeUnitOfWork(UnitOfWork): self.property = property self.epc = epc or FakeEpcRepo() self.solar = solar or FakeSolarRepo() + self.spatial = spatial or FakeSpatialRepo() self.property_baseline = property_baseline or FakePropertyBaselineRepo() self.scenario = scenario or FakeScenarioRepository() self.product = product or _UnsetProductRepo() diff --git a/tests/repositories/test_unit_of_work.py b/tests/repositories/test_unit_of_work.py index e3ee2f73..5008d4c6 100644 --- a/tests/repositories/test_unit_of_work.py +++ b/tests/repositories/test_unit_of_work.py @@ -13,6 +13,7 @@ from repositories.plan.plan_repository import PlanRepository from repositories.postgres_unit_of_work import PostgresUnitOfWork from repositories.product.product_repository import ProductRepository from repositories.scenario.scenario_repository import ScenarioRepository +from repositories.spatial.spatial_repository import SpatialRepository def _session_factory(db_engine: Engine) -> Callable[[], Session]: @@ -75,6 +76,16 @@ def test_unit_exposes_the_modelling_repos_bound_to_its_session( assert isinstance(uow.plan, PlanRepository) +def test_unit_exposes_the_spatial_cache_repo_bound_to_its_session( + db_engine: Engine, +) -> None: + # Arrange / Act + with PostgresUnitOfWork(_session_factory(db_engine)) as uow: + # Assert — Ingestion writes the OS spatial reference cache through the + # same unit it persists the EPC/solar with (ADR-0020). + assert isinstance(uow.spatial, SpatialRepository) + + def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) -> None: # Arrange new_unit = lambda: PostgresUnitOfWork(_session_factory(db_engine))