Handler pre-fetches stored EPCs and routes per-property via refetch_epc flag 🟩

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-06-26 10:13:51 +00:00
parent c51ca47467
commit a940c94b33
2 changed files with 222 additions and 12 deletions

View file

@ -92,6 +92,7 @@ from repositories.comparable_properties.epc_comparable_properties_repository imp
EpcComparablePropertiesRepository,
SkippedCohortCert,
)
from repositories.epc.epc_postgres_repository import EpcPostgresRepository
from repositories.geospatial.geospatial_s3_repository import (
GeospatialS3Repository,
ParquetReader,
@ -507,6 +508,17 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
list(set(uprns.values()))
)
)
epc_repo = EpcPostgresRepository(read_session)
stored_lodged_epcs: dict[int, EpcPropertyData] = (
epc_repo.get_for_properties(property_ids)
if not refetch_epc
else {}
)
stored_predicted_epcs: dict[int, EpcPropertyData] = (
epc_repo.get_predicted_for_properties(property_ids)
if not repredict_epc
else {}
)
read_session.close()
# Each Property models in its own child SubTask (failures isolated here),
@ -531,7 +543,12 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
spatial.coordinates if spatial is not None else None
)
epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn)
stored_lodged = stored_lodged_epcs.get(pid)
if not refetch_epc and stored_lodged is not None:
logger.info(f"property={pid} using stored lodged EPC (refetch_epc=False)")
epc: Optional[EpcPropertyData] = stored_lodged
else:
epc = epc_client.get_by_uprn(uprn)
overrides = overlays_from(overrides_reader.overrides_for(pid))
predicted_epc: Optional[EpcPropertyData] = None
@ -551,17 +568,24 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
logger.info(
f"property={pid} no lodged EPC — attempting prediction"
)
predicted_epc = _predict_epc(
property_id=pid,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
attributes_reader=prediction_attrs_reader,
coordinates=coordinates,
cohort_for=_get_cohort,
broaden=_broaden,
predictor=predictor,
)
stored_predicted = stored_predicted_epcs.get(pid)
if not repredict_epc and stored_predicted is not None:
logger.info(
f"property={pid} using stored predicted EPC (repredict_epc=False)"
)
predicted_epc = stored_predicted
else:
predicted_epc = _predict_epc(
property_id=pid,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
attributes_reader=prediction_attrs_reader,
coordinates=coordinates,
cohort_for=_get_cohort,
broaden=_broaden,
predictor=predictor,
)
effective_epc = Property(
identity=PropertyIdentity(
portfolio_id=portfolio_id,

View file

@ -1107,6 +1107,192 @@ def test_cohort_cache_prevents_duplicate_candidates_for_calls() -> None:
MockCandidates.return_value.candidates_for.assert_called_once_with(POSTCODE)
# ---------------------------------------------------------------------------
# refetch_epc flag
# ---------------------------------------------------------------------------
def test_refetch_epc_false_with_stored_epc_skips_api_call() -> None:
"""refetch_epc=False + stored lodged EPC present: EpcClientService.get_by_uprn
is never called; the stored EPC is used and reaches run_modelling."""
# Arrange
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
stored_epc = MagicMock()
mock_plan = _plan_mock()
mock_uow = MagicMock()
with ExitStack() as stack:
stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV))
stack.enter_context(
patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine)
)
mock_epc_client = stack.enter_context(
patch("applications.modelling_e2e.handler.EpcClientService")
).return_value
mock_epc_client.get_by_uprn.return_value = MagicMock() # would be called if flag ignored
stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository"))
stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient"))
stack.enter_context(
patch("applications.modelling_e2e.handler._spatial_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler.overlays_from", return_value=[])
)
stack.enter_context(
patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader")
).return_value.overrides_for_many.return_value = {}
stack.enter_context(
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()]
stack.enter_context(
patch("applications.modelling_e2e.handler.catalogue_snapshot_with_off_catalogue_overrides")
)
stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
mock_run_modelling = stack.enter_context(
patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan)
)
# Bulk-read of stored lodged EPCs returns the stored EPC for this property
stack.enter_context(
patch("applications.modelling_e2e.handler.EpcPostgresRepository")
).return_value.get_for_properties.return_value = {PROPERTY_ID: stored_epc}
MockUoW = stack.enter_context(
patch("applications.modelling_e2e.handler.PostgresUnitOfWork")
)
MockUoW.return_value.__enter__.return_value = mock_uow
MockUoW.return_value.__exit__.return_value = False
# Act
_call_handler({**_BODY, "refetch_epc": False})
# Assert — API not called; stored EPC flows into run_modelling
mock_epc_client.get_by_uprn.assert_not_called()
mock_run_modelling.assert_called_once()
# Stored lodged EPC is persisted in the lodged slot
mock_uow.epc.save.assert_called_once_with(
stored_epc, property_id=PROPERTY_ID, portfolio_id=PORTFOLIO_ID
)
def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None:
"""refetch_epc=False + no stored lodged EPC: handler falls back to the live
EPC API call rather than silently skipping the property."""
# Arrange
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
live_epc = MagicMock()
mock_plan = _plan_mock()
mock_uow = MagicMock()
with ExitStack() as stack:
stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV))
stack.enter_context(
patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine)
)
mock_epc_client = stack.enter_context(
patch("applications.modelling_e2e.handler.EpcClientService")
).return_value
mock_epc_client.get_by_uprn.return_value = live_epc
stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository"))
stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient"))
stack.enter_context(
patch("applications.modelling_e2e.handler._spatial_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler.overlays_from", return_value=[])
)
stack.enter_context(
patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader")
).return_value.overrides_for_many.return_value = {}
stack.enter_context(
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()]
stack.enter_context(
patch("applications.modelling_e2e.handler.catalogue_snapshot_with_off_catalogue_overrides")
)
stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
stack.enter_context(
patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan)
)
# No stored lodged EPC for this property
stack.enter_context(
patch("applications.modelling_e2e.handler.EpcPostgresRepository")
).return_value.get_for_properties.return_value = {}
MockUoW = stack.enter_context(
patch("applications.modelling_e2e.handler.PostgresUnitOfWork")
)
MockUoW.return_value.__enter__.return_value = mock_uow
MockUoW.return_value.__exit__.return_value = False
# Act
_call_handler({**_BODY, "refetch_epc": False})
# Assert — API was called as fallback; live EPC is persisted
mock_epc_client.get_by_uprn.assert_called_once_with(UPRN)
mock_uow.epc.save.assert_called_once_with(
live_epc, property_id=PROPERTY_ID, portfolio_id=PORTFOLIO_ID
)
def test_refetch_epc_true_always_calls_api_even_if_stored_epc_exists() -> None:
"""refetch_epc=True (default): EpcClientService.get_by_uprn is called even
when a stored lodged EPC exists existing behaviour is preserved."""
# Arrange
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
live_epc = MagicMock()
mock_plan = _plan_mock()
mock_uow = MagicMock()
with ExitStack() as stack:
stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV))
stack.enter_context(
patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine)
)
mock_epc_client = stack.enter_context(
patch("applications.modelling_e2e.handler.EpcClientService")
).return_value
mock_epc_client.get_by_uprn.return_value = live_epc
stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository"))
stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient"))
stack.enter_context(
patch("applications.modelling_e2e.handler._spatial_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None)
)
stack.enter_context(
patch("applications.modelling_e2e.handler.overlays_from", return_value=[])
)
stack.enter_context(
patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader")
).return_value.overrides_for_many.return_value = {}
stack.enter_context(
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()]
stack.enter_context(
patch("applications.modelling_e2e.handler.catalogue_snapshot_with_off_catalogue_overrides")
)
stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
stack.enter_context(
patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan)
)
MockUoW = stack.enter_context(
patch("applications.modelling_e2e.handler.PostgresUnitOfWork")
)
MockUoW.return_value.__enter__.return_value = mock_uow
MockUoW.return_value.__exit__.return_value = False
# Act — default refetch_epc=True (not setting the flag at all)
_call_handler(_BODY)
# Assert — API was called regardless
mock_epc_client.get_by_uprn.assert_called_once_with(UPRN)
# ---------------------------------------------------------------------------
# Dry-run
# ---------------------------------------------------------------------------