diff --git a/domain/property_baseline/calculator_rebaseliner.py b/domain/property_baseline/calculator_rebaseliner.py index b2443784..cbfaace7 100644 --- a/domain/property_baseline/calculator_rebaseliner.py +++ b/domain/property_baseline/calculator_rebaseliner.py @@ -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( diff --git a/domain/sap10_calculator/calculator.py b/domain/sap10_calculator/calculator.py index 47366741..43226da1 100644 --- a/domain/sap10_calculator/calculator.py +++ b/domain/sap10_calculator/calculator.py @@ -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. diff --git a/tests/domain/property_baseline/test_calculator_rebaseliner.py b/tests/domain/property_baseline/test_calculator_rebaseliner.py index ea1230fc..f22e152f 100644 --- a/tests/domain/property_baseline/test_calculator_rebaseliner.py +++ b/tests/domain/property_baseline/test_calculator_rebaseliner.py @@ -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)