mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +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,
|
EpcComparablePropertiesRepository,
|
||||||
SkippedCohortCert,
|
SkippedCohortCert,
|
||||||
)
|
)
|
||||||
|
from repositories.epc.epc_postgres_repository import EpcPostgresRepository
|
||||||
from repositories.geospatial.geospatial_s3_repository import (
|
from repositories.geospatial.geospatial_s3_repository import (
|
||||||
GeospatialS3Repository,
|
GeospatialS3Repository,
|
||||||
ParquetReader,
|
ParquetReader,
|
||||||
|
|
@ -507,6 +508,17 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
|
||||||
list(set(uprns.values()))
|
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()
|
read_session.close()
|
||||||
|
|
||||||
# Each Property models in its own child SubTask (failures isolated here),
|
# 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
|
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))
|
overrides = overlays_from(overrides_reader.overrides_for(pid))
|
||||||
predicted_epc: Optional[EpcPropertyData] = None
|
predicted_epc: Optional[EpcPropertyData] = None
|
||||||
|
|
||||||
|
|
@ -551,17 +568,24 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
|
||||||
logger.info(
|
logger.info(
|
||||||
f"property={pid} no lodged EPC — attempting prediction"
|
f"property={pid} no lodged EPC — attempting prediction"
|
||||||
)
|
)
|
||||||
predicted_epc = _predict_epc(
|
stored_predicted = stored_predicted_epcs.get(pid)
|
||||||
property_id=pid,
|
if not repredict_epc and stored_predicted is not None:
|
||||||
uprn=uprn,
|
logger.info(
|
||||||
postcode=postcode,
|
f"property={pid} using stored predicted EPC (repredict_epc=False)"
|
||||||
portfolio_id=portfolio_id,
|
)
|
||||||
attributes_reader=prediction_attrs_reader,
|
predicted_epc = stored_predicted
|
||||||
coordinates=coordinates,
|
else:
|
||||||
cohort_for=_get_cohort,
|
predicted_epc = _predict_epc(
|
||||||
broaden=_broaden,
|
property_id=pid,
|
||||||
predictor=predictor,
|
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(
|
effective_epc = Property(
|
||||||
identity=PropertyIdentity(
|
identity=PropertyIdentity(
|
||||||
portfolio_id=portfolio_id,
|
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)
|
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
|
# Dry-run
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue