mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #1283 from Hestia-Homes/fix/rdsap-21-0-0-cohort-cert-skips
Align RdSAP 21.0.0 schema optionality with 21.0.1 (unblock ~35 cohort certs)
This commit is contained in:
commit
eefeccf0bc
3 changed files with 87 additions and 36 deletions
|
|
@ -1850,13 +1850,15 @@ class EpcPropertyDataMapper:
|
||||||
door_count=schema.door_count,
|
door_count=schema.door_count,
|
||||||
habitable_rooms_count=schema.habitable_room_count,
|
habitable_rooms_count=schema.habitable_room_count,
|
||||||
heated_rooms_count=schema.heated_room_count,
|
heated_rooms_count=schema.heated_room_count,
|
||||||
wet_rooms_count=schema.wet_rooms_count,
|
# Optional in 21.0.0 (real certs omit them); coalesce counts to 0
|
||||||
|
# like every other mapper so None can't reach a NOT-NULL column.
|
||||||
|
wet_rooms_count=schema.wet_rooms_count or 0,
|
||||||
extensions_count=schema.extensions_count,
|
extensions_count=schema.extensions_count,
|
||||||
open_chimneys_count=schema.open_chimneys_count,
|
open_chimneys_count=schema.open_chimneys_count or 0,
|
||||||
insulated_door_count=schema.insulated_door_count,
|
insulated_door_count=schema.insulated_door_count,
|
||||||
draughtproofed_door_count=schema.draughtproofed_door_count,
|
draughtproofed_door_count=schema.draughtproofed_door_count,
|
||||||
led_fixed_lighting_bulbs_count=schema.led_fixed_lighting_bulbs_count,
|
led_fixed_lighting_bulbs_count=schema.led_fixed_lighting_bulbs_count or 0,
|
||||||
cfl_fixed_lighting_bulbs_count=schema.cfl_fixed_lighting_bulbs_count,
|
cfl_fixed_lighting_bulbs_count=schema.cfl_fixed_lighting_bulbs_count or 0,
|
||||||
incandescent_fixed_lighting_bulbs_count=schema.incandescent_fixed_lighting_bulbs_count,
|
incandescent_fixed_lighting_bulbs_count=schema.incandescent_fixed_lighting_bulbs_count,
|
||||||
roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs),
|
roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs),
|
||||||
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
||||||
|
|
@ -1928,7 +1930,7 @@ class EpcPropertyDataMapper:
|
||||||
sap_windows=[
|
sap_windows=[
|
||||||
SapWindow(
|
SapWindow(
|
||||||
frame_material="PVC" if w.pvc_frame == "true" else None,
|
frame_material="PVC" if w.pvc_frame == "true" else None,
|
||||||
glazing_gap=w.glazing_gap,
|
glazing_gap=w.glazing_gap, # pyright: ignore[reportArgumentType]
|
||||||
orientation=w.orientation,
|
orientation=w.orientation,
|
||||||
window_type=w.window_type,
|
window_type=w.window_type,
|
||||||
frame_factor=w.frame_factor,
|
frame_factor=w.frame_factor,
|
||||||
|
|
@ -1939,10 +1941,17 @@ class EpcPropertyDataMapper:
|
||||||
window_location=w.window_location,
|
window_location=w.window_location,
|
||||||
window_wall_type=w.window_wall_type,
|
window_wall_type=w.window_wall_type,
|
||||||
permanent_shutters_present=w.permanent_shutters_present == "Y",
|
permanent_shutters_present=w.permanent_shutters_present == "Y",
|
||||||
window_transmission_details=WindowTransmissionDetails(
|
# Real-API 21.0.0 certs may omit transmission details; the
|
||||||
u_value=w.window_transmission_details.u_value,
|
# calc then synthesises the window U/g from glazing_type +
|
||||||
data_source=w.window_transmission_details.data_source,
|
# glazing_gap (mirrors the 21.0.1 `_api_sap_window` guard).
|
||||||
solar_transmittance=w.window_transmission_details.solar_transmittance,
|
window_transmission_details=(
|
||||||
|
WindowTransmissionDetails(
|
||||||
|
u_value=w.window_transmission_details.u_value,
|
||||||
|
data_source=w.window_transmission_details.data_source,
|
||||||
|
solar_transmittance=w.window_transmission_details.solar_transmittance,
|
||||||
|
)
|
||||||
|
if w.window_transmission_details is not None
|
||||||
|
else None
|
||||||
),
|
),
|
||||||
permanent_shutters_insulated=w.permanent_shutters_insulated,
|
permanent_shutters_insulated=w.permanent_shutters_insulated,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,42 @@ class TestFromRdSapSchema21_0_0:
|
||||||
schema = from_dict(RdSapSchema21_0_0, load("21_0_0.json"))
|
schema = from_dict(RdSapSchema21_0_0, load("21_0_0.json"))
|
||||||
return EpcPropertyDataMapper.from_rdsap_schema_21_0_0(schema)
|
return EpcPropertyDataMapper.from_rdsap_schema_21_0_0(schema)
|
||||||
|
|
||||||
|
def test_optional_fields_omitted_maps_not_crashed(self) -> None:
|
||||||
|
# Real-API 21.0.0 cohort certs omit a set of fields that the dataclass
|
||||||
|
# modelled as required while 21.0.1 already treats as optional. The
|
||||||
|
# modelling_e2e cohort skipped 33 such certs (e.g. 2205-3036-3484-0400-
|
||||||
|
# 5718). This is the exact union of fields those certs omit — all are
|
||||||
|
# optional in 21.0.1; widening 21.0.0 to match maps them into the donor
|
||||||
|
# pool instead of dropping them. (Building-part fields are NOT widened:
|
||||||
|
# the only one those certs omit is roof_insulation_thickness.)
|
||||||
|
data = load("21_0_0.json")
|
||||||
|
for window in data.get("sap_windows", []):
|
||||||
|
for k in ("frame_factor", "glazing_gap", "pvc_frame",
|
||||||
|
"window_transmission_details"):
|
||||||
|
window.pop(k, None)
|
||||||
|
for k in ("wet_rooms_count", "open_chimneys_count",
|
||||||
|
"pressure_test_certificate_number", "windows_transmission_details",
|
||||||
|
"mechanical_ventilation_index_number", "mechanical_vent_duct_type",
|
||||||
|
"mechanical_vent_duct_placement", "mechanical_vent_duct_insulation",
|
||||||
|
"mechanical_vent_duct_insulation_level",
|
||||||
|
"mechanical_vent_measured_installation", "insulated_door_u_value",
|
||||||
|
"low_energy_fixed_lighting_bulbs_count",
|
||||||
|
"cfl_fixed_lighting_bulbs_count", "led_fixed_lighting_bulbs_count",
|
||||||
|
"suggested_improvements"):
|
||||||
|
data.pop(k, None)
|
||||||
|
data["sap_energy_source"].pop("wind_turbine_details", None)
|
||||||
|
data["sap_energy_source"].pop("pv_battery_count", None)
|
||||||
|
for bp in data.get("sap_building_parts", []):
|
||||||
|
bp.pop("roof_insulation_thickness", None)
|
||||||
|
|
||||||
|
schema = from_dict(RdSapSchema21_0_0, data)
|
||||||
|
result = EpcPropertyDataMapper.from_rdsap_schema_21_0_0(schema)
|
||||||
|
|
||||||
|
assert result.uprn == 12457
|
||||||
|
# coalesced, not None → safe for the NOT-NULL epc_property column
|
||||||
|
assert result.wet_rooms_count == 0
|
||||||
|
assert result.open_chimneys_count == 0
|
||||||
|
|
||||||
def test_uprn(self, result: EpcPropertyData) -> None:
|
def test_uprn(self, result: EpcPropertyData) -> None:
|
||||||
assert result.uprn == 12457
|
assert result.uprn == 12457
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class EnergyElement:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Addendum:
|
class Addendum:
|
||||||
addendum_numbers: List[int]
|
addendum_numbers: Optional[List[int]] = None
|
||||||
stone_walls: Optional[str] = None
|
stone_walls: Optional[str] = None
|
||||||
system_build: Optional[str] = None
|
system_build: Optional[str] = None
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ class PhotovoltaicSupplyNoneOrNoDetails:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PhotovoltaicSupply:
|
class PhotovoltaicSupply:
|
||||||
none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails
|
none_or_no_details: Optional[PhotovoltaicSupplyNoneOrNoDetails] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -127,15 +127,15 @@ class PhotovoltaicArray:
|
||||||
overshading: int
|
overshading: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(kw_only=True)
|
||||||
class SapEnergySource:
|
class SapEnergySource:
|
||||||
mains_gas: str
|
mains_gas: str
|
||||||
meter_type: int
|
meter_type: int
|
||||||
pv_connection: int
|
pv_connection: int
|
||||||
pv_battery_count: int
|
pv_battery_count: Optional[int] = None
|
||||||
photovoltaic_supply: Union[PhotovoltaicSupply, List[List[PhotovoltaicArray]]]
|
photovoltaic_supply: Union[PhotovoltaicSupply, List[List[PhotovoltaicArray]]]
|
||||||
wind_turbines_count: int
|
wind_turbines_count: int
|
||||||
wind_turbine_details: WindTurbineDetails
|
wind_turbine_details: Optional[WindTurbineDetails] = None
|
||||||
gas_smart_meter_present: str
|
gas_smart_meter_present: str
|
||||||
is_dwelling_export_capable: str
|
is_dwelling_export_capable: str
|
||||||
wind_turbines_terrain_type: int
|
wind_turbines_terrain_type: int
|
||||||
|
|
@ -153,11 +153,8 @@ class WindowTransmissionDetails:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SapWindow:
|
class SapWindow:
|
||||||
pvc_frame: str
|
|
||||||
glazing_gap: int
|
|
||||||
orientation: int
|
orientation: int
|
||||||
window_type: int
|
window_type: int
|
||||||
frame_factor: float
|
|
||||||
glazing_type: int
|
glazing_type: int
|
||||||
window_width: float
|
window_width: float
|
||||||
window_height: float
|
window_height: float
|
||||||
|
|
@ -165,8 +162,15 @@ class SapWindow:
|
||||||
window_location: int
|
window_location: int
|
||||||
window_wall_type: int
|
window_wall_type: int
|
||||||
permanent_shutters_present: str
|
permanent_shutters_present: str
|
||||||
window_transmission_details: WindowTransmissionDetails
|
|
||||||
permanent_shutters_insulated: str
|
permanent_shutters_insulated: str
|
||||||
|
# Optional window refinements: real-API 21.0.0 certs routinely omit these
|
||||||
|
# (frame_factor → Table 6c fallback, transmission → SAP10 glazing lookup,
|
||||||
|
# glazing_gap → type-only U). Modelled required originally but defaulted in
|
||||||
|
# 21.0.1; mirror that so otherwise-complete cohort certs are not skipped.
|
||||||
|
pvc_frame: Optional[str] = None
|
||||||
|
glazing_gap: Optional[int] = None
|
||||||
|
frame_factor: Optional[float] = None
|
||||||
|
window_transmission_details: Optional[WindowTransmissionDetails] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -273,7 +277,7 @@ class SapAlternativeWall:
|
||||||
sheltered_wall: Optional[str] = None
|
sheltered_wall: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(kw_only=True)
|
||||||
class SapBuildingPart:
|
class SapBuildingPart:
|
||||||
identifier: str
|
identifier: str
|
||||||
wall_dry_lined: str
|
wall_dry_lined: str
|
||||||
|
|
@ -287,7 +291,7 @@ class SapBuildingPart:
|
||||||
party_wall_construction: Union[int, str]
|
party_wall_construction: Union[int, str]
|
||||||
wall_thickness_measured: str
|
wall_thickness_measured: str
|
||||||
roof_insulation_location: Union[int, str]
|
roof_insulation_location: Union[int, str]
|
||||||
roof_insulation_thickness: Union[str, int]
|
roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||||
sap_room_in_roof: Optional[SapRoomInRoof] = None
|
sap_room_in_roof: Optional[SapRoomInRoof] = None
|
||||||
sap_alternative_wall_1: Optional[SapAlternativeWall] = None
|
sap_alternative_wall_1: Optional[SapAlternativeWall] = None
|
||||||
sap_alternative_wall_2: Optional[SapAlternativeWall] = None
|
sap_alternative_wall_2: Optional[SapAlternativeWall] = None
|
||||||
|
|
@ -382,7 +386,9 @@ class RenewableHeatIncentive:
|
||||||
impact_of_solid_wall_insulation: Optional[int] = None
|
impact_of_solid_wall_insulation: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
# kw_only so optional fields (aligned with 21.0.1) can be defaulted in place
|
||||||
|
# without reordering; parsed only via `from_dict` (keyword construction).
|
||||||
|
@dataclass(kw_only=True)
|
||||||
class RdSapSchema21_0_0:
|
class RdSapSchema21_0_0:
|
||||||
uprn: int
|
uprn: int
|
||||||
roofs: List[EnergyElement]
|
roofs: List[EnergyElement]
|
||||||
|
|
@ -414,7 +420,7 @@ class RdSapSchema21_0_0:
|
||||||
assessment_type: str
|
assessment_type: str
|
||||||
completion_date: str
|
completion_date: str
|
||||||
inspection_date: str
|
inspection_date: str
|
||||||
wet_rooms_count: int
|
wet_rooms_count: Optional[int] = None
|
||||||
extensions_count: int
|
extensions_count: int
|
||||||
measurement_type: int
|
measurement_type: int
|
||||||
total_floor_area: int
|
total_floor_area: int
|
||||||
|
|
@ -425,7 +431,7 @@ class RdSapSchema21_0_0:
|
||||||
sap_energy_source: SapEnergySource
|
sap_energy_source: SapEnergySource
|
||||||
secondary_heating: EnergyElement
|
secondary_heating: EnergyElement
|
||||||
sap_building_parts: List[SapBuildingPart]
|
sap_building_parts: List[SapBuildingPart]
|
||||||
open_chimneys_count: int
|
open_chimneys_count: Optional[int] = None
|
||||||
solar_water_heating: str
|
solar_water_heating: str
|
||||||
habitable_room_count: int
|
habitable_room_count: int
|
||||||
heating_cost_current: float
|
heating_cost_current: float
|
||||||
|
|
@ -438,10 +444,10 @@ class RdSapSchema21_0_0:
|
||||||
has_hot_water_cylinder: str
|
has_hot_water_cylinder: str
|
||||||
heating_cost_potential: float
|
heating_cost_potential: float
|
||||||
hot_water_cost_current: float
|
hot_water_cost_current: float
|
||||||
insulated_door_u_value: float
|
insulated_door_u_value: Optional[float] = None
|
||||||
mechanical_ventilation: int
|
mechanical_ventilation: int
|
||||||
percent_draughtproofed: int
|
percent_draughtproofed: int
|
||||||
suggested_improvements: List[SuggestedImprovement]
|
suggested_improvements: Optional[List[SuggestedImprovement]] = None
|
||||||
co2_emissions_potential: float
|
co2_emissions_potential: float
|
||||||
energy_rating_potential: int
|
energy_rating_potential: int
|
||||||
lighting_cost_potential: float
|
lighting_cost_potential: float
|
||||||
|
|
@ -449,27 +455,27 @@ class RdSapSchema21_0_0:
|
||||||
hot_water_cost_potential: float
|
hot_water_cost_potential: float
|
||||||
renewable_heat_incentive: RenewableHeatIncentive
|
renewable_heat_incentive: RenewableHeatIncentive
|
||||||
draughtproofed_door_count: int
|
draughtproofed_door_count: int
|
||||||
mechanical_vent_duct_type: int
|
mechanical_vent_duct_type: Optional[int] = None
|
||||||
windows_transmission_details: WindowsTransmissionDetails
|
windows_transmission_details: Optional[WindowsTransmissionDetails] = None
|
||||||
cfl_fixed_lighting_bulbs_count: int
|
cfl_fixed_lighting_bulbs_count: Optional[int] = None
|
||||||
energy_consumption_current: int
|
energy_consumption_current: int
|
||||||
has_fixed_air_conditioning: str
|
has_fixed_air_conditioning: str
|
||||||
multiple_glazed_proportion: int
|
multiple_glazed_proportion: int
|
||||||
calculation_software_version: str
|
calculation_software_version: str
|
||||||
energy_consumption_potential: int
|
energy_consumption_potential: int
|
||||||
environmental_impact_current: int
|
environmental_impact_current: int
|
||||||
led_fixed_lighting_bulbs_count: int
|
led_fixed_lighting_bulbs_count: Optional[int] = None
|
||||||
mechanical_vent_duct_placement: int
|
mechanical_vent_duct_placement: Optional[int] = None
|
||||||
mechanical_vent_duct_insulation: int
|
mechanical_vent_duct_insulation: Optional[int] = None
|
||||||
potential_energy_efficiency_band: str
|
potential_energy_efficiency_band: str
|
||||||
pressure_test_certificate_number: int
|
pressure_test_certificate_number: Optional[int] = None
|
||||||
mechanical_ventilation_index_number: int
|
mechanical_ventilation_index_number: Optional[int] = None
|
||||||
co2_emissions_current_per_floor_area: int
|
co2_emissions_current_per_floor_area: int
|
||||||
current_energy_efficiency_band: str
|
current_energy_efficiency_band: str
|
||||||
environmental_impact_potential: int
|
environmental_impact_potential: int
|
||||||
low_energy_fixed_lighting_bulbs_count: int
|
low_energy_fixed_lighting_bulbs_count: Optional[int] = None
|
||||||
mechanical_vent_duct_insulation_level: int
|
mechanical_vent_duct_insulation_level: Optional[int] = None
|
||||||
mechanical_vent_measured_installation: str
|
mechanical_vent_measured_installation: Optional[str] = None
|
||||||
incandescent_fixed_lighting_bulbs_count: int
|
incandescent_fixed_lighting_bulbs_count: int
|
||||||
sap_flat_details: Optional[SapFlatDetails] = None
|
sap_flat_details: Optional[SapFlatDetails] = None
|
||||||
addendum: Optional[Addendum] = None
|
addendum: Optional[Addendum] = None
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue