From c3691d9af2c0ef97d3b268eae1fcea0d3811f753 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 1 Jun 2026 14:54:59 +0000 Subject: [PATCH] =?UTF-8?q?refactor(property-baseline):=20rename=20baselin?= =?UTF-8?q?e=20=E2=86=92=20property=5Fbaseline=20aggregate=20(PR=20#1139?= =?UTF-8?q?=20review)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wholesale rename of the Baseline aggregate to PropertyBaseline for clarity / to disambiguate from baselines that appear elsewhere in Modelling. Scoped to this aggregate only — the distinct Rebaselining term (rebaseline_reason, StubRebaseliner, RebaselineNotImplemented) is deliberately untouched. - domain/baseline → domain/property_baseline; BaselinePerformance → PropertyBaselinePerformance. - repositories/baseline → repositories/property_baseline; BaselineRepository / BaselinePostgresRepository → PropertyBaseline*. - orchestration/baseline_orchestrator.py → property_baseline_orchestrator.py; BaselineOrchestrator → PropertyBaselineOrchestrator. BaselineStage → PropertyBaselineStage. - infrastructure/postgres: baseline_performance_table.py → property_baseline_performance_table.py; table `baseline_performance` → `property_baseline_performance`; Model renamed. - UnitOfWork attribute `.baseline` → `.property_baseline`. - Docs: ADR-0004 references + migration doc (renamed to property-baseline-performance-table.md) updated. CONTEXT.md glossary term ("Baseline Performance") left as-is pending a ubiquitous-language call (raised on the PR). 123 tests pass; pyright strict clean (only the unrelated pre-existing moto import errors remain). Co-Authored-By: Claude Opus 4.8 --- applications/ara_first_run/handler.py | 6 +-- ...eline-performance-lodged-effective-pair.md | 14 +++--- ...=> property-baseline-performance-table.md} | 8 ++-- .../__init__.py | 0 .../performance.py | 0 .../property_baseline_performance.py} | 6 +-- .../rebaseliner.py | 4 +- ...=> property_baseline_performance_table.py} | 22 +++++----- orchestration/first_run_pipeline.py | 4 +- ...r.py => property_baseline_orchestrator.py} | 12 +++--- .../baseline/baseline_postgres_repository.py | 43 ------------------- repositories/postgres_unit_of_work.py | 6 +-- .../__init__.py | 0 .../property_baseline_postgres_repository.py | 43 +++++++++++++++++++ .../property_baseline_repository.py} | 10 ++--- repositories/unit_of_work.py | 4 +- .../__init__.py | 0 .../test_performance.py | 2 +- .../test_rebaseliner.py | 4 +- tests/orchestration/fakes.py | 16 +++---- .../test_first_run_pipeline_integration.py | 20 ++++----- ...=> test_property_baseline_orchestrator.py} | 28 ++++++------ .../__init__.py | 0 ..._property_baseline_postgres_repository.py} | 24 +++++------ tests/repositories/test_unit_of_work.py | 20 ++++----- 25 files changed, 148 insertions(+), 148 deletions(-) rename docs/migrations/{baseline-performance-table.md => property-baseline-performance-table.md} (87%) rename domain/{baseline => property_baseline}/__init__.py (100%) rename domain/{baseline => property_baseline}/performance.py (100%) rename domain/{baseline/baseline_performance.py => property_baseline/property_baseline_performance.py} (84%) rename domain/{baseline => property_baseline}/rebaseliner.py (94%) rename infrastructure/postgres/{baseline_performance_table.py => property_baseline_performance_table.py} (75%) rename orchestration/{baseline_orchestrator.py => property_baseline_orchestrator.py} (86%) delete mode 100644 repositories/baseline/baseline_postgres_repository.py rename repositories/{baseline => property_baseline}/__init__.py (100%) create mode 100644 repositories/property_baseline/property_baseline_postgres_repository.py rename repositories/{baseline/baseline_repository.py => property_baseline/property_baseline_repository.py} (52%) rename tests/domain/{baseline => property_baseline}/__init__.py (100%) rename tests/domain/{baseline => property_baseline}/test_performance.py (92%) rename tests/domain/{baseline => property_baseline}/test_rebaseliner.py (90%) rename tests/orchestration/{test_baseline_orchestrator.py => test_property_baseline_orchestrator.py} (74%) rename tests/repositories/{baseline => property_baseline}/__init__.py (100%) rename tests/repositories/{baseline/test_baseline_postgres_repository.py => property_baseline/test_property_baseline_postgres_repository.py} (73%) diff --git a/applications/ara_first_run/handler.py b/applications/ara_first_run/handler.py index f9cb6be7..147bf066 100644 --- a/applications/ara_first_run/handler.py +++ b/applications/ara_first_run/handler.py @@ -10,10 +10,10 @@ from sqlmodel import Session from applications.ara_first_run.ara_first_run_trigger_body import ( AraFirstRunTriggerBody, ) -from domain.baseline.rebaseliner import StubRebaseliner +from domain.property_baseline.rebaseliner import StubRebaseliner from infrastructure.postgres.config import PostgresConfig from infrastructure.postgres.engine import make_engine -from orchestration.baseline_orchestrator import BaselineOrchestrator +from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator from orchestration.first_run_pipeline import FirstRunPipeline from orchestration.ingestion_orchestrator import ( EpcFetcher, @@ -78,7 +78,7 @@ def build_first_run_pipeline( geospatial_repo=geospatial_repo, solar_fetcher=solar_fetcher, ), - baseline=BaselineOrchestrator( + baseline=PropertyBaselineOrchestrator( unit_of_work=unit_of_work, rebaseliner=StubRebaseliner(), ), diff --git a/docs/adr/0004-baseline-performance-lodged-effective-pair.md b/docs/adr/0004-baseline-performance-lodged-effective-pair.md index ba275473..fc27be7d 100644 --- a/docs/adr/0004-baseline-performance-lodged-effective-pair.md +++ b/docs/adr/0004-baseline-performance-lodged-effective-pair.md @@ -1,27 +1,27 @@ -# `BaselinePerformance` stores both lodged and effective values +# `PropertyBaselinePerformance` stores both lodged and effective values -A Property's current performance has two states we care about: the rating that was lodged on the government register (the "lodged" SAP / band / carbon / heat) and the rating produced by the modelling pipeline against the current Effective EPC (the "effective" values, which may have been rebaselined by ML when the EPC was pre-SAP10 or when Landlord Overrides / Site Notes changed physical state). We considered storing a single set of values — the rebaselined-if-needed-otherwise-lodged figures — and rejected that. Both are stored as a pair on every `BaselinePerformance`, equal when no rebaselining trigger fires. +A Property's current performance has two states we care about: the rating that was lodged on the government register (the "lodged" SAP / band / carbon / heat) and the rating produced by the modelling pipeline against the current Effective EPC (the "effective" values, which may have been rebaselined by ML when the EPC was pre-SAP10 or when Landlord Overrides / Site Notes changed physical state). We considered storing a single set of values — the rebaselined-if-needed-otherwise-lodged figures — and rejected that. Both are stored as a pair on every `PropertyBaselinePerformance`, equal when no rebaselining trigger fires. The pair lets the FE show "this is what the gov register says vs this is the SAP10-equivalent we modelled against" side by side without a second query, and keeps the audit trail clean: a user looking at a property's plan can see exactly which figure drove the recommendation pipeline. Storing only one set forces a downstream consumer to recompute the missing one from raw EPC fields when it needs both, which is the kind of derivation creep we want to keep out of the FE. -The cost is a wider row + the discipline that **every** `BaselinePerformance` populates both halves, even when they're equal. Annual kWh, fuel split and bills are not paired — they are always derived deterministically by `EpcEnergyDerivationService` against the Effective state, because the EPC's recorded cost fields use fuel rates pinned to the inspection date and the UCL correction depends on the modelled band. +The cost is a wider row + the discipline that **every** `PropertyBaselinePerformance` populates both halves, even when they're equal. Annual kWh, fuel split and bills are not paired — they are always derived deterministically by `EpcEnergyDerivationService` against the Effective state, because the EPC's recorded cost fields use fuel rates pinned to the inspection date and the UCL correction depends on the modelled band. ## Consequences - Reversing this means rewriting every consumer that has learned to read both values. Hard to roll back once the FE depends on the pair. - The rebaseline trigger has two reasons (`pre_sap10`, `physical_state_changed`, or `both`) — store the reason alongside so we know *why* a property was rebaselined when debugging. -### Amendment (2026-05-30, #1135): standalone `baseline_performance` table +### Amendment (2026-05-30, #1135): standalone `property_baseline_performance` table The original consequence read *"`property_details_epc` (or its successor) carries 8 fields instead of 4 for the SAP-equivalent block"* — i.e. the pair as columns on the EPC-details table. That is superseded. `property_details_epc` is being **retired**: it is too tightly coupled to the schema of the legacy EPC API, which the Ara rebuild is moving off. So the pair has no home there. -`BaselinePerformance` instead persists as its **own standalone `baseline_performance` table, one -row per Property**, behind a dedicated `BaselineRepository` port (`save` / `get_for_property`), +`PropertyBaselinePerformance` instead persists as its **own standalone `property_baseline_performance` table, one +row per Property**, behind a dedicated `PropertyBaselineRepository` port (`save` / `get_for_property`), mirroring the EPC slice's repo shape. This is the cleaner model regardless of the retirement: -`BaselinePerformance` is its own aggregate (a Property's current performance), not a detail of any +`PropertyBaselinePerformance` is its own aggregate (a Property's current performance), not a detail of any single EPC. The row is **flat typed columns**, not a JSONB blob, because the FE both surfaces the block and diff --git a/docs/migrations/baseline-performance-table.md b/docs/migrations/property-baseline-performance-table.md similarity index 87% rename from docs/migrations/baseline-performance-table.md rename to docs/migrations/property-baseline-performance-table.md index 24e06179..66864eb9 100644 --- a/docs/migrations/baseline-performance-table.md +++ b/docs/migrations/property-baseline-performance-table.md @@ -1,8 +1,8 @@ -# `baseline_performance` table — FE-owned migration +# `property_baseline_performance` table — FE-owned migration **Context:** Slice 6 (Hestia-Homes/Model#1135) of the `ara_first_run` rebuild. The -`BaselineOrchestrator` establishes a Property's **Baseline Performance** (ADR-0004) and persists it -via a new `BaselineRepository` port. This is a brand-new table — no predecessor. +`PropertyBaselineOrchestrator` establishes a Property's **Baseline Performance** (ADR-0004) and persists it +via a new `PropertyBaselineRepository` port. This is a brand-new table — no predecessor. Per ADR-0004's amendment, the lodged/effective pair does **not** land on `property_details_epc` (which is being retired as too coupled to the legacy EPC-API schema). It lands here, as its own @@ -12,7 +12,7 @@ The SQLModel row is defined in `infrastructure/postgres/` so the ephemeral-Postg via `SQLModel.metadata.create_all`. The **production migration is FE-owned (Drizzle ORM)** — a straight lift-and-shift of the columns below. -## `baseline_performance` — one row per Property +## `property_baseline_performance` — one row per Property | Column | Type | Notes | |---|---|---| diff --git a/domain/baseline/__init__.py b/domain/property_baseline/__init__.py similarity index 100% rename from domain/baseline/__init__.py rename to domain/property_baseline/__init__.py diff --git a/domain/baseline/performance.py b/domain/property_baseline/performance.py similarity index 100% rename from domain/baseline/performance.py rename to domain/property_baseline/performance.py diff --git a/domain/baseline/baseline_performance.py b/domain/property_baseline/property_baseline_performance.py similarity index 84% rename from domain/baseline/baseline_performance.py rename to domain/property_baseline/property_baseline_performance.py index 8db6e05d..8da9bbf2 100644 --- a/domain/baseline/baseline_performance.py +++ b/domain/property_baseline/property_baseline_performance.py @@ -2,12 +2,12 @@ from __future__ import annotations from dataclasses import dataclass -from domain.baseline.performance import Performance -from domain.baseline.rebaseliner import RebaselineReason +from domain.property_baseline.performance import Performance +from domain.property_baseline.rebaseliner import RebaselineReason @dataclass(frozen=True) -class BaselinePerformance: +class PropertyBaselinePerformance: """A Property's current performance aggregate (CONTEXT.md, ADR-0004). Holds both halves — ``lodged`` (what the gov register says) and diff --git a/domain/baseline/rebaseliner.py b/domain/property_baseline/rebaseliner.py similarity index 94% rename from domain/baseline/rebaseliner.py rename to domain/property_baseline/rebaseliner.py index 40034a58..a80552ea 100644 --- a/domain/baseline/rebaseliner.py +++ b/domain/property_baseline/rebaseliner.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import Literal from datatypes.epc.domain.epc_property_data import EpcPropertyData -from domain.baseline.performance import Performance +from domain.property_baseline.performance import Performance RebaselineReason = Literal["none", "pre_sap10", "physical_state_changed", "both"] @@ -29,7 +29,7 @@ class Rebaseliner(ABC): Rebaselining (CONTEXT.md) re-predicts the rated quantities via ML when the EPC was lodged pre-SAP10 or its physical state diverged from the lodged EPC; otherwise Effective Performance equals Lodged. Injected into the - BaselineOrchestrator (ADR-0011) so the ML adapter can swap in without + PropertyBaselineOrchestrator (ADR-0011) so the ML adapter can swap in without touching the orchestrator, and so the single-property re-score-on-override flow reuses the same port. """ diff --git a/infrastructure/postgres/baseline_performance_table.py b/infrastructure/postgres/property_baseline_performance_table.py similarity index 75% rename from infrastructure/postgres/baseline_performance_table.py rename to infrastructure/postgres/property_baseline_performance_table.py index fad4be9d..f43d9f3e 100644 --- a/infrastructure/postgres/baseline_performance_table.py +++ b/infrastructure/postgres/property_baseline_performance_table.py @@ -5,20 +5,20 @@ from typing import ClassVar, Optional, cast from sqlmodel import Field, SQLModel from datatypes.epc.domain.epc import Epc -from domain.baseline.baseline_performance import BaselinePerformance -from domain.baseline.performance import Performance -from domain.baseline.rebaseliner import RebaselineReason +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from domain.property_baseline.performance import Performance +from domain.property_baseline.rebaseliner import RebaselineReason -class BaselinePerformanceModel(SQLModel, table=True): - """The ``baseline_performance`` row — one per Property (ADR-0004). +class PropertyBaselinePerformanceModel(SQLModel, table=True): + """The ``property_baseline_performance`` row — one per Property (ADR-0004). Flat typed columns (not a JSONB blob) so the FE can both surface the block and query the lodged-vs-effective pair. The production migration is FE-owned - (Drizzle); see docs/migrations/baseline-performance-table.md. + (Drizzle); see docs/migrations/property-baseline-performance-table.md. """ - __tablename__: ClassVar[str] = "baseline_performance" # pyright: ignore[reportIncompatibleVariableOverride] + __tablename__: ClassVar[str] = "property_baseline_performance" # pyright: ignore[reportIncompatibleVariableOverride] id: Optional[int] = Field(default=None, primary_key=True) property_id: int = Field(unique=True, index=True) @@ -40,8 +40,8 @@ class BaselinePerformanceModel(SQLModel, table=True): @classmethod def from_domain( - cls, baseline: BaselinePerformance, property_id: int - ) -> "BaselinePerformanceModel": + cls, baseline: PropertyBaselinePerformance, property_id: int + ) -> "PropertyBaselinePerformanceModel": return cls( property_id=property_id, lodged_sap_score=baseline.lodged.sap_score, @@ -57,8 +57,8 @@ class BaselinePerformanceModel(SQLModel, table=True): water_heating_kwh=baseline.water_heating_kwh, ) - def to_domain(self) -> BaselinePerformance: - return BaselinePerformance( + def to_domain(self) -> PropertyBaselinePerformance: + return PropertyBaselinePerformance( lodged=Performance( sap_score=self.lodged_sap_score, epc_band=Epc(self.lodged_epc_band), diff --git a/orchestration/first_run_pipeline.py b/orchestration/first_run_pipeline.py index 3d642d9e..6d521a35 100644 --- a/orchestration/first_run_pipeline.py +++ b/orchestration/first_run_pipeline.py @@ -29,7 +29,7 @@ class IngestionStage(Protocol): def run(self, property_ids: list[int]) -> None: ... -class BaselineStage(Protocol): +class PropertyBaselineStage(Protocol): """Stage 2 — establishes each Property's Baseline Performance.""" def run(self, property_ids: list[int]) -> None: ... @@ -57,7 +57,7 @@ class FirstRunPipeline: self, *, ingestion: IngestionStage, - baseline: BaselineStage, + baseline: PropertyBaselineStage, modelling: ModellingStage, ) -> None: self._ingestion = ingestion diff --git a/orchestration/baseline_orchestrator.py b/orchestration/property_baseline_orchestrator.py similarity index 86% rename from orchestration/baseline_orchestrator.py rename to orchestration/property_baseline_orchestrator.py index 9a1138c8..df2bf579 100644 --- a/orchestration/baseline_orchestrator.py +++ b/orchestration/property_baseline_orchestrator.py @@ -6,13 +6,13 @@ from datatypes.epc.domain.epc_property_data import ( EpcPropertyData, RenewableHeatIncentive, ) -from domain.baseline.baseline_performance import BaselinePerformance -from domain.baseline.performance import lodged_performance -from domain.baseline.rebaseliner import Rebaseliner +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from domain.property_baseline.performance import lodged_performance +from domain.property_baseline.rebaseliner import Rebaseliner from repositories.unit_of_work import UnitOfWork -class BaselineOrchestrator: +class PropertyBaselineOrchestrator: """Stage 2: establish each Property's Baseline Performance and persist it. Runs the whole batch in **one** Unit of Work and commits once (ADR-0012): @@ -46,14 +46,14 @@ class BaselineOrchestrator: effective_epc, lodged ) rhi = _require_rhi(effective_epc) - baseline = BaselinePerformance( + baseline = PropertyBaselinePerformance( lodged=lodged, effective=effective, rebaseline_reason=reason, space_heating_kwh=rhi.space_heating_kwh, water_heating_kwh=rhi.water_heating_kwh, ) - uow.baseline.save(baseline, property_id) + uow.property_baseline.save(baseline, property_id) uow.commit() diff --git a/repositories/baseline/baseline_postgres_repository.py b/repositories/baseline/baseline_postgres_repository.py deleted file mode 100644 index 7a5b5807..00000000 --- a/repositories/baseline/baseline_postgres_repository.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -from typing import Optional - -from sqlmodel import Session, col, delete, select - -from domain.baseline.baseline_performance import BaselinePerformance -from infrastructure.postgres.baseline_performance_table import ( - BaselinePerformanceModel, -) -from repositories.baseline.baseline_repository import BaselineRepository - - -class BaselinePostgresRepository(BaselineRepository): - """Maps BaselinePerformance to/from the ``baseline_performance`` table.""" - - def __init__(self, session: Session) -> None: - self._session = session - - def save(self, baseline: BaselinePerformance, property_id: int) -> int: - # Idempotent on property_id: a re-run (or re-score) replaces the row - # rather than hitting the unique constraint (ADR-0012). - self._session.exec( # type: ignore[call-overload] - delete(BaselinePerformanceModel).where( - col(BaselinePerformanceModel.property_id) == property_id - ) - ) - row = BaselinePerformanceModel.from_domain(baseline, property_id) - self._session.add(row) - self._session.flush() - if row.id is None: - raise ValueError("baseline_performance row did not receive an id") - return row.id - - def get_for_property( - self, property_id: int - ) -> Optional[BaselinePerformance]: - row = self._session.exec( - select(BaselinePerformanceModel).where( - BaselinePerformanceModel.property_id == property_id - ) - ).first() - return row.to_domain() if row is not None else None diff --git a/repositories/postgres_unit_of_work.py b/repositories/postgres_unit_of_work.py index bd5957e9..da91604b 100644 --- a/repositories/postgres_unit_of_work.py +++ b/repositories/postgres_unit_of_work.py @@ -6,8 +6,8 @@ from typing import Optional from sqlmodel import Session -from repositories.baseline.baseline_postgres_repository import ( - BaselinePostgresRepository, +from repositories.property_baseline.property_baseline_postgres_repository import ( + PropertyBaselinePostgresRepository, ) from repositories.epc.epc_postgres_repository import EpcPostgresRepository from repositories.property.property_postgres_repository import ( @@ -35,7 +35,7 @@ class PostgresUnitOfWork(UnitOfWork): self.property = PropertyPostgresRepository(self._session, epc_repo) self.epc = epc_repo self.solar = SolarPostgresRepository(self._session) - self.baseline = BaselinePostgresRepository(self._session) + self.property_baseline = PropertyBaselinePostgresRepository(self._session) return self def __exit__( diff --git a/repositories/baseline/__init__.py b/repositories/property_baseline/__init__.py similarity index 100% rename from repositories/baseline/__init__.py rename to repositories/property_baseline/__init__.py diff --git a/repositories/property_baseline/property_baseline_postgres_repository.py b/repositories/property_baseline/property_baseline_postgres_repository.py new file mode 100644 index 00000000..113614d9 --- /dev/null +++ b/repositories/property_baseline/property_baseline_postgres_repository.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import Optional + +from sqlmodel import Session, col, delete, select + +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from infrastructure.postgres.property_baseline_performance_table import ( + PropertyBaselinePerformanceModel, +) +from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository + + +class PropertyBaselinePostgresRepository(PropertyBaselineRepository): + """Maps PropertyBaselinePerformance to/from the ``property_baseline_performance`` table.""" + + def __init__(self, session: Session) -> None: + self._session = session + + def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int: + # Idempotent on property_id: a re-run (or re-score) replaces the row + # rather than hitting the unique constraint (ADR-0012). + self._session.exec( # type: ignore[call-overload] + delete(PropertyBaselinePerformanceModel).where( + col(PropertyBaselinePerformanceModel.property_id) == property_id + ) + ) + row = PropertyBaselinePerformanceModel.from_domain(baseline, property_id) + self._session.add(row) + self._session.flush() + if row.id is None: + raise ValueError("property_baseline_performance row did not receive an id") + return row.id + + def get_for_property( + self, property_id: int + ) -> Optional[PropertyBaselinePerformance]: + row = self._session.exec( + select(PropertyBaselinePerformanceModel).where( + PropertyBaselinePerformanceModel.property_id == property_id + ) + ).first() + return row.to_domain() if row is not None else None diff --git a/repositories/baseline/baseline_repository.py b/repositories/property_baseline/property_baseline_repository.py similarity index 52% rename from repositories/baseline/baseline_repository.py rename to repositories/property_baseline/property_baseline_repository.py index 67e430f5..c237f56a 100644 --- a/repositories/baseline/baseline_repository.py +++ b/repositories/property_baseline/property_baseline_repository.py @@ -3,21 +3,21 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import Optional -from domain.baseline.baseline_performance import BaselinePerformance +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance -class BaselineRepository(ABC): +class PropertyBaselineRepository(ABC): """Persists and loads a Property's Baseline Performance. One Baseline Performance per Property (ADR-0004: persisted as one row). The - Postgres adapter writes the standalone ``baseline_performance`` table — not + Postgres adapter writes the standalone ``property_baseline_performance`` table — not columns on the retiring ``property_details_epc``. """ @abstractmethod - def save(self, baseline: BaselinePerformance, property_id: int) -> int: ... + def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int: ... @abstractmethod def get_for_property( self, property_id: int - ) -> Optional[BaselinePerformance]: ... + ) -> Optional[PropertyBaselinePerformance]: ... diff --git a/repositories/unit_of_work.py b/repositories/unit_of_work.py index af5b77f2..cb1cc1d8 100644 --- a/repositories/unit_of_work.py +++ b/repositories/unit_of_work.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from types import TracebackType from typing import Optional -from repositories.baseline.baseline_repository import BaselineRepository +from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository from repositories.epc.epc_repository import EpcRepository from repositories.property.property_repository import PropertyRepository from repositories.solar.solar_repository import SolarRepository @@ -25,7 +25,7 @@ class UnitOfWork(ABC): property: PropertyRepository epc: EpcRepository solar: SolarRepository - baseline: BaselineRepository + property_baseline: PropertyBaselineRepository @abstractmethod def commit(self) -> None: ... diff --git a/tests/domain/baseline/__init__.py b/tests/domain/property_baseline/__init__.py similarity index 100% rename from tests/domain/baseline/__init__.py rename to tests/domain/property_baseline/__init__.py diff --git a/tests/domain/baseline/test_performance.py b/tests/domain/property_baseline/test_performance.py similarity index 92% rename from tests/domain/baseline/test_performance.py rename to tests/domain/property_baseline/test_performance.py index 6e8f080e..9d7011cb 100644 --- a/tests/domain/baseline/test_performance.py +++ b/tests/domain/property_baseline/test_performance.py @@ -2,7 +2,7 @@ from __future__ import annotations from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData -from domain.baseline.performance import Performance, lodged_performance +from domain.property_baseline.performance import Performance, lodged_performance def _epc_with_recorded_performance( diff --git a/tests/domain/baseline/test_rebaseliner.py b/tests/domain/property_baseline/test_rebaseliner.py similarity index 90% rename from tests/domain/baseline/test_rebaseliner.py rename to tests/domain/property_baseline/test_rebaseliner.py index f4ceee70..8f669aed 100644 --- a/tests/domain/baseline/test_rebaseliner.py +++ b/tests/domain/property_baseline/test_rebaseliner.py @@ -6,8 +6,8 @@ import pytest from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData -from domain.baseline.performance import Performance -from domain.baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner +from domain.property_baseline.performance import Performance +from domain.property_baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner def _epc(*, sap_version: Optional[float]) -> EpcPropertyData: diff --git a/tests/orchestration/fakes.py b/tests/orchestration/fakes.py index 24138520..3e2feef0 100644 --- a/tests/orchestration/fakes.py +++ b/tests/orchestration/fakes.py @@ -10,10 +10,10 @@ from types import TracebackType from typing import Any, Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData -from domain.baseline.baseline_performance import BaselinePerformance +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance from domain.property.properties import Properties from domain.property.property import Property -from repositories.baseline.baseline_repository import BaselineRepository +from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository from repositories.epc.epc_repository import EpcRepository from repositories.property.property_repository import PropertyRepository from repositories.solar.solar_repository import SolarRepository @@ -74,17 +74,17 @@ class FakeSolarRepo(SolarRepository): raise NotImplementedError -class FakeBaselineRepo(BaselineRepository): +class FakePropertyBaselineRepo(PropertyBaselineRepository): def __init__(self) -> None: - self.saved: list[tuple[BaselinePerformance, int]] = [] + self.saved: list[tuple[PropertyBaselinePerformance, int]] = [] - def save(self, baseline: BaselinePerformance, property_id: int) -> int: + def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int: self.saved.append((baseline, property_id)) return len(self.saved) def get_for_property( self, property_id: int - ) -> Optional[BaselinePerformance]: # pragma: no cover + ) -> Optional[PropertyBaselinePerformance]: # pragma: no cover raise NotImplementedError @@ -97,12 +97,12 @@ class FakeUnitOfWork(UnitOfWork): property: FakePropertyRepo, epc: Optional[FakeEpcRepo] = None, solar: Optional[FakeSolarRepo] = None, - baseline: Optional[FakeBaselineRepo] = None, + property_baseline: Optional[FakePropertyBaselineRepo] = None, ) -> None: self.property = property self.epc = epc or FakeEpcRepo() self.solar = solar or FakeSolarRepo() - self.baseline = baseline or FakeBaselineRepo() + self.property_baseline = property_baseline or FakePropertyBaselineRepo() self.commits = 0 def __enter__(self) -> "FakeUnitOfWork": diff --git a/tests/orchestration/test_first_run_pipeline_integration.py b/tests/orchestration/test_first_run_pipeline_integration.py index d96351c7..781dcf87 100644 --- a/tests/orchestration/test_first_run_pipeline_integration.py +++ b/tests/orchestration/test_first_run_pipeline_integration.py @@ -18,19 +18,19 @@ from sqlmodel import Session, select from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.mapper import EpcPropertyDataMapper -from domain.baseline.rebaseliner import StubRebaseliner +from domain.property_baseline.rebaseliner import StubRebaseliner from domain.geospatial.coordinates import Coordinates -from infrastructure.postgres.baseline_performance_table import ( - BaselinePerformanceModel, +from infrastructure.postgres.property_baseline_performance_table import ( + PropertyBaselinePerformanceModel, ) from infrastructure.postgres.epc_property_table import EpcPropertyModel from infrastructure.postgres.property_table import PropertyRow -from orchestration.baseline_orchestrator import BaselineOrchestrator +from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator from orchestration.first_run_pipeline import FirstRunPipeline from orchestration.ingestion_orchestrator import IngestionOrchestrator from orchestration.modelling_orchestrator import ModellingOrchestrator -from repositories.baseline.baseline_postgres_repository import ( - BaselinePostgresRepository, +from repositories.property_baseline.property_baseline_postgres_repository import ( + PropertyBaselinePostgresRepository, ) from repositories.geospatial.geospatial_repository import GeospatialRepository from repositories.materials.materials_repository import MaterialsRepository @@ -110,7 +110,7 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun( geospatial_repo=_NoCoordinates(), solar_fetcher=_UnusedSolarFetcher(), ), - baseline=BaselineOrchestrator( + baseline=PropertyBaselineOrchestrator( unit_of_work=unit_of_work, rebaseliner=StubRebaseliner() ), modelling=ModellingOrchestrator( @@ -128,13 +128,13 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun( # property_ids crossed the stage boundary), and the re-run replaced rather # than duplicated either row. with Session(db_engine) as session: - baseline = BaselinePostgresRepository(session).get_for_property(10) + baseline = PropertyBaselinePostgresRepository(session).get_for_property(10) epc_rows = session.exec( select(EpcPropertyModel).where(EpcPropertyModel.property_id == 10) ).all() baseline_rows = session.exec( - select(BaselinePerformanceModel).where( - BaselinePerformanceModel.property_id == 10 + select(PropertyBaselinePerformanceModel).where( + PropertyBaselinePerformanceModel.property_id == 10 ) ).all() diff --git a/tests/orchestration/test_baseline_orchestrator.py b/tests/orchestration/test_property_baseline_orchestrator.py similarity index 74% rename from tests/orchestration/test_baseline_orchestrator.py rename to tests/orchestration/test_property_baseline_orchestrator.py index a18628ec..cb67d176 100644 --- a/tests/orchestration/test_baseline_orchestrator.py +++ b/tests/orchestration/test_property_baseline_orchestrator.py @@ -7,13 +7,13 @@ from datatypes.epc.domain.epc_property_data import ( EpcPropertyData, RenewableHeatIncentive, ) -from domain.baseline.baseline_performance import BaselinePerformance -from domain.baseline.performance import Performance -from domain.baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from domain.property_baseline.performance import Performance +from domain.property_baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner from domain.property.property import Property, PropertyIdentity -from orchestration.baseline_orchestrator import BaselineOrchestrator +from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator from tests.orchestration.fakes import ( - FakeBaselineRepo, + FakePropertyBaselineRepo, FakePropertyRepo, FakeUnitOfWork, ) @@ -39,12 +39,12 @@ def _property(*, sap_version: float) -> Property: def test_run_establishes_persists_and_commits_the_batch_once() -> None: # Arrange - baseline_repo = FakeBaselineRepo() + property_baseline_repo = FakePropertyBaselineRepo() uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(sap_version=10.2)}), - baseline=baseline_repo, + property_baseline=property_baseline_repo, ) - orchestrator = BaselineOrchestrator( + orchestrator = PropertyBaselineOrchestrator( unit_of_work=lambda: uow, rebaseliner=StubRebaseliner() ) @@ -56,9 +56,9 @@ def test_run_establishes_persists_and_commits_the_batch_once() -> None: lodged = Performance( sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180 ) - assert baseline_repo.saved == [ + assert property_baseline_repo.saved == [ ( - BaselinePerformance( + PropertyBaselinePerformance( lodged=lodged, effective=lodged, rebaseline_reason="none", @@ -73,12 +73,12 @@ def test_run_establishes_persists_and_commits_the_batch_once() -> None: def test_run_raises_on_a_pre_sap10_property_and_does_not_commit() -> None: # Arrange — a pre-SAP10 cert needs ML rebaselining, which is not wired yet. - baseline_repo = FakeBaselineRepo() + property_baseline_repo = FakePropertyBaselineRepo() uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(sap_version=9.94)}), - baseline=baseline_repo, + property_baseline=property_baseline_repo, ) - orchestrator = BaselineOrchestrator( + orchestrator = PropertyBaselineOrchestrator( unit_of_work=lambda: uow, rebaseliner=StubRebaseliner() ) @@ -86,5 +86,5 @@ def test_run_raises_on_a_pre_sap10_property_and_does_not_commit() -> None: # committed (all-or-nothing). with pytest.raises(RebaselineNotImplemented): orchestrator.run([10]) - assert baseline_repo.saved == [] + assert property_baseline_repo.saved == [] assert uow.commits == 0 diff --git a/tests/repositories/baseline/__init__.py b/tests/repositories/property_baseline/__init__.py similarity index 100% rename from tests/repositories/baseline/__init__.py rename to tests/repositories/property_baseline/__init__.py diff --git a/tests/repositories/baseline/test_baseline_postgres_repository.py b/tests/repositories/property_baseline/test_property_baseline_postgres_repository.py similarity index 73% rename from tests/repositories/baseline/test_baseline_postgres_repository.py rename to tests/repositories/property_baseline/test_property_baseline_postgres_repository.py index df1da9e8..6395d0f9 100644 --- a/tests/repositories/baseline/test_baseline_postgres_repository.py +++ b/tests/repositories/property_baseline/test_property_baseline_postgres_repository.py @@ -4,14 +4,14 @@ from sqlalchemy import Engine from sqlmodel import Session from datatypes.epc.domain.epc import Epc -from domain.baseline.baseline_performance import BaselinePerformance -from domain.baseline.performance import Performance -from repositories.baseline.baseline_postgres_repository import ( - BaselinePostgresRepository, +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from domain.property_baseline.performance import Performance +from repositories.property_baseline.property_baseline_postgres_repository import ( + PropertyBaselinePostgresRepository, ) -def _baseline() -> BaselinePerformance: +def _baseline() -> PropertyBaselinePerformance: lodged = Performance( sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180 ) @@ -20,7 +20,7 @@ def _baseline() -> BaselinePerformance: effective = Performance( sap_score=64, epc_band=Epc.D, co2_emissions=2.4, primary_energy_intensity=210 ) - return BaselinePerformance( + return PropertyBaselinePerformance( lodged=lodged, effective=effective, rebaseline_reason="pre_sap10", @@ -33,12 +33,12 @@ def test_baseline_performance_round_trips(db_engine: Engine) -> None: # Arrange baseline = _baseline() with Session(db_engine) as session: - BaselinePostgresRepository(session).save(baseline, property_id=10) + PropertyBaselinePostgresRepository(session).save(baseline, property_id=10) session.commit() # Act with Session(db_engine) as session: - loaded = BaselinePostgresRepository(session).get_for_property(10) + loaded = PropertyBaselinePostgresRepository(session).get_for_property(10) # Assert — the full aggregate reconstructs, both halves intact. assert loaded == baseline @@ -50,7 +50,7 @@ def test_resaving_baseline_for_a_property_replaces_rather_than_duplicating( # Arrange — a re-run re-establishes the same property's baseline with a # different rating. first = _baseline() - rerun = BaselinePerformance( + rerun = PropertyBaselinePerformance( lodged=Performance( sap_score=80, epc_band=Epc.B, @@ -71,21 +71,21 @@ def test_resaving_baseline_for_a_property_replaces_rather_than_duplicating( # Act — save twice for the same property_id (must not hit the unique # constraint, must overwrite). with Session(db_engine) as session: - repo = BaselinePostgresRepository(session) + repo = PropertyBaselinePostgresRepository(session) repo.save(first, property_id=10) repo.save(rerun, property_id=10) session.commit() # Assert with Session(db_engine) as session: - loaded = BaselinePostgresRepository(session).get_for_property(10) + loaded = PropertyBaselinePostgresRepository(session).get_for_property(10) assert loaded == rerun def test_get_for_property_returns_none_when_absent(db_engine: Engine) -> None: # Arrange / Act with Session(db_engine) as session: - loaded = BaselinePostgresRepository(session).get_for_property(999) + loaded = PropertyBaselinePostgresRepository(session).get_for_property(999) # Assert assert loaded is None diff --git a/tests/repositories/test_unit_of_work.py b/tests/repositories/test_unit_of_work.py index 2851edaf..03018562 100644 --- a/tests/repositories/test_unit_of_work.py +++ b/tests/repositories/test_unit_of_work.py @@ -7,8 +7,8 @@ from sqlalchemy import Engine from sqlmodel import Session from datatypes.epc.domain.epc import Epc -from domain.baseline.baseline_performance import BaselinePerformance -from domain.baseline.performance import Performance +from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance +from domain.property_baseline.performance import Performance from repositories.postgres_unit_of_work import PostgresUnitOfWork @@ -16,11 +16,11 @@ def _session_factory(db_engine: Engine) -> Callable[[], Session]: return lambda: Session(db_engine) -def _baseline() -> BaselinePerformance: +def _baseline() -> PropertyBaselinePerformance: perf = Performance( sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180 ) - return BaselinePerformance( + return PropertyBaselinePerformance( lodged=perf, effective=perf, rebaseline_reason="none", @@ -36,12 +36,12 @@ def test_committed_work_is_visible_to_a_later_unit(db_engine: Engine) -> None: # Act with new_unit() as uow: - uow.baseline.save(baseline, property_id=10) + uow.property_baseline.save(baseline, property_id=10) uow.commit() # Assert — a fresh unit reads back what the first one committed. with new_unit() as uow: - loaded = uow.baseline.get_for_property(10) + loaded = uow.property_baseline.get_for_property(10) assert loaded == baseline @@ -52,12 +52,12 @@ def test_an_exception_in_the_block_rolls_the_batch_back(db_engine: Engine) -> No # Act — a property mid-batch raises after a write but before commit. with pytest.raises(RuntimeError, match="boom"): with new_unit() as uow: - uow.baseline.save(_baseline(), property_id=10) + uow.property_baseline.save(_baseline(), property_id=10) raise RuntimeError("boom") # Assert — nothing from the aborted batch is persisted. with new_unit() as uow: - assert uow.baseline.get_for_property(10) is None + assert uow.property_baseline.get_for_property(10) is None def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) -> None: @@ -66,8 +66,8 @@ def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) -> # Act — write but never commit. with new_unit() as uow: - uow.baseline.save(_baseline(), property_id=10) + uow.property_baseline.save(_baseline(), property_id=10) # Assert with new_unit() as uow: - assert uow.baseline.get_for_property(10) is None + assert uow.property_baseline.get_for_property(10) is None