mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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:
parent
c51ca47467
commit
a940c94b33
2 changed files with 222 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue