Resolve a Property's prediction attributes from landlord overrides in gov-code space 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-16 15:18:44 +00:00
parent c5cffd9047
commit 864ba8dc1b
2 changed files with 141 additions and 0 deletions

View file

@ -0,0 +1,55 @@
"""The real ``PredictionTargetAttributesReader`` — landlord-overrides-backed.
Composes the faithful ``PropertyOverridesReader`` with the valuecode mapping:
reads the Property's main-building (building_part 0) ``property_type`` /
``built_form_type`` overrides and translates them into the gov-EPC code space the
cohort filter compares against (ADR-0031). An unresolvable ``property_type``
becomes None, which gates the Property out of prediction downstream
(``build_prediction_target``). Wall/roof overrides are left to the later
``epc_with_overlay`` slice this reader conditions cohort selection only.
"""
from __future__ import annotations
from typing import Optional
from domain.epc.override_code_mapping import (
built_form_to_code,
property_type_to_code,
)
from domain.epc_prediction.prediction_target import PredictionTargetAttributes
from repositories.property.prediction_target_attributes_reader import (
PredictionTargetAttributesReader,
)
from repositories.property.property_overrides_reader import PropertyOverridesReader
_MAIN_BUILDING = 0
_PROPERTY_TYPE_COMPONENT = "property_type"
_BUILT_FORM_COMPONENT = "built_form_type"
class OverrideBackedPredictionAttributesReader(PredictionTargetAttributesReader):
def __init__(self, overrides_reader: PropertyOverridesReader) -> None:
self._overrides_reader = overrides_reader
def attributes_for(self, property_id: int) -> PredictionTargetAttributes:
overrides = self._overrides_reader.overrides_for(property_id)
property_type_value = overrides.value(_PROPERTY_TYPE_COMPONENT, _MAIN_BUILDING)
built_form_value = overrides.value(_BUILT_FORM_COMPONENT, _MAIN_BUILDING)
property_type: Optional[str] = (
property_type_to_code(property_type_value)
if property_type_value is not None
else None
)
built_form: Optional[str] = (
built_form_to_code(built_form_value)
if built_form_value is not None
else None
)
return PredictionTargetAttributes(
property_type=property_type,
built_form=built_form,
)

View file

@ -0,0 +1,86 @@
"""The landlord-overrides-backed PredictionTargetAttributesReader (ADR-0031).
Unit-level: a fake ``PropertyOverridesReader`` supplies value-space snapshots so
these tests pin the composition main-building selection, valuecode mapping,
and the gate (unresolvable property_type None) without a database.
"""
from __future__ import annotations
from repositories.property.override_backed_prediction_attributes_reader import (
OverrideBackedPredictionAttributesReader,
)
from repositories.property.property_overrides_reader import (
PropertyOverridesReader,
ResolvedPropertyOverride,
ResolvedPropertyOverrides,
)
class _FakeOverridesReader(PropertyOverridesReader):
def __init__(self, *rows: ResolvedPropertyOverride) -> None:
self._snapshot = ResolvedPropertyOverrides(rows=rows)
def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides:
return self._snapshot
def test_main_building_property_type_is_mapped_to_its_gov_code() -> None:
# Arrange
reader = OverrideBackedPredictionAttributesReader(
_FakeOverridesReader(
ResolvedPropertyOverride("property_type", 0, "House"),
)
)
# Act
attributes = reader.attributes_for(1)
# Assert
assert attributes.property_type == "0"
def test_built_form_is_mapped_and_only_the_main_building_is_read() -> None:
# Arrange — main building is a House/Detached; an extension (part 1) carries a
# different property type that must not be read.
reader = OverrideBackedPredictionAttributesReader(
_FakeOverridesReader(
ResolvedPropertyOverride("property_type", 0, "House"),
ResolvedPropertyOverride("built_form_type", 0, "Detached"),
ResolvedPropertyOverride("property_type", 1, "Flat"),
)
)
# Act
attributes = reader.attributes_for(1)
# Assert — built_form mapped to its code; the part-1 "Flat" is ignored.
assert attributes.property_type == "0"
assert attributes.built_form == "1"
def test_unresolvable_property_type_gates_the_property_out() -> None:
# Arrange — the landlord override resolved only to "Unknown".
reader = OverrideBackedPredictionAttributesReader(
_FakeOverridesReader(
ResolvedPropertyOverride("property_type", 0, "Unknown"),
)
)
# Act
attributes = reader.attributes_for(1)
# Assert — None property_type makes build_prediction_target skip the Property.
assert attributes.property_type is None
def test_property_with_no_overrides_yields_no_attributes() -> None:
# Arrange — nothing resolved for the Property.
reader = OverrideBackedPredictionAttributesReader(_FakeOverridesReader())
# Act
attributes = reader.attributes_for(1)
# Assert
assert attributes.property_type is None
assert attributes.built_form is None