From 803484defd8319e1d893f23e65c53b7cd97f0a88 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 15:59:06 +0000 Subject: [PATCH] =?UTF-8?q?Map=20lbwf=20data=20to=20new=20structure=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/lbwf/lbwf_element_map.py | 3 +- .../domain/mapping/lbwf/lbwf_mapper.py | 103 ++++++++++++------ .../domain/mapping/peabody/peabody_mapper.py | 7 +- backend/condition/tests/custom_asserts.py | 74 +++++++++++++ .../tests/mapping/test_lbwf_mapper.py | 12 +- .../tests/mapping/test_peabody_mapper.py | 5 +- 6 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 backend/condition/tests/custom_asserts.py diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index dfd9ca4e..96f21b84 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -175,7 +175,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWALLFN2": ElementMapping( element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=2, + element_instance=1, + aspect_instance=2, ), "EXTWALLINS": ElementMapping( element=ElementType.EXTERNAL_WALL, diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index fa61abf0..f11133bf 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,6 +1,9 @@ -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper @@ -19,51 +22,85 @@ class LbwfMapper(Mapper): def map_asset_conditions_for_property( self, client_property_data: Any, survey_year: Optional[int] = None ) -> PropertyConditionSurvey: - raise NotImplementedError - assert isinstance( client_property_data, LbwfHouse ) # TODO: think of a better way to do this - mapped_assets: List[Element] = [] + elements_by_key: dict[tuple[ElementType, int], Element] = {} - uprn: int = client_property_data.uprn for raw_asset in client_property_data.assets: - # Ignore metadata rows - if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: - try: - element_mapping: ElementMapping = LbwfMapper._map_element( - raw_asset.element_code - ) - except: - logger.warning( - f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" - ) - continue + element_mapping = LbwfMapper._safe_map_element(raw_asset) - mapped_assets.append( - Element( - uprn=uprn, - element_type=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.attribute_code_description, - quantity=raw_asset.quantity, - install_date=raw_asset.install_date, - renewal_year=LbwfMapper._calculate_renewal_year( - raw_asset, survey_year - ), - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=raw_asset.element_comments, - ) + aspect_condition = LbwfMapper._build_aspect_condition( + raw_asset, element_mapping, survey_year + ) + + element_key = ( + element_mapping.element, + element_mapping.element_instance or 1, + ) + + LbwfMapper._attach_aspect_condition_to_element( + elements_by_key, element_key, aspect_condition + ) + + return PropertyConditionSurvey( + uprn=client_property_data.uprn, + elements=list(elements_by_key.values()), + date=date(2000, 1, 1), # Temp - not sure how to get this + source="LBWF", # TODO: Make this the system, not the client + ) + + @staticmethod + def _safe_map_element(raw_asset: LbwfAssetCondition) -> Optional[ElementMapping]: + try: + return LbwfMapper._map_element(raw_asset.element_code) + except KeyError: + logger.warning( + logger.warning( + f"Unrecognised LBWF Asset Element: " + f"{raw_asset.element_code} ({raw_asset.element_code_description})). " + "Skipping record" ) - - return mapped_assets + ) + return None @staticmethod def _map_element(lbwf_element_code: str) -> ElementMapping: return LBWF_ELEMENT_MAP[lbwf_element_code] + @staticmethod + def _build_aspect_condition( + raw_asset, element_mapping: ElementMapping, survey_year: int + ) -> AspectCondition: + return AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + install_date=raw_asset.install_date, + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), + comments=raw_asset.element_comments, + ) + + @staticmethod + def _attach_aspect_condition_to_element( + elements_by_key: Dict[Tuple[ElementType, int], Element], + element_key: Tuple[ElementType, int], + aspect_condition: AspectCondition, + ) -> None: + element = elements_by_key.get(element_key) + + if element is None: + element = Element( + element_type=element_key[0], + element_instance=element_key[1], + aspect_conditions=[], + ) + elements_by_key[element_key] = element + + element.aspect_conditions.append(aspect_condition) + @staticmethod def _calculate_renewal_year( lbwf_asset: LbwfAssetCondition, survey_year: Optional[int] diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 7749b024..184b2898 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -10,6 +10,9 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( ) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.domain.property_condition_survey import PropertyConditionSurvey +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger @@ -52,7 +55,7 @@ class PeabodyMapper(Mapper): ) @staticmethod - def _safe_map_element(raw_asset) -> Optional[ElementMapping]: + def _safe_map_element(raw_asset: PeabodyAssetCondition) -> Optional[ElementMapping]: try: return PeabodyMapper._map_element( raw_asset.element_code, @@ -98,7 +101,7 @@ class PeabodyMapper(Mapper): aspect_instance=element_mapping.aspect_instance or 1, value=raw_asset.material_or_answer, quantity=raw_asset.renewal_quantity, - install_date=None, + install_date=None, # Not available in peabody data renewal_year=raw_asset.renewal_year, comments=None, ) diff --git a/backend/condition/tests/custom_asserts.py b/backend/condition/tests/custom_asserts.py new file mode 100644 index 00000000..9e3abd7f --- /dev/null +++ b/backend/condition/tests/custom_asserts.py @@ -0,0 +1,74 @@ +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey + + +class CustomAsserts: + def assert_property_condition_surveys_equal( + actual: PropertyConditionSurvey, + expected: PropertyConditionSurvey, + ) -> bool: + assert actual.uprn == expected.uprn, "UPRN differs" + assert actual.source == expected.source, "Source differs" + assert actual.date == expected.date, "Date differs" + + assert len(actual.elements) == len(expected.elements), ( + f"Expected {len(expected.elements)} elements, " + f"got {len(actual.elements)}" + ) + + for i, (actual_element, expected_element) in enumerate( + zip(actual.elements, expected.elements) + ): + assert actual_element.element_type == expected_element.element_type, ( + f"Element[{i}] type differs: " + f"{actual_element.element_type} != {expected_element.element_type}" + ) + assert ( + actual_element.element_instance == expected_element.element_instance + ), ( + f"Element[{i}] instance differs: " + f"{actual_element.element_instance} != {expected_element.element_instance}" + ) + + assert len(actual_element.aspect_conditions) == len( + expected_element.aspect_conditions + ), f"Element[{i}] aspect count differs" + + for j, (actual_aspect, expected_aspect) in enumerate( + zip( + actual_element.aspect_conditions, + expected_element.aspect_conditions, + ) + ): + prefix = f"Element[{i}].Aspect[{j}]" + + assert actual_aspect.aspect_type == expected_aspect.aspect_type, ( + f"{prefix}.aspect_type differs: " + f"{actual_aspect.aspect_type} != {expected_aspect.aspect_type}" + ) + assert ( + actual_aspect.aspect_instance == expected_aspect.aspect_instance + ), ( + f"{prefix}.aspect_instance differs: " + f"{actual_aspect.aspect_instance} != {expected_aspect.aspect_instance}" + ) + assert actual_aspect.value == expected_aspect.value, ( + f"{prefix}.value differs: " + f"{actual_aspect.value} != {expected_aspect.value}" + ) + assert actual_aspect.quantity == expected_aspect.quantity, ( + f"{prefix}.quantity differs: " + f"{actual_aspect.quantity} != {expected_aspect.quantity}" + ) + assert actual_aspect.install_date == expected_aspect.install_date, ( + f"{prefix}.install_date differs: " + f"{actual_aspect.install_date} != {expected_aspect.install_date}" + ) + assert actual_aspect.renewal_year == expected_aspect.renewal_year, ( + f"{prefix}.renewal_year differs: " + f"{actual_aspect.renewal_year} != {expected_aspect.renewal_year}" + ) + assert actual_aspect.comments == expected_aspect.comments, ( + f"{prefix}.comments differs: " + f"{actual_aspect.comments} != {expected_aspect.comments}" + ) + return True diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 064e06f7..77890155 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -1,6 +1,3 @@ -from typing import List -from xml.dom.minidom import Element -import pytest from datetime import date from backend.condition.domain.aspect_condition import AspectCondition @@ -13,6 +10,7 @@ from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_lbwf_mapper_maps_house(): @@ -265,7 +263,7 @@ def test_lbwf_mapper_maps_house(): quantity=None, install_date=None, renewal_year=None, - comments=None, + comments="Source of Data = ACT", ) ], ), @@ -359,8 +357,10 @@ def test_lbwf_mapper_maps_house(): # act actual_condition_survey: PropertyConditionSurvey = ( - mapper.map_asset_conditions_for_property(lbwf_house) + mapper.map_asset_conditions_for_property(lbwf_house, survey_year) ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + ) diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 63cd19c9..979258b0 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -10,6 +10,7 @@ from backend.condition.parsing.records.peabody.peabody_asset_condition import ( ) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_peabody_mapper_maps_property(): @@ -214,4 +215,6 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + )