revert epc timeout to 10s

This commit is contained in:
Khalim Conn-Kowlessar 2026-06-23 11:19:23 +00:00
parent 52e5a4d402
commit c3422704f5
3 changed files with 20 additions and 27 deletions

View file

@ -12,9 +12,10 @@ When no lodged EPC is found, EPC Prediction (Path 3, ADR-0031) synthesises one
from the postcode cohort. ``_cohort_cache`` is module-level so warm Lambda from the postcode cohort. ``_cohort_cache`` is module-level so warm Lambda
containers re-processing the same postcode avoid redundant fetches. containers re-processing the same postcode avoid redundant fetches.
``secondary_heating_removal`` is excluded unconditionally: the live ``material`` All Measure Types are considered: pricing goes through
catalogue does not yet carry this measure type, causing a crash during catalogue ``catalogue_with_off_catalogue_overrides`` so the measures the live ``material``
reads for properties with a lodged secondary heater. catalogue cannot supply (``secondary_heating_removal``, the glazing and heating
gaps) are priced from the committed off-catalogue overlay instead of crashing.
DB engine is module-scoped so the connection pool is reused across warm DB engine is module-scoped so the connection pool is reused across warm
invocations (ADR-0012). invocations (ADR-0012).
@ -45,7 +46,6 @@ from domain.epc_prediction.prediction_target import build_prediction_target
from domain.geospatial.coordinates import Coordinates from domain.geospatial.coordinates import Coordinates
from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.geospatial.planning_restrictions import PlanningRestrictions
from domain.geospatial.spatial_reference import SpatialReference from domain.geospatial.spatial_reference import SpatialReference
from domain.modelling.measure_type import MeasureType
from domain.property.property import Property, PropertyIdentity from domain.property.property import Property, PropertyIdentity
from domain.tasks.tasks import Source from domain.tasks.tasks import Source
from harness.console import run_modelling from harness.console import run_modelling
@ -67,7 +67,9 @@ from repositories.geospatial.geospatial_s3_repository import (
ParquetReader, ParquetReader,
) )
from repositories.postgres_unit_of_work import PostgresUnitOfWork from repositories.postgres_unit_of_work import PostgresUnitOfWork
from repositories.product.product_postgres_repository import ProductPostgresRepository from repositories.product.composite_product_repository import (
catalogue_with_off_catalogue_overrides,
)
from repositories.property.landlord_override_overlays import overlays_from from repositories.property.landlord_override_overlays import overlays_from
from repositories.property.override_backed_prediction_attributes_reader import ( from repositories.property.override_backed_prediction_attributes_reader import (
OverrideBackedPredictionAttributesReader, OverrideBackedPredictionAttributesReader,
@ -212,7 +214,7 @@ def handler(body: dict[str, Any], context: Any) -> None:
read_session = Session(engine) read_session = Session(engine)
try: try:
scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0] scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0]
products = ProductPostgresRepository(read_session) products = catalogue_with_off_catalogue_overrides(read_session)
errors: list[int] = [] errors: list[int] = []
@ -282,21 +284,14 @@ def handler(body: dict[str, Any], context: Any) -> None:
None if no_solar else _solar_insights_for(solar_client, spatial) None if no_solar else _solar_insights_for(solar_client, spatial)
) )
# secondary_heating_removal is absent from the live material.type # All Measure Types are considered: the off-catalogue overlay
# enum; exclude unconditionally until the catalogue gap is resolved. # (catalogue_with_off_catalogue_overrides) prices the measures the
# system_tune_up and system_tune_up_zoned have no active product. # live material catalogue cannot supply, so none need excluding.
considered: Optional[frozenset[MeasureType]] = (
frozenset(MeasureType)
- {MeasureType.SECONDARY_HEATING_REMOVAL}
- {MeasureType.SYSTEM_TUNE_UP_ZONED}
- {MeasureType.SYSTEM_TUNE_UP}
)
plan = run_modelling( plan = run_modelling(
effective_epc, effective_epc,
planning_restrictions=restrictions, planning_restrictions=restrictions,
solar_insights=solar_insights, solar_insights=solar_insights,
considered_measures=considered, considered_measures=None,
products=products, products=products,
scenario=scenario, scenario=scenario,
print_table=False, print_table=False,

View file

@ -19,10 +19,8 @@ from datatypes.epc.search import EpcSearchResult
class EpcClientService: class EpcClientService:
BASE_URL = "https://api.get-energy-performance-data.communities.gov.uk" BASE_URL = "https://api.get-energy-performance-data.communities.gov.uk"
# The gov API's per-UPRN search latency is variable: usually ~0.2s but with # The gov API's per-UPRN search latency is variable: usually ~0.2s but with
# intermittent slow spells. 10s was low enough that a slow spell timed out and # intermittent slow spells.
# call_with_retry then re-issued it (compounding the cost); 30s rides out the REQUEST_TIMEOUT = 10.0
# spell instead. Rate-limit (429) handling stays with call_with_retry.
REQUEST_TIMEOUT = 30.0
def __init__(self, auth_token: str) -> None: def __init__(self, auth_token: str) -> None:
self._headers = { self._headers = {

View file

@ -167,7 +167,7 @@ def test_lodged_epc_path_saves_epc_plan_and_marks_modelled() -> None:
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.Session") patch("applications.modelling_e2e.handler.Session")
@ -285,7 +285,7 @@ def test_prediction_path_saves_plan_without_epc_save() -> None:
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.Session") patch("applications.modelling_e2e.handler.Session")
@ -381,7 +381,7 @@ def test_empty_cohort_gates_property_out_and_raises() -> None:
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
MockUoW = stack.enter_context( MockUoW = stack.enter_context(
@ -462,7 +462,7 @@ def test_partial_batch_failure_raises_runtime_error_listing_failed_ids() -> None
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
stack.enter_context( stack.enter_context(
@ -576,7 +576,7 @@ def test_cohort_cache_prevents_duplicate_candidates_for_calls() -> None:
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
stack.enter_context( stack.enter_context(
@ -659,7 +659,7 @@ def test_dry_run_skips_all_db_writes() -> None:
patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") patch("applications.modelling_e2e.handler.ScenarioPostgresRepository")
).return_value.get_many.return_value = [MagicMock()] ).return_value.get_many.return_value = [MagicMock()]
stack.enter_context( stack.enter_context(
patch("applications.modelling_e2e.handler.ProductPostgresRepository") patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")
) )
stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context(patch("applications.modelling_e2e.handler.Session"))
stack.enter_context( stack.enter_context(