various fixes

This commit is contained in:
Daniel Roth 2026-06-22 14:21:52 +00:00
parent 409fb8485c
commit dc830fea63
4 changed files with 48 additions and 12 deletions

View file

@ -39,7 +39,9 @@ from infrastructure.solar.google_solar_api_client import (
BuildingInsightsNotFoundError, BuildingInsightsNotFoundError,
GoogleSolarApiClient, GoogleSolarApiClient,
) )
from applications.modelling_e2e.modelling_e2e_trigger_body import ModellingE2ETriggerBody from applications.modelling_e2e.modelling_e2e_trigger_body import (
ModellingE2ETriggerBody,
)
from repositories.geospatial.geospatial_s3_repository import ( from repositories.geospatial.geospatial_s3_repository import (
GeospatialS3Repository, GeospatialS3Repository,
ParquetReader, ParquetReader,
@ -57,9 +59,12 @@ from repositories.scenario.scenario_postgres_repository import (
ScenarioPostgresRepository, ScenarioPostgresRepository,
) )
from utilities.aws_lambda.task_handler import task_handler from utilities.aws_lambda.task_handler import task_handler
from utilities.logger import setup_logger
_engine: Optional[Engine] = None _engine: Optional[Engine] = None
logger = setup_logger()
def _get_engine() -> Engine: def _get_engine() -> Engine:
global _engine global _engine
@ -72,7 +77,9 @@ def _s3_parquet_reader() -> ParquetReader:
bucket = os.environ["DATA_BUCKET"] bucket = os.environ["DATA_BUCKET"]
def read(key: str) -> pd.DataFrame: def read(key: str) -> pd.DataFrame:
s3: Any = cast(Any, boto3.client("s3")) # pyright: ignore[reportUnknownMemberType] s3: Any = cast(
Any, boto3.client("s3")
) # pyright: ignore[reportUnknownMemberType]
raw = cast(bytes, s3.get_object(Bucket=bucket, Key=key)["Body"].read()) raw = cast(bytes, s3.get_object(Bucket=bucket, Key=key)["Body"].read())
return pd.read_parquet(io.BytesIO(raw)) # type: ignore[return-value] return pd.read_parquet(io.BytesIO(raw)) # type: ignore[return-value]
@ -110,6 +117,11 @@ def handler(body: dict[str, Any], context: Any) -> None:
no_solar = trigger.no_solar no_solar = trigger.no_solar
dry_run = trigger.dry_run dry_run = trigger.dry_run
logger.info(
f"start property={property_id} portfolio={portfolio_id} "
f"scenario={scenario_id} no_solar={no_solar} dry_run={dry_run}"
)
engine = _get_engine() engine = _get_engine()
epc_client = EpcClientService(os.environ["OPEN_EPC_API_TOKEN"]) epc_client = EpcClientService(os.environ["OPEN_EPC_API_TOKEN"])
geospatial = GeospatialS3Repository(_s3_parquet_reader()) geospatial = GeospatialS3Repository(_s3_parquet_reader())
@ -121,10 +133,12 @@ def handler(body: dict[str, Any], context: Any) -> None:
{"pid": property_id}, {"pid": property_id},
).one() ).one()
uprn = int(row[0]) uprn = int(row[0])
logger.info(f"resolved uprn={uprn}")
epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn) epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn)
if epc is None: if epc is None:
raise ValueError(f"no EPC found for UPRN {uprn} (property {property_id})") raise ValueError(f"no EPC found for UPRN {uprn} (property {property_id})")
logger.info(f"fetched EPC (energy_rating_current={epc.energy_rating_current})")
overrides_reader = PropertyOverridesPostgresReader(lambda: Session(engine)) overrides_reader = PropertyOverridesPostgresReader(lambda: Session(engine))
overlaid = Property( overlaid = Property(
@ -140,10 +154,18 @@ def handler(body: dict[str, Any], context: Any) -> None:
restrictions = ( restrictions = (
spatial.restrictions if spatial is not None else PlanningRestrictions() spatial.restrictions if spatial is not None else PlanningRestrictions()
) )
solar_insights = None if no_solar else _solar_insights_for(solar_client, spatial) logger.info(f"spatial={'found' if spatial is not None else 'not found'}")
if no_solar:
solar_insights = None
logger.info("solar skipped (no_solar=True)")
else:
solar_insights = _solar_insights_for(solar_client, spatial)
logger.info(f"solar={'found' if solar_insights is not None else 'not found'}")
with Session(engine) as session: with Session(engine) as session:
scenario = ScenarioPostgresRepository(session).get_many([scenario_id])[0] scenario = ScenarioPostgresRepository(session).get_many([scenario_id])[0]
logger.info(f"loaded scenario goal={scenario.goal!r} goal_value={scenario.goal_value!r}")
products = ProductPostgresRepository(session) products = ProductPostgresRepository(session)
# secondary_heating_removal is absent from the live material.type enum; # secondary_heating_removal is absent from the live material.type enum;
@ -152,6 +174,7 @@ def handler(body: dict[str, Any], context: Any) -> None:
MeasureType.SECONDARY_HEATING_REMOVAL MeasureType.SECONDARY_HEATING_REMOVAL
} }
logger.info("running modelling pipeline")
plan = run_modelling( plan = run_modelling(
effective_epc, effective_epc,
planning_restrictions=restrictions, planning_restrictions=restrictions,
@ -161,14 +184,15 @@ def handler(body: dict[str, Any], context: Any) -> None:
scenario=scenario, scenario=scenario,
print_table=False, print_table=False,
) )
logger.info(
f"modelling complete: SAP {plan.baseline.sap_continuous:.1f}"
f"{plan.post_sap_continuous:.1f} measures={len(plan.measures)} "
f"cost=£{plan.cost_of_works:,.0f}"
)
if dry_run: if dry_run:
measure_types = ", ".join(m.measure_type for m in plan.measures) or "none" measure_types = ", ".join(m.measure_type for m in plan.measures) or "none"
print( logger.info(f"[dry_run] measures=[{measure_types}] — skipping DB write")
f"[dry_run] property={property_id} scenario={scenario_id} "
f"SAP {plan.baseline.sap_continuous:.1f}{plan.post_sap_continuous:.1f} "
f"measures=[{measure_types}] cost=£{plan.cost_of_works:,.0f}"
)
return return
PlanPostgresRepository(session).save( PlanPostgresRepository(session).save(
plan, plan,
@ -181,3 +205,4 @@ def handler(body: dict[str, Any], context: Any) -> None:
property_id, has_recommendations=bool(plan.measures) property_id, has_recommendations=bool(plan.measures)
) )
session.commit() session.commit()
logger.info("plan saved")

View file

@ -12,9 +12,9 @@ payload = {
{ {
"body": json.dumps( "body": json.dumps(
{ {
"property_id": 0, "property_id": 709634,
"portfolio_id": 0, "portfolio_id": 785,
"scenario_id": 0, "scenario_id": 1266,
"no_solar": False, "no_solar": False,
"dry_run": True, "dry_run": True,
} }

View file

@ -1,7 +1,10 @@
awslambdaric awslambdaric
boto3 boto3
numpy==2.1.2
pandas==2.2.2 pandas==2.2.2
pyarrow pyarrow==17.0.0
httpx
requests
pydantic pydantic
sqlalchemy==2.0.36 sqlalchemy==2.0.36
sqlmodel sqlmodel

View file

@ -5,6 +5,7 @@ TaskOrchestrator.create_task_with_subtask(...) + run_subtask(...).
""" """
import json import json
import logging
from contextlib import AbstractContextManager from contextlib import AbstractContextManager
from functools import wraps from functools import wraps
from typing import Any, Callable, Optional, cast from typing import Any, Callable, Optional, cast
@ -13,6 +14,8 @@ from utilities.aws_lambda.default_orchestrator import default_orchestrator
from domain.tasks.tasks import Source from domain.tasks.tasks import Source
from orchestration.task_orchestrator import TaskOrchestrator from orchestration.task_orchestrator import TaskOrchestrator
logger = logging.getLogger(__name__)
OrchestratorCM = Callable[[], AbstractContextManager[TaskOrchestrator]] OrchestratorCM = Callable[[], AbstractContextManager[TaskOrchestrator]]
@ -63,6 +66,11 @@ def task_handler(
) )
results.append(result) results.append(result)
except Exception: except Exception:
logger.exception(
"subtask failed (task_source=%s source_id=%s)",
task_source,
source_id,
)
if "Records" in event: if "Records" in event:
message_id = record.get("messageId", "") message_id = record.get("messageId", "")
failures.append({"itemIdentifier": message_id}) failures.append({"itemIdentifier": message_id})