mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
refactor(baseline): SapCalculator ABC replaces the Calculator Protocol
PR feedback: prefer an abstract base the calculator inherits from over a structural Protocol. Define `SapCalculator(ABC)` in the calculator package (the engine owns its own contract) and have `Sap10Calculator` inherit it; a future methodology is another subclass. Placing the ABC with the engine — not in property_baseline — keeps the dependency pointing consumer -> engine (sap10_calculator imports nothing from property_baseline). Consistent with the repo's existing port convention (FuelRatesRepository(ABC)). CalculatorRebaseliner keeps its reference to SapCalculator type-only (under TYPE_CHECKING), so the module still does not import the calculator at runtime. Test fakes now inherit the ABC since structural conformance no longer applies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d33d46fb6d
commit
298755fbe0
3 changed files with 22 additions and 14 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional, Protocol
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from datatypes.epc.domain.epc import Epc
|
||||
from domain.property_baseline.performance import Performance
|
||||
|
|
@ -9,7 +9,7 @@ from domain.property_baseline.rebaseliner import Rebaseliner, RebaselineReason
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.sap10_calculator.calculator import SapResult
|
||||
from domain.sap10_calculator.calculator import SapCalculator, SapResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -22,13 +22,6 @@ _REL_TOL = 0.01
|
|||
_KG_PER_TONNE = 1000.0
|
||||
|
||||
|
||||
class Calculator(Protocol):
|
||||
"""The slice of `Sap10Calculator` the rebaseliner needs — `Sap10Calculator`
|
||||
satisfies it structurally, so this module does not import the calculator."""
|
||||
|
||||
def calculate(self, epc: "EpcPropertyData") -> "SapResult": ...
|
||||
|
||||
|
||||
def performance_from_sap_result(result: "SapResult") -> Performance:
|
||||
"""The four rated quantities, read off a `SapResult`: band derived from the
|
||||
score, CO2 converted kg→tonnes, PEUI rounded to the lodged integer scale."""
|
||||
|
|
@ -58,7 +51,7 @@ class CalculatorRebaseliner(Rebaseliner):
|
|||
is fixed immediately.
|
||||
"""
|
||||
|
||||
def __init__(self, calculator: Calculator) -> None:
|
||||
def __init__(self, calculator: "SapCalculator") -> None:
|
||||
self._calculator = calculator
|
||||
|
||||
def rebaseline(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ Appendix L + U. RdSAP10 Table 32 (p.95) for fuel prices/CO2/PE factors.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Final, Optional, TYPE_CHECKING
|
||||
|
||||
|
|
@ -751,7 +752,21 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
)
|
||||
|
||||
|
||||
class Sap10Calculator:
|
||||
class SapCalculator(ABC):
|
||||
"""The contract a SAP calculator satisfies: an `EpcPropertyData` in, a
|
||||
typed `SapResult` out. `Sap10Calculator` is the SAP 10.2 implementation;
|
||||
a future methodology (e.g. SAP 10.3 / a successor) is another subclass.
|
||||
|
||||
Consumers (e.g. `CalculatorRebaseliner`) depend on this abstraction, not
|
||||
on a concrete calculator — so the engine can be swapped without touching
|
||||
them.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def calculate(self, epc: "EpcPropertyData") -> SapResult: ...
|
||||
|
||||
|
||||
class Sap10Calculator(SapCalculator):
|
||||
"""Deterministic SAP 10.2 calculator entry point. Maps an
|
||||
`EpcPropertyData` to typed `CalculatorInputs` via the RdSAP-driven
|
||||
`cert_to_inputs` mapper and runs the 12-month worksheet loop.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from datatypes.epc.domain.epc import Epc
|
|||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.property_baseline.calculator_rebaseliner import CalculatorRebaseliner
|
||||
from domain.property_baseline.performance import Performance
|
||||
from domain.sap10_calculator.calculator import SapResult
|
||||
from domain.sap10_calculator.calculator import SapCalculator, SapResult
|
||||
from domain.sap10_calculator.exceptions import UnmappedSapCode
|
||||
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ def _sap_result(
|
|||
)
|
||||
|
||||
|
||||
class _StubCalculator:
|
||||
class _StubCalculator(SapCalculator):
|
||||
def __init__(self, result: SapResult) -> None:
|
||||
self._result = result
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ def test_a_10_2_cert_logs_divergence_when_the_calculator_disagrees(
|
|||
|
||||
def test_a_calculator_raise_propagates_and_aborts() -> None:
|
||||
# Arrange — the calculator is load-bearing, so a raise is not swallowed.
|
||||
class _Raising:
|
||||
class _Raising(SapCalculator):
|
||||
def calculate(self, epc: EpcPropertyData) -> SapResult:
|
||||
raise UnmappedSapCode("heat_emitter_type", 99)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue