From 51309328e6f168e022b636a8f371db50d2550416 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 11 Jun 2026 11:39:07 +0000 Subject: [PATCH] =?UTF-8?q?Parse=20and=20map=20all=20RdSAP-Schema-18.0=20c?= =?UTF-8?q?orpus=20certs=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 10 ++- datatypes/epc/schema/rdsap_schema_18_0.py | 74 ++++++++++++++--------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 95e48b45..9d9a7802 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -747,7 +747,12 @@ class EpcPropertyDataMapper: uprn=schema.uprn, assessment_type=schema.assessment_type, sap_version=schema.sap_version, - dwelling_type=schema.dwelling_type.value, + # ADR-0028: 18.0 lodges dwelling_type as str OR localised dict. + dwelling_type=( + schema.dwelling_type + if isinstance(schema.dwelling_type, str) + else schema.dwelling_type.value + ), property_type=str(schema.property_type), built_form=str(schema.built_form), address_line_1=schema.address_line_1, @@ -842,6 +847,7 @@ class EpcPropertyDataMapper: ) ) if es.photovoltaic_supply + and es.photovoltaic_supply.none_or_no_details else None ), ), @@ -895,7 +901,7 @@ class EpcPropertyDataMapper: ), sap_room_in_roof=( SapRoomInRoof( - floor_area=bp.sap_room_in_roof.floor_area.value, + floor_area=_measurement_value(bp.sap_room_in_roof.floor_area), construction_age_band=bp.sap_room_in_roof.construction_age_band, ) if bp.sap_room_in_roof diff --git a/datatypes/epc/schema/rdsap_schema_18_0.py b/datatypes/epc/schema/rdsap_schema_18_0.py index 0d798a35..4289aef7 100644 --- a/datatypes/epc/schema/rdsap_schema_18_0.py +++ b/datatypes/epc/schema/rdsap_schema_18_0.py @@ -60,7 +60,7 @@ class PhotovoltaicSupplyNoneOrNoDetails: @dataclass class PhotovoltaicSupply: - none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails + none_or_no_details: Optional[PhotovoltaicSupplyNoneOrNoDetails] = None @dataclass @@ -85,30 +85,38 @@ class SapFloorDimension: @dataclass class SapRoomInRoof: - """Room-in-roof details. floor_area is a Measurement object in schema 18.0.""" + """Room-in-roof details. floor_area is usually a Measurement object in 18.0, + but 6/1000 certs lodge it as a plain number (ADR-0028) — read via + `_measurement_value`, which coerces both shapes.""" - floor_area: Measurement + floor_area: Union[Measurement, int, float] insulation: str roof_room_connected: str construction_age_band: str -@dataclass +@dataclass(kw_only=True) class SapBuildingPart: - identifier: str - wall_dry_lined: str - wall_thickness: int - floor_heat_loss: int - roof_construction: int - wall_construction: int - building_part_number: int - sap_floor_dimensions: List[SapFloorDimension] - wall_insulation_type: int - construction_age_band: str - party_wall_construction: Union[int, str] - wall_thickness_measured: str - roof_insulation_location: Union[int, str] - roof_insulation_thickness: Union[str, int] + # Data-driven required→optional (ADR-0028): 17/1000 certs lodge a + # conservatory-shaped part carrying only {double_glazed, floor_area, + # glazed_perimeter, room_height} — none of the construction fields. Every + # field is Optional (the 21.0.1/20.0.0 precedent); the all-None part flows + # through harmlessly because the conservatory's effect is carried separately + # by conservatory_type. + identifier: Optional[str] = None + wall_dry_lined: Optional[str] = None + wall_thickness: Optional[int] = None + floor_heat_loss: Optional[int] = None + roof_construction: Optional[int] = None + wall_construction: Optional[int] = None + building_part_number: Optional[int] = None + sap_floor_dimensions: Optional[List[SapFloorDimension]] = None + wall_insulation_type: Optional[int] = None + construction_age_band: Optional[str] = None + party_wall_construction: Optional[Union[int, str]] = None + wall_thickness_measured: Optional[str] = None + roof_insulation_location: Optional[Union[int, str]] = None + roof_insulation_thickness: Optional[Union[str, int]] = None sap_room_in_roof: Optional[SapRoomInRoof] = None wall_insulation_thickness: Optional[str] = None floor_insulation_thickness: Optional[str] = None @@ -140,15 +148,18 @@ class SuggestedImprovement: environmental_impact_rating: int -@dataclass +@dataclass(kw_only=True) class AlternativeImprovement: - sequence: int - typical_saving: CostAmount - improvement_type: str - improvement_details: ImprovementDetails - improvement_category: int - energy_performance_rating: int - environmental_impact_rating: int + # ADR-0028: 165/1000 lodge a reduced alternative-improvement shape (only + # improvement_details/-type). Parse-only — the mapper does not read + # alternative_improvements — so every field is Optional. + sequence: Optional[int] = None + typical_saving: Optional[CostAmount] = None + improvement_type: Optional[str] = None + improvement_details: Optional[ImprovementDetails] = None + improvement_category: Optional[int] = None + energy_performance_rating: Optional[int] = None + environmental_impact_rating: Optional[int] = None @dataclass @@ -175,8 +186,9 @@ class RdSapSchema18_0: built_form: int door_count: int glazed_area: int - # glazing_gap is an integer in 18.0 (e.g. 12 mm), unlike 17.x where it was a string - glazing_gap: int + # ADR-0028: glazing_gap is lodged as int (e.g. 12 mm), str ("16+"), or + # omitted across the corpus (433/1000) — widen + default, not int-required. + glazing_gap: Optional[Union[int, str]] = None region_code: int report_type: int sap_heating: SapHeating @@ -185,7 +197,9 @@ class RdSapSchema18_0: uprn_source: str country_code: str main_heating: List[EnergyElement] - dwelling_type: DescriptionV1 + # ADR-0028: 392/1000 lodge dwelling_type as a plain str, not a localised + # DescriptionV1 object (matches 20.0.0). Widen so both shapes parse. + dwelling_type: Union[str, DescriptionV1] language_code: int property_type: int address_line_1: str @@ -198,7 +212,7 @@ class RdSapSchema18_0: transaction_type: int conservatory_type: int heated_room_count: int - pvc_window_frames: str + pvc_window_frames: Optional[str] = None registration_date: str sap_energy_source: SapEnergySource secondary_heating: EnergyElement