Tolerate non-string building-part identifier (fix TypeError)

A SAP-16.x cohort cert (9258-4062-7265-2844-7954) lodges a bare int building-
part identifier (the second part as `1` after "Main Dwelling").
`BuildingPartIdentifier.from_api_string` regex-matched it assuming a string and
raised "TypeError: expected string or bytes-like object, got 'int'", failing the
whole property.

Fix: guard the match on `isinstance(api_identifier, str)` so a non-string
identifier falls to OTHER, matching the documented "anything unrecognised ->
OTHER" contract. The baseline SAP fabric sums all building parts regardless of
identifier, so OTHER is SAP-neutral; the identifier only labels parts for
measure targeting. Fixes every mapper (all route through from_api_string).

Cert now maps + calculates (sap 63). Regression test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-24 10:33:23 +00:00
parent 22cb47a280
commit 6d98ecf375
2 changed files with 34 additions and 4 deletions

View file

@ -35,15 +35,18 @@ class BuildingPartIdentifier(Enum):
OTHER = "other"
@classmethod
def from_api_string(cls, api_identifier: Optional[str]) -> "BuildingPartIdentifier":
def from_api_string(
cls, api_identifier: Optional[object]
) -> "BuildingPartIdentifier":
"""Map a gov-EPC API `BuildingPart.identifier` to its canonical
member. "Main Dwelling" MAIN; "Extension N" EXTENSION_N
(for N in 1..4). `None` (permitted by the 21_0_1 schema) and
anything unrecognised fall to OTHER.
(for N in 1..4). `None` (permitted by the 21_0_1 schema), a non-string
identifier (some SAP-16.x certs lodge a bare int), and anything else
unrecognised fall to OTHER.
"""
if api_identifier == "Main Dwelling":
return cls.MAIN
if api_identifier is not None:
if isinstance(api_identifier, str):
match = _API_EXTENSION.match(api_identifier)
if match is not None:
return cls.extension(int(match.group(1)))

View file

@ -0,0 +1,27 @@
from __future__ import annotations
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
def test_main_dwelling_maps_to_main() -> None:
assert BuildingPartIdentifier.from_api_string("Main Dwelling") == (
BuildingPartIdentifier.MAIN
)
def test_extension_string_maps_to_numbered_extension() -> None:
assert BuildingPartIdentifier.from_api_string("Extension 2") == (
BuildingPartIdentifier.EXTENSION_2
)
def test_none_falls_to_other() -> None:
assert BuildingPartIdentifier.from_api_string(None) == BuildingPartIdentifier.OTHER
def test_non_string_identifier_falls_to_other_not_crash() -> None:
# Some SAP-16.x certs lodge a bare int building-part identifier (e.g. the
# second part as `1` after "Main Dwelling"). This previously raised
# `TypeError: expected string or bytes-like object, got 'int'` and failed
# the whole property — now it falls to OTHER like any unrecognised value.
assert BuildingPartIdentifier.from_api_string(1) == BuildingPartIdentifier.OTHER