mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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:
commit
3de1de48cd
5 changed files with 273588 additions and 37 deletions
136742
backend/magic_plan/magicplan_api_plan_response_example_3.json
Normal file
136742
backend/magic_plan/magicplan_api_plan_response_example_3.json
Normal file
File diff suppressed because one or more lines are too long
136742
backend/magic_plan/magicplan_api_plan_response_example_4.json
Normal file
136742
backend/magic_plan/magicplan_api_plan_response_example_4.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,10 +7,20 @@ def map_plan(mp: MagicPlan) -> Plan:
|
||||||
return Plan(
|
return Plan(
|
||||||
uid=mp.plan.id,
|
uid=mp.plan.id,
|
||||||
name=mp.plan.name,
|
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],
|
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:
|
def _map_floor(f: api.Floor) -> Floor:
|
||||||
return Floor(
|
return Floor(
|
||||||
level=f.level,
|
level=f.level,
|
||||||
|
|
@ -23,10 +33,12 @@ def _map_room(r: api.Room) -> Room:
|
||||||
width, length = _parse_dimensions(r.dimensions)
|
width, length = _parse_dimensions(r.dimensions)
|
||||||
return Room(
|
return Room(
|
||||||
name=r.name,
|
name=r.name,
|
||||||
width=width,
|
width_m=width,
|
||||||
length=length,
|
length_m=length,
|
||||||
area=round(r.area, 2),
|
area_m2=round(r.area, 2),
|
||||||
windows=[_map_window(wi) for wi in r.wall_items if wi.symbol.id.startswith("window")],
|
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")],
|
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:
|
def _map_window(wi: api.WallItem) -> Window:
|
||||||
return Window(
|
return Window(
|
||||||
width=round(wi.size.x, 2),
|
width_m=round(wi.size.x, 2),
|
||||||
height=round(wi.size.z, 2),
|
height_m=round(wi.size.z, 2),
|
||||||
area=round(wi.size.x * wi.size.z, 2),
|
area_m2=round(wi.size.x * wi.size.z, 2),
|
||||||
opening_type=wi.symbol.id.removeprefix("window"),
|
opening_type=wi.symbol.id.removeprefix("window"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _map_door(wi: api.WallItem) -> Door:
|
def _map_door(wi: api.WallItem) -> Door:
|
||||||
return Door(width=round(wi.size.x, 2))
|
return Door(width_mm=round(wi.size.x, 2))
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,23 @@ from dataclasses import dataclass, field
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Window:
|
class Window:
|
||||||
width: float
|
width_m: float
|
||||||
height: float
|
height_m: float
|
||||||
area: float
|
area_m2: float
|
||||||
opening_type: str
|
opening_type: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Door:
|
class Door:
|
||||||
width: float
|
width_mm: float # TODO: should this be m or mm?
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Room:
|
class Room:
|
||||||
name: str
|
name: str
|
||||||
width: float
|
width_m: float
|
||||||
length: float
|
length_m: float
|
||||||
area: float
|
area_m2: float
|
||||||
windows: list[Window] = field(default_factory=list[Window])
|
windows: list[Window] = field(default_factory=list[Window])
|
||||||
doors: list[Door] = field(default_factory=list[Door])
|
doors: list[Door] = field(default_factory=list[Door])
|
||||||
|
|
||||||
|
|
@ -35,4 +35,6 @@ class Floor:
|
||||||
class Plan:
|
class Plan:
|
||||||
uid: str
|
uid: str
|
||||||
name: str | None
|
name: str | None
|
||||||
|
address: str | None = None
|
||||||
|
postcode: str | None = None
|
||||||
floors: list[Floor] = field(default_factory=list[Floor])
|
floors: list[Floor] = field(default_factory=list[Floor])
|
||||||
|
|
|
||||||
|
|
@ -45,20 +45,20 @@ def test_first_room_name(plan: Plan):
|
||||||
|
|
||||||
def test_room_dimensions_are_floats(plan: Plan):
|
def test_room_dimensions_are_floats(plan: Plan):
|
||||||
room = plan.floors[0].rooms[0]
|
room = plan.floors[0].rooms[0]
|
||||||
assert isinstance(room.width, float)
|
assert isinstance(room.width_m, float)
|
||||||
assert isinstance(room.length, float)
|
assert isinstance(room.length_m, float)
|
||||||
assert isinstance(room.area, float)
|
assert isinstance(room.area_m2, float)
|
||||||
|
|
||||||
|
|
||||||
def test_room_area_rounded_to_2dp(plan: Plan):
|
def test_room_area_rounded_to_2dp(plan: Plan):
|
||||||
room = plan.floors[0].rooms[0]
|
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):
|
def test_room_dimensions_parsed_from_string(plan: Plan):
|
||||||
room = plan.floors[0].rooms[0]
|
room = plan.floors[0].rooms[0]
|
||||||
assert room.width == pytest.approx(2.67)
|
assert room.width_m == pytest.approx(2.67)
|
||||||
assert room.length == pytest.approx(2.98)
|
assert room.length_m == pytest.approx(2.98)
|
||||||
|
|
||||||
|
|
||||||
def test_kitchen_has_windows(plan: Plan):
|
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):
|
def test_window_fields_are_floats(plan: Plan):
|
||||||
window = plan.floors[0].rooms[0].windows[0]
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
assert isinstance(window.width, float)
|
assert isinstance(window.width_m, float)
|
||||||
assert isinstance(window.height, float)
|
assert isinstance(window.height_m, float)
|
||||||
assert isinstance(window.area, float)
|
assert isinstance(window.area_m2, float)
|
||||||
|
|
||||||
|
|
||||||
def test_window_opening_type_prefix_stripped(plan: Plan):
|
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):
|
def test_window_area_is_width_times_height(plan: Plan):
|
||||||
window = plan.floors[0].rooms[0].windows[0]
|
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):
|
def test_window_dimensions_rounded_to_2dp(plan: Plan):
|
||||||
window = plan.floors[0].rooms[0].windows[0]
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
assert window.width == 1.40
|
assert window.width_m == 1.40
|
||||||
assert window.height == 1.20
|
assert window.height_m == 1.20
|
||||||
assert window.area == 1.68
|
assert window.area_m2 == 1.68
|
||||||
|
|
||||||
|
|
||||||
def test_door_width_rounded_to_2dp(plan: Plan):
|
def test_door_width_rounded_to_2dp(plan: Plan):
|
||||||
door = plan.floors[0].rooms[0].doors[0]
|
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):
|
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):
|
def test_door_width_is_float(plan: Plan):
|
||||||
door = plan.floors[0].rooms[0].doors[0]
|
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 ---
|
# --- 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):
|
def test_plan2_room_area_rounded_to_2dp(plan2: Plan):
|
||||||
room = plan2.floors[0].rooms[0]
|
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):
|
def test_plan2_room_dimensions_parsed_from_string(plan2: Plan):
|
||||||
room = plan2.floors[0].rooms[0]
|
room = plan2.floors[0].rooms[0]
|
||||||
assert room.width == pytest.approx(1.12)
|
assert room.width_m == pytest.approx(1.12)
|
||||||
assert room.length == pytest.approx(0.86)
|
assert room.length_m == pytest.approx(0.86)
|
||||||
|
|
||||||
|
|
||||||
def test_plan2_room_with_no_windows(plan2: Plan):
|
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):
|
def test_plan2_window_dimensions_rounded_to_2dp(plan2: Plan):
|
||||||
window = plan2.floors[0].rooms[0].windows[0]
|
window = plan2.floors[0].rooms[0].windows[0]
|
||||||
assert window.width == 0.39
|
assert window.width_m == 0.39
|
||||||
assert window.height == 0.67
|
assert window.height_m == 0.67
|
||||||
assert window.area == 0.26
|
assert window.area_m2 == 0.26
|
||||||
|
|
||||||
|
|
||||||
def test_plan2_window_opening_type_casement(plan2: Plan):
|
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):
|
def test_plan2_door_width_rounded_to_2dp(plan2: Plan):
|
||||||
door = plan2.floors[0].rooms[0].doors[0]
|
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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue