mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Fail unmodellable properties with a specific, debuggable error 🟩
_predict_epc returned None for three unrelated causes — unresolved property_type, an empty same-type cohort, and a degenerate (no MAIN part) prediction — which the handler collapsed into one generic "not predictable" string. The SubTask output could not say which cause fired or which data to fix. Raise a specific PropertyNotModellableError subclass per cause, each carrying the property's identity (property_id, uprn, postcode, portfolio_id) and cause-specific context. The unresolved-property-type message points at the likely missing/contradictory Landlord Override. All subclass ValueError, so the per-property failure boundary keeps catching them and records str(exc). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd06b19c77
commit
04ee16488e
2 changed files with 41 additions and 14 deletions
|
|
@ -67,6 +67,11 @@ from infrastructure.solar.google_solar_api_client import (
|
|||
BuildingInsightsNotFoundError,
|
||||
GoogleSolarApiClient,
|
||||
)
|
||||
from applications.modelling_e2e.errors import (
|
||||
DegeneratePredictionError,
|
||||
NoSameTypeComparablesError,
|
||||
UnresolvedPropertyTypeError,
|
||||
)
|
||||
from applications.modelling_e2e.modelling_e2e_trigger_body import (
|
||||
ModellingE2ETriggerBody,
|
||||
)
|
||||
|
|
@ -181,16 +186,19 @@ def _predict_epc(
|
|||
cohort_for: Callable[[str], list[ComparableProperty]],
|
||||
broaden: Callable[[PredictionTarget], list[ComparableProperty]],
|
||||
predictor: EpcPrediction,
|
||||
) -> Optional[EpcPropertyData]:
|
||||
) -> EpcPropertyData:
|
||||
"""Synthesise an EpcPropertyData for an EPC-less property from its postcode
|
||||
cohort (EPC Prediction Path 3, ADR-0031), or None when ineligible.
|
||||
cohort (EPC Prediction Path 3, ADR-0031).
|
||||
|
||||
When the property's own postcode holds no same-type comparables (a sparse
|
||||
postcode — e.g. the only flat among houses), the cohort is broadened to the
|
||||
real unit postcodes physically nearest it (``broaden``) before giving up.
|
||||
|
||||
Returns None when property_type is unresolvable (hard cohort filter cannot
|
||||
fire) or when even the broadened cohort is empty after filtering.
|
||||
Raises a specific ``PropertyNotModellableError`` subclass — naming the cause
|
||||
and carrying the property's identity — when it cannot predict: property_type
|
||||
unresolved, an empty same-type cohort, or a degenerate (no MAIN part)
|
||||
prediction. The per-property handler records ``str(exc)`` in the SubTask
|
||||
output, so the cause is debuggable from the output alone.
|
||||
"""
|
||||
attributes = attributes_reader.attributes_for(property_id)
|
||||
identity = PropertyIdentity(
|
||||
|
|
@ -198,18 +206,41 @@ def _predict_epc(
|
|||
)
|
||||
target = build_prediction_target(identity, coordinates, attributes)
|
||||
if target is None:
|
||||
return None
|
||||
raise UnresolvedPropertyTypeError(
|
||||
property_id=property_id,
|
||||
uprn=uprn,
|
||||
postcode=postcode,
|
||||
portfolio_id=portfolio_id,
|
||||
property_type=attributes.property_type,
|
||||
built_form=attributes.built_form,
|
||||
)
|
||||
comparables = select_comparables(target, cohort_for(target.postcode))
|
||||
broadened = False
|
||||
if not comparables.members:
|
||||
broadened = True
|
||||
comparables = select_comparables(target, broaden(target))
|
||||
if not comparables.members:
|
||||
return None
|
||||
raise NoSameTypeComparablesError(
|
||||
property_id=property_id,
|
||||
uprn=uprn,
|
||||
postcode=postcode,
|
||||
portfolio_id=portfolio_id,
|
||||
property_type=target.property_type,
|
||||
broadened=broadened,
|
||||
)
|
||||
predicted = predictor.predict(target, comparables)
|
||||
if not any(
|
||||
part.identifier is BuildingPartIdentifier.MAIN
|
||||
for part in predicted.sap_building_parts
|
||||
):
|
||||
return None
|
||||
raise DegeneratePredictionError(
|
||||
property_id=property_id,
|
||||
uprn=uprn,
|
||||
postcode=postcode,
|
||||
portfolio_id=portfolio_id,
|
||||
property_type=target.property_type,
|
||||
cohort_size=len(comparables.members),
|
||||
)
|
||||
return predicted
|
||||
|
||||
|
||||
|
|
@ -344,12 +375,6 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
|
|||
broaden=_broaden,
|
||||
predictor=predictor,
|
||||
)
|
||||
if predicted_epc is None:
|
||||
raise ValueError(
|
||||
f"no EPC for UPRN {uprn} and not predictable "
|
||||
f"(unresolved property_type, or no same-type "
|
||||
f"comparables in or near '{postcode}')"
|
||||
)
|
||||
effective_epc = Property(
|
||||
identity=PropertyIdentity(
|
||||
portfolio_id=portfolio_id,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ from applications.modelling_e2e.errors import (
|
|||
NoSameTypeComparablesError,
|
||||
UnresolvedPropertyTypeError,
|
||||
)
|
||||
from applications.modelling_e2e.handler import _predict_epc
|
||||
from applications.modelling_e2e.handler import (
|
||||
_predict_epc, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue