Merge pull request #1051 from Hestia-Homes/feature/magicplan-api-client

MagicPlan: tweak domain model to align with db schema
This commit is contained in:
Jun-te Kim 2026-05-07 11:32:53 +01:00 committed by GitHub
commit 3de1de48cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 273588 additions and 37 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,10 +7,20 @@ def map_plan(mp: MagicPlan) -> Plan:
return Plan(
uid=mp.plan.id,
name=mp.plan.name,
address=_map_address(mp.plan.address),
postcode=mp.plan.address.postal_code if mp.plan.address else None,
floors=[_map_floor(f) for f in mp.plan_detail.plan.floors],
)
def _map_address(addr: api.Address | None) -> str | None:
if addr is None:
return None
street = " ".join(p for p in [addr.street_number, addr.street] if p) or None
parts = [p for p in [street, addr.city, addr.country] if p]
return ", ".join(parts) if parts else None
def _map_floor(f: api.Floor) -> Floor:
return Floor(
level=f.level,
@ -23,10 +33,12 @@ def _map_room(r: api.Room) -> Room:
width, length = _parse_dimensions(r.dimensions)
return Room(
name=r.name,
width=width,
length=length,
area=round(r.area, 2),
windows=[_map_window(wi) for wi in r.wall_items if wi.symbol.id.startswith("window")],
width_m=width,
length_m=length,
area_m2=round(r.area, 2),
windows=[
_map_window(wi) for wi in r.wall_items if wi.symbol.id.startswith("window")
],
doors=[_map_door(wi) for wi in r.wall_items if wi.symbol.id.startswith("door")],
)
@ -42,12 +54,12 @@ def _parse_dimensions(dimensions: str | None) -> tuple[float, float]:
def _map_window(wi: api.WallItem) -> Window:
return Window(
width=round(wi.size.x, 2),
height=round(wi.size.z, 2),
area=round(wi.size.x * wi.size.z, 2),
width_m=round(wi.size.x, 2),
height_m=round(wi.size.z, 2),
area_m2=round(wi.size.x * wi.size.z, 2),
opening_type=wi.symbol.id.removeprefix("window"),
)
def _map_door(wi: api.WallItem) -> Door:
return Door(width=round(wi.size.x, 2))
return Door(width_mm=round(wi.size.x, 2))

View file

@ -3,23 +3,23 @@ from dataclasses import dataclass, field
@dataclass
class Window:
width: float
height: float
area: float
width_m: float
height_m: float
area_m2: float
opening_type: str
@dataclass
class Door:
width: float
width_mm: float # TODO: should this be m or mm?
@dataclass
class Room:
name: str
width: float
length: float
area: float
width_m: float
length_m: float
area_m2: float
windows: list[Window] = field(default_factory=list[Window])
doors: list[Door] = field(default_factory=list[Door])
@ -35,4 +35,6 @@ class Floor:
class Plan:
uid: str
name: str | None
address: str | None = None
postcode: str | None = None
floors: list[Floor] = field(default_factory=list[Floor])

View file

@ -45,20 +45,20 @@ def test_first_room_name(plan: Plan):
def test_room_dimensions_are_floats(plan: Plan):
room = plan.floors[0].rooms[0]
assert isinstance(room.width, float)
assert isinstance(room.length, float)
assert isinstance(room.area, float)
assert isinstance(room.width_m, float)
assert isinstance(room.length_m, float)
assert isinstance(room.area_m2, float)
def test_room_area_rounded_to_2dp(plan: Plan):
room = plan.floors[0].rooms[0]
assert room.area == 7.95
assert room.area_m2 == 7.95
def test_room_dimensions_parsed_from_string(plan: Plan):
room = plan.floors[0].rooms[0]
assert room.width == pytest.approx(2.67)
assert room.length == pytest.approx(2.98)
assert room.width_m == pytest.approx(2.67)
assert room.length_m == pytest.approx(2.98)
def test_kitchen_has_windows(plan: Plan):
@ -68,9 +68,9 @@ def test_kitchen_has_windows(plan: Plan):
def test_window_fields_are_floats(plan: Plan):
window = plan.floors[0].rooms[0].windows[0]
assert isinstance(window.width, float)
assert isinstance(window.height, float)
assert isinstance(window.area, float)
assert isinstance(window.width_m, float)
assert isinstance(window.height_m, float)
assert isinstance(window.area_m2, float)
def test_window_opening_type_prefix_stripped(plan: Plan):
@ -81,19 +81,19 @@ def test_window_opening_type_prefix_stripped(plan: Plan):
def test_window_area_is_width_times_height(plan: Plan):
window = plan.floors[0].rooms[0].windows[0]
assert window.area == pytest.approx(window.width * window.height, rel=1e-2)
assert window.area_m2 == pytest.approx(window.width_m * window.height_m, rel=1e-2)
def test_window_dimensions_rounded_to_2dp(plan: Plan):
window = plan.floors[0].rooms[0].windows[0]
assert window.width == 1.40
assert window.height == 1.20
assert window.area == 1.68
assert window.width_m == 1.40
assert window.height_m == 1.20
assert window.area_m2 == 1.68
def test_door_width_rounded_to_2dp(plan: Plan):
door = plan.floors[0].rooms[0].doors[0]
assert door.width == 0.79
assert door.width_mm == 0.79
def test_kitchen_has_doors(plan: Plan):
@ -103,7 +103,7 @@ def test_kitchen_has_doors(plan: Plan):
def test_door_width_is_float(plan: Plan):
door = plan.floors[0].rooms[0].doors[0]
assert isinstance(door.width, float)
assert isinstance(door.width_mm, float)
# --- Fixture 2: magicplan_api_plan_response_example_2.json ---
@ -136,13 +136,13 @@ def test_plan2_first_room_name(plan2: Plan):
def test_plan2_room_area_rounded_to_2dp(plan2: Plan):
room = plan2.floors[0].rooms[0]
assert room.area == 0.96
assert room.area_m2 == 0.96
def test_plan2_room_dimensions_parsed_from_string(plan2: Plan):
room = plan2.floors[0].rooms[0]
assert room.width == pytest.approx(1.12)
assert room.length == pytest.approx(0.86)
assert room.width_m == pytest.approx(1.12)
assert room.length_m == pytest.approx(0.86)
def test_plan2_room_with_no_windows(plan2: Plan):
@ -153,9 +153,9 @@ def test_plan2_room_with_no_windows(plan2: Plan):
def test_plan2_window_dimensions_rounded_to_2dp(plan2: Plan):
window = plan2.floors[0].rooms[0].windows[0]
assert window.width == 0.39
assert window.height == 0.67
assert window.area == 0.26
assert window.width_m == 0.39
assert window.height_m == 0.67
assert window.area_m2 == 0.26
def test_plan2_window_opening_type_casement(plan2: Plan):
@ -171,4 +171,57 @@ def test_plan2_window_opening_type_hung(plan2: Plan):
def test_plan2_door_width_rounded_to_2dp(plan2: Plan):
door = plan2.floors[0].rooms[0].doors[0]
assert door.width == 0.71
assert door.width_mm == 0.71
# --- Address and postcode fields ---
def test_plan_postcode(plan: Plan):
assert plan.postcode == "BR2 8BZ"
def test_plan_address(plan: Plan):
assert plan.address == "2 Laburnum Way, Bromley, GB"
def test_plan2_postcode(plan2: Plan):
assert plan2.postcode == "BR1 3LP"
def test_plan2_address(plan2: Plan):
assert plan2.address == "11 Station Road, Bromley, GB"
# --- Fixture 3: street_number set, city absent ---
@pytest.fixture(scope="module")
def plan3() -> Plan:
payload = json.loads(
(FIXTURE_DIR / "magicplan_api_plan_response_example_3.json").read_text()
)
return map_plan(MagicPlan.model_validate(payload["data"]))
def test_plan3_address_uses_street_number_and_omits_city(plan3: Plan):
assert plan3.address == "2 Laburnum Way, GB"
def test_plan3_postcode(plan3: Plan):
assert plan3.postcode == "BR2 8BZ"
# --- Fixture 4: street_number set, street absent ---
@pytest.fixture(scope="module")
def plan4() -> Plan:
payload = json.loads(
(FIXTURE_DIR / "magicplan_api_plan_response_example_4.json").read_text()
)
return map_plan(MagicPlan.model_validate(payload["data"]))
def test_plan4_address_uses_street_number_when_street_absent(plan4: Plan):
assert plan4.address == "2, Bromley, GB"