mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
refactor(property-baseline): rename baseline → property_baseline aggregate (PR #1139 review)
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 <noreply@anthropic.com>
This commit is contained in:
parent
8685f8ba3a
commit
c3691d9af2
25 changed files with 148 additions and 148 deletions
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|---|---|---|
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
"""
|
||||
|
|
@ -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),
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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]: ...
|
||||
|
|
@ -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: ...
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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:
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue