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,
|
BuildingInsightsNotFoundError,
|
||||||
GoogleSolarApiClient,
|
GoogleSolarApiClient,
|
||||||
)
|
)
|
||||||
|
from applications.modelling_e2e.errors import (
|
||||||
|
DegeneratePredictionError,
|
||||||
|
NoSameTypeComparablesError,
|
||||||
|
UnresolvedPropertyTypeError,
|
||||||
|
)
|
||||||
from applications.modelling_e2e.modelling_e2e_trigger_body import (
|
from applications.modelling_e2e.modelling_e2e_trigger_body import (
|
||||||
ModellingE2ETriggerBody,
|
ModellingE2ETriggerBody,
|
||||||
)
|
)
|
||||||
|
|
@ -181,16 +186,19 @@ def _predict_epc(
|
||||||
cohort_for: Callable[[str], list[ComparableProperty]],
|
cohort_for: Callable[[str], list[ComparableProperty]],
|
||||||
broaden: Callable[[PredictionTarget], list[ComparableProperty]],
|
broaden: Callable[[PredictionTarget], list[ComparableProperty]],
|
||||||
predictor: EpcPrediction,
|
predictor: EpcPrediction,
|
||||||
) -> Optional[EpcPropertyData]:
|
) -> EpcPropertyData:
|
||||||
"""Synthesise an EpcPropertyData for an EPC-less property from its postcode
|
"""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
|
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
|
postcode — e.g. the only flat among houses), the cohort is broadened to the
|
||||||
real unit postcodes physically nearest it (``broaden``) before giving up.
|
real unit postcodes physically nearest it (``broaden``) before giving up.
|
||||||
|
|
||||||
Returns None when property_type is unresolvable (hard cohort filter cannot
|
Raises a specific ``PropertyNotModellableError`` subclass — naming the cause
|
||||||
fire) or when even the broadened cohort is empty after filtering.
|
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)
|
attributes = attributes_reader.attributes_for(property_id)
|
||||||
identity = PropertyIdentity(
|
identity = PropertyIdentity(
|
||||||
|
|
@ -198,18 +206,41 @@ def _predict_epc(
|
||||||
)
|
)
|
||||||
target = build_prediction_target(identity, coordinates, attributes)
|
target = build_prediction_target(identity, coordinates, attributes)
|
||||||
if target is None:
|
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))
|
comparables = select_comparables(target, cohort_for(target.postcode))
|
||||||
|
broadened = False
|
||||||
if not comparables.members:
|
if not comparables.members:
|
||||||
|
broadened = True
|
||||||
comparables = select_comparables(target, broaden(target))
|
comparables = select_comparables(target, broaden(target))
|
||||||
if not comparables.members:
|
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)
|
predicted = predictor.predict(target, comparables)
|
||||||
if not any(
|
if not any(
|
||||||
part.identifier is BuildingPartIdentifier.MAIN
|
part.identifier is BuildingPartIdentifier.MAIN
|
||||||
for part in predicted.sap_building_parts
|
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
|
return predicted
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -344,12 +375,6 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
|
||||||
broaden=_broaden,
|
broaden=_broaden,
|
||||||
predictor=predictor,
|
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(
|
effective_epc = Property(
|
||||||
identity=PropertyIdentity(
|
identity=PropertyIdentity(
|
||||||
portfolio_id=portfolio_id,
|
portfolio_id=portfolio_id,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ from applications.modelling_e2e.errors import (
|
||||||
NoSameTypeComparablesError,
|
NoSameTypeComparablesError,
|
||||||
UnresolvedPropertyTypeError,
|
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 (
|
from datatypes.epc.domain.epc_property_data import (
|
||||||
BuildingPartIdentifier,
|
BuildingPartIdentifier,
|
||||||
EpcPropertyData,
|
EpcPropertyData,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue