mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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:
parent
c5cffd9047
commit
864ba8dc1b
2 changed files with 141 additions and 0 deletions
|
|
@ -0,0 +1,55 @@
|
|||
"""The real ``PredictionTargetAttributesReader`` — landlord-overrides-backed.
|
||||
|
||||
Composes the faithful ``PropertyOverridesReader`` with the value→code 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,
|
||||
)
|
||||
|
|
@ -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, value→code 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
|
||||
Loading…
Add table
Reference in a new issue