Parse Magic Plan XML 🟩

This commit is contained in:
Daniel Roth 2026-04-28 15:11:03 +00:00
parent 4b3b100d63
commit f6ab407b33
4 changed files with 373 additions and 173 deletions

View file

View file

@ -1,23 +1,11 @@
import json
import math
import xml.etree.ElementTree as ET
from pathlib import Path
import pytest
from backend.magic_plan.models import (
MagicPlanDoor,
MagicPlanExploded,
MagicPlanExplodedOpening,
MagicPlanExplodedWall,
MagicPlanFloor,
MagicPlanFurniture,
MagicPlanMainDimension,
MagicPlanPlan,
MagicPlanRoom,
MagicPlanRoomPoint,
MagicPlanSymbolInstance,
MagicPlanWallPoint,
MagicPlanWindow,
)
from backend.magic_plan.models import MagicPlanPlan
from backend.magic_plan.xml_parser import parse_magicplan_xml
FIXTURES = Path(__file__).parent.parent
@ -29,7 +17,7 @@ def _load(filename: str) -> str:
ET.fromstring(content)
return content
except ET.ParseError:
return json.loads(content)
return json.loads(content) # type: ignore[return-value]
@pytest.fixture(scope="module")
@ -53,55 +41,55 @@ def plan3() -> MagicPlanPlan:
class TestPlanAttributes:
def test_id(self, plan1: MagicPlanPlan):
def test_id(self, plan1: MagicPlanPlan) -> None:
assert plan1.id == "b66bb427-33fe-4865-8d57-bad7d9d3f2e5"
def test_uid(self, plan1: MagicPlanPlan):
def test_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.uid == "69e5fafc.0890b3ff"
def test_name(self, plan1: MagicPlanPlan):
def test_name(self, plan1: MagicPlanPlan) -> None:
assert plan1.name == "275 Carr Hill Rd NE9 5ND"
def test_type(self, plan1: MagicPlanPlan):
def test_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.type == "0"
def test_interior_wall_width(self, plan1: MagicPlanPlan):
assert plan1.interior_wall_width == pytest.approx(0.12)
def test_interior_wall_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.interior_wall_width, 0.12)
def test_exterior_wall_width(self, plan1: MagicPlanPlan):
assert plan1.exterior_wall_width == pytest.approx(0.25)
def test_exterior_wall_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.exterior_wall_width, 0.25)
def test_schematic_false(self, plan1: MagicPlanPlan):
def test_schematic_false(self, plan1: MagicPlanPlan) -> None:
assert plan1.schematic is False
def test_has_land_survey_address_false(self, plan1: MagicPlanPlan):
def test_has_land_survey_address_false(self, plan1: MagicPlanPlan) -> None:
assert plan1.has_land_survey_address is False
def test_last_patch_identifier(self, plan1: MagicPlanPlan):
def test_last_patch_identifier(self, plan1: MagicPlanPlan) -> None:
assert plan1.last_patch_identifier == "0"
def test_last_roll_identifier(self, plan1: MagicPlanPlan):
def test_last_roll_identifier(self, plan1: MagicPlanPlan) -> None:
assert plan1.last_roll_identifier == "0"
class TestPlanValues:
def test_date(self, plan1: MagicPlanPlan):
def test_date(self, plan1: MagicPlanPlan) -> None:
assert plan1.values["date"] == "2026-04-20"
def test_statistics_area_of_height(self, plan1: MagicPlanPlan):
def test_statistics_area_of_height(self, plan1: MagicPlanPlan) -> None:
assert plan1.values["statistics.areaOfHeight"] == "2.134"
def test_statistics_basement_account(self, plan1: MagicPlanPlan):
def test_statistics_basement_account(self, plan1: MagicPlanPlan) -> None:
assert plan1.values["statistics.basement.account"] == "100"
def test_statistics_exterior_walls(self, plan1: MagicPlanPlan):
def test_statistics_exterior_walls(self, plan1: MagicPlanPlan) -> None:
assert plan1.values["statistics.exteriorWalls"] == "0"
def test_statistics_interior_walls(self, plan1: MagicPlanPlan):
def test_statistics_interior_walls(self, plan1: MagicPlanPlan) -> None:
assert plan1.values["statistics.interiorWalls"] == "0"
def test_plan2_date(self, plan2: MagicPlanPlan):
def test_plan2_date(self, plan2: MagicPlanPlan) -> None:
assert plan2.values["date"] == "2026-04-16"
@ -111,44 +99,44 @@ class TestPlanValues:
class TestFloors:
def test_floor_count_plan1(self, plan1: MagicPlanPlan):
def test_floor_count_plan1(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors) == 2
def test_floor_count_plan2(self, plan2: MagicPlanPlan):
def test_floor_count_plan2(self, plan2: MagicPlanPlan) -> None:
assert len(plan2.floors) == 1
def test_floor_count_plan3(self, plan3: MagicPlanPlan):
def test_floor_count_plan3(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors) == 1
def test_ground_floor_name(self, plan1: MagicPlanPlan):
def test_ground_floor_name(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].name == "Ground Floor"
def test_first_floor_name(self, plan1: MagicPlanPlan):
def test_first_floor_name(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[1].name == "1st Floor"
def test_ground_floor_type(self, plan1: MagicPlanPlan):
def test_ground_floor_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].floor_type == "0"
def test_upper_floor_type(self, plan1: MagicPlanPlan):
def test_upper_floor_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[1].floor_type == "1"
def test_floor_uid(self, plan1: MagicPlanPlan):
def test_floor_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].uid == "69e5fb20.4feef7ff"
def test_ground_floor_area_without_walls(self, plan1: MagicPlanPlan):
assert plan1.floors[0].area_without_walls == pytest.approx(40.20736)
def test_ground_floor_area_without_walls(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].area_without_walls, 40.20736)
def test_ground_floor_area_with_walls(self, plan1: MagicPlanPlan):
assert plan1.floors[0].area_with_walls == pytest.approx(48.40593)
def test_ground_floor_area_with_walls(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].area_with_walls, 48.40593)
def test_ground_floor_area_with_interior_walls_only(self, plan1: MagicPlanPlan):
assert plan1.floors[0].area_with_interior_walls_only == pytest.approx(40.67878)
def test_ground_floor_area_with_interior_walls_only(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].area_with_interior_walls_only, 40.67878)
def test_floor_rotation(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rotation == pytest.approx(0.0)
def test_floor_rotation(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rotation, 0.0, abs_tol=1e-9)
def test_floor_compass_angle(self, plan1: MagicPlanPlan):
assert plan1.floors[0].compass_angle == pytest.approx(-1.0)
def test_floor_compass_angle(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].compass_angle, -1.0)
# ---------------------------------------------------------------------------
@ -157,19 +145,19 @@ class TestFloors:
class TestSymbolInstance:
def test_symbol_instance_id(self, plan1: MagicPlanPlan):
def test_symbol_instance_id(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].symbol_instance.id == "floor"
def test_symbol_instance_uid(self, plan1: MagicPlanPlan):
def test_symbol_instance_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].symbol_instance.uid == "69e5fb20.4feef7ff"
def test_symbol_instance_symbol(self, plan1: MagicPlanPlan):
def test_symbol_instance_symbol(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].symbol_instance.symbol == "floor"
def test_symbol_instance_parent_uid(self, plan1: MagicPlanPlan):
def test_symbol_instance_parent_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].symbol_instance.parent_uid == ""
def test_symbol_instance_ceiling_height_value(self, plan1: MagicPlanPlan):
def test_symbol_instance_ceiling_height_value(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].symbol_instance.values["ceilingHeight"] == "2.323164"
@ -179,60 +167,59 @@ class TestSymbolInstance:
class TestRooms:
def test_ground_floor_room_count(self, plan1: MagicPlanPlan):
def test_ground_floor_room_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms) == 2
def test_first_floor_room_count(self, plan1: MagicPlanPlan):
def test_first_floor_room_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[1].rooms) == 4
def test_plan3_room_count(self, plan3: MagicPlanPlan):
def test_plan3_room_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].rooms) == 9
def test_room_type(self, plan1: MagicPlanPlan):
def test_room_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].type == "Kitchen"
def test_room_uid(self, plan1: MagicPlanPlan):
def test_room_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].uid == "69e5fbc8.71027bff"
def test_room_area(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].area == pytest.approx(10.78332)
def test_room_area(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].area, 10.78332)
def test_room_perimeter(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].perimeter == pytest.approx(13.32812)
def test_room_perimeter(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].perimeter, 13.32812)
def test_room_x(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].x == pytest.approx(3.80616)
def test_room_x(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].x, 3.80616)
def test_room_y(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].y == pytest.approx(0.23162)
def test_room_y(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].y, 0.23162)
def test_room_rotation(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].rotation == pytest.approx(0.0)
def test_room_rotation(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].rotation, 0.0, abs_tol=1e-9)
def test_room_was_modified_false(self, plan1: MagicPlanPlan):
def test_room_was_modified_false(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].was_modified is False
def test_room_was_modified_true(self, plan1: MagicPlanPlan):
# Closet on 1st floor has wasModified=1
def test_room_was_modified_true(self, plan1: MagicPlanPlan) -> None:
closet = plan1.floors[1].rooms[1]
assert closet.type == "Closet"
assert closet.was_modified is True
def test_room_linked_room_0(self, plan1: MagicPlanPlan):
def test_room_linked_room_0(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].linked_room_0 == "-1"
def test_room_linked_room_1(self, plan1: MagicPlanPlan):
def test_room_linked_room_1(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].linked_room_1 == "-1"
def test_room_ceiling_height_value(self, plan1: MagicPlanPlan):
def test_room_ceiling_height_value(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].values["ceilingHeight"] == "2.323164"
def test_room_label_value(self, plan2: MagicPlanPlan):
def test_room_label_value(self, plan2: MagicPlanPlan) -> None:
dining = plan2.floors[0].rooms[1]
assert dining.type == "Dining Room"
assert dining.values["label"] == "Room"
def test_room_no_label_when_absent(self, plan1: MagicPlanPlan):
def test_room_no_label_when_absent(self, plan1: MagicPlanPlan) -> None:
assert "label" not in plan1.floors[0].rooms[0].values
@ -242,25 +229,25 @@ class TestRooms:
class TestRoomPoints:
def test_kitchen_point_count(self, plan1: MagicPlanPlan):
def test_kitchen_point_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[0].points) == 4
def test_living_room_point_count(self, plan1: MagicPlanPlan):
def test_living_room_point_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[1].points) == 8
def test_point_snapped_x(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].points[0].snapped_x == pytest.approx(-1.44357)
def test_point_snapped_x(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].points[0].snapped_x, -1.44357)
def test_point_snapped_y(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].points[0].snapped_y == pytest.approx(-2.00846)
def test_point_snapped_y(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].points[0].snapped_y, -2.00846)
def test_point_height(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].points[0].height == pytest.approx(2.323164)
def test_point_height(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].points[0].height, 2.323164)
def test_point_uid(self, plan1: MagicPlanPlan):
def test_point_uid(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].points[0].uid == "69e5fbc8.710363ff"
def test_point_values_empty_when_absent(self, plan1: MagicPlanPlan):
def test_point_values_empty_when_absent(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].points[0].values == {}
@ -270,42 +257,41 @@ class TestRoomPoints:
class TestDoors:
def test_kitchen_door_count(self, plan1: MagicPlanPlan):
def test_kitchen_door_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[0].doors) == 5
def test_corridor_door_count(self, plan3: MagicPlanPlan):
def test_corridor_door_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].rooms[0].doors) == 9
def test_door_wall_point_index(self, plan1: MagicPlanPlan):
def test_door_wall_point_index(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].doors[0].wall_point_index == 3
def test_door_type(self, plan1: MagicPlanPlan):
def test_door_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].doors[0].type == "4"
def test_door_u(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].doors[0].u == pytest.approx(0.47585)
def test_door_u(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].doors[0].u, 0.47585)
def test_door_snapped_width(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].doors[0].snapped_width == pytest.approx(1.82394)
def test_door_snapped_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].doors[0].snapped_width, 1.82394)
def test_door_snapped_height(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].doors[0].snapped_height == pytest.approx(2.08411)
def test_door_snapped_height(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].doors[0].snapped_height, 2.08411)
def test_door_snapped_orientation(self, plan1: MagicPlanPlan):
def test_door_snapped_orientation(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].doors[0].snapped_orientation == 3
def test_door_twin_wall_item_uid_present(self, plan1: MagicPlanPlan):
def test_door_twin_wall_item_uid_present(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].doors[0].twin_wall_item_uid == "69e5fbc8.74614fff"
def test_door_twin_wall_item_uid_absent(self, plan2: MagicPlanPlan):
# Example 2 doors have no twinWallItemUid
def test_door_twin_wall_item_uid_absent(self, plan2: MagicPlanPlan) -> None:
assert plan2.floors[0].rooms[0].doors[0].twin_wall_item_uid is None
def test_door_symbol_instance(self, plan1: MagicPlanPlan):
def test_door_symbol_instance(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].doors[0].symbol_instance == "W-0-0"
def test_door_inset_x(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].doors[0].inset_x == pytest.approx(0.0)
def test_door_inset_x(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].doors[0].inset_x, 0.0, abs_tol=1e-9)
# ---------------------------------------------------------------------------
@ -314,30 +300,30 @@ class TestDoors:
class TestWindows:
def test_kitchen_window_count(self, plan1: MagicPlanPlan):
def test_kitchen_window_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[0].windows) == 1
def test_corridor_window_count_zero(self, plan3: MagicPlanPlan):
def test_corridor_window_count_zero(self, plan3: MagicPlanPlan) -> None:
corridor = plan3.floors[0].rooms[0]
assert corridor.type == "Corridor"
assert len(corridor.windows) == 0
def test_window_wall_point_index(self, plan1: MagicPlanPlan):
def test_window_wall_point_index(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].windows[0].wall_point_index == 1
def test_window_type(self, plan1: MagicPlanPlan):
def test_window_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].windows[0].type == "1"
def test_window_u(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].windows[0].u == pytest.approx(0.68463)
def test_window_u(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].windows[0].u, 0.68463)
def test_window_snapped_width(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].windows[0].snapped_width == pytest.approx(1.71972)
def test_window_snapped_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].windows[0].snapped_width, 1.71972)
def test_window_snapped_height(self, plan1: MagicPlanPlan):
assert plan1.floors[0].rooms[0].windows[0].snapped_height == pytest.approx(0.94940)
def test_window_snapped_height(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].rooms[0].windows[0].snapped_height, 0.94940)
def test_window_symbol_instance(self, plan1: MagicPlanPlan):
def test_window_symbol_instance(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].rooms[0].windows[0].symbol_instance == "W-0-2"
@ -347,21 +333,19 @@ class TestWindows:
class TestRoomFurniture:
def test_kitchen_furniture_count(self, plan1: MagicPlanPlan):
def test_kitchen_furniture_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[0].furniture) == 5
def test_bathroom_furniture_count_zero(self, plan1: MagicPlanPlan):
def test_bathroom_furniture_count_zero(self, plan1: MagicPlanPlan) -> None:
bathroom = plan1.floors[1].rooms[0]
assert bathroom.type == "Bathroom"
assert len(bathroom.furniture) == 0
def test_furniture_type(self, plan1: MagicPlanPlan):
furn = plan1.floors[0].rooms[0].furniture[0]
assert isinstance(furn.type, str)
def test_furniture_type(self, plan1: MagicPlanPlan) -> None:
assert isinstance(plan1.floors[0].rooms[0].furniture[0].type, str)
def test_furniture_symbol_instance(self, plan1: MagicPlanPlan):
furn = plan1.floors[0].rooms[0].furniture[0]
assert isinstance(furn.symbol_instance, str)
def test_furniture_symbol_instance(self, plan1: MagicPlanPlan) -> None:
assert isinstance(plan1.floors[0].rooms[0].furniture[0].symbol_instance, str)
# ---------------------------------------------------------------------------
@ -370,16 +354,16 @@ class TestRoomFurniture:
class TestFloorLevelFurniture:
def test_floor_furniture_count(self, plan3: MagicPlanPlan):
def test_floor_furniture_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].furniture) == 1
def test_floor_furniture_x(self, plan3: MagicPlanPlan):
assert plan3.floors[0].furniture[0].x == pytest.approx(-2.09376)
def test_floor_furniture_x(self, plan3: MagicPlanPlan) -> None:
assert math.isclose(plan3.floors[0].furniture[0].x, -2.09376)
def test_floor_furniture_y(self, plan3: MagicPlanPlan):
assert plan3.floors[0].furniture[0].y == pytest.approx(3.13664)
def test_floor_furniture_y(self, plan3: MagicPlanPlan) -> None:
assert math.isclose(plan3.floors[0].furniture[0].y, 3.13664)
def test_floor_furniture_absent_plan1(self, plan1: MagicPlanPlan):
def test_floor_furniture_absent_plan1(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].furniture) == 0
@ -389,16 +373,14 @@ class TestFloorLevelFurniture:
class TestMainDimensions:
def test_main_dimension_count(self, plan1: MagicPlanPlan):
def test_main_dimension_count(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].rooms[0].main_dimensions) == 2
def test_main_dimension_from_point(self, plan1: MagicPlanPlan):
dim = plan1.floors[0].rooms[0].main_dimensions[0]
assert isinstance(dim.from_point, int)
def test_main_dimension_from_point(self, plan1: MagicPlanPlan) -> None:
assert isinstance(plan1.floors[0].rooms[0].main_dimensions[0].from_point, int)
def test_main_dimension_is_set(self, plan1: MagicPlanPlan):
dim = plan1.floors[0].rooms[0].main_dimensions[0]
assert isinstance(dim.is_set, bool)
def test_main_dimension_is_set(self, plan1: MagicPlanPlan) -> None:
assert isinstance(plan1.floors[0].rooms[0].main_dimensions[0].is_set, bool)
# ---------------------------------------------------------------------------
@ -407,59 +389,59 @@ class TestMainDimensions:
class TestExploded:
def test_exploded_wall_count_ground_floor(self, plan1: MagicPlanPlan):
def test_exploded_wall_count_ground_floor(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].exploded.walls) == 11
def test_exploded_door_count_ground_floor(self, plan1: MagicPlanPlan):
def test_exploded_door_count_ground_floor(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].exploded.doors) == 6
def test_exploded_window_count_ground_floor(self, plan1: MagicPlanPlan):
def test_exploded_window_count_ground_floor(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.floors[0].exploded.windows) == 2
def test_exploded_wall_type(self, plan1: MagicPlanPlan):
def test_exploded_wall_type(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].exploded.walls[0].wall_type == "exterior"
def test_exploded_wall_points(self, plan1: MagicPlanPlan):
def test_exploded_wall_points(self, plan1: MagicPlanPlan) -> None:
pts = plan1.floors[0].exploded.walls[0].points
assert len(pts) == 2
assert pts[0].x == pytest.approx(2.363)
assert pts[0].y == pytest.approx(-1.778)
assert pts[0].height == pytest.approx(2.323164)
assert math.isclose(pts[0].x, 2.363)
assert math.isclose(pts[0].y, -1.778)
assert math.isclose(pts[0].height, 2.323164)
def test_exploded_door_x1(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.doors[0].x1 == pytest.approx(5.24973)
def test_exploded_door_x1(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.doors[0].x1, 5.24973)
def test_exploded_door_y1(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.doors[0].y1 == pytest.approx(-1.333153)
def test_exploded_door_y1(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.doors[0].y1, -1.333153)
def test_exploded_door_width(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.doors[0].width == pytest.approx(0.773)
def test_exploded_door_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.doors[0].width, 0.773)
def test_exploded_door_height(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.doors[0].height == pytest.approx(2.02225)
def test_exploded_door_height(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.doors[0].height, 2.02225)
def test_exploded_door_symbol_instance(self, plan1: MagicPlanPlan):
def test_exploded_door_symbol_instance(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].exploded.doors[0].symbol_instance == "W-0-1"
def test_exploded_window_x1(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.windows[0].x1 == pytest.approx(5.24973)
def test_exploded_window_x1(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.windows[0].x1, 5.24973)
def test_exploded_window_width(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.windows[0].width == pytest.approx(1.71972)
def test_exploded_window_width(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.windows[0].width, 1.71972)
def test_exploded_window_height(self, plan1: MagicPlanPlan):
assert plan1.floors[0].exploded.windows[0].height == pytest.approx(0.949396)
def test_exploded_window_height(self, plan1: MagicPlanPlan) -> None:
assert math.isclose(plan1.floors[0].exploded.windows[0].height, 0.949396)
def test_exploded_window_symbol_instance(self, plan1: MagicPlanPlan):
def test_exploded_window_symbol_instance(self, plan1: MagicPlanPlan) -> None:
assert plan1.floors[0].exploded.windows[0].symbol_instance == "W-0-2"
def test_plan3_exploded_wall_count(self, plan3: MagicPlanPlan):
def test_plan3_exploded_wall_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].exploded.walls) == 33
def test_plan3_exploded_door_count(self, plan3: MagicPlanPlan):
def test_plan3_exploded_door_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].exploded.doors) == 12
def test_plan3_exploded_window_count(self, plan3: MagicPlanPlan):
def test_plan3_exploded_window_count(self, plan3: MagicPlanPlan) -> None:
assert len(plan3.floors[0].exploded.windows) == 5
@ -469,12 +451,11 @@ class TestExploded:
class TestInteriorRoomPoints:
def test_interior_room_floor_count_plan1(self, plan1: MagicPlanPlan):
def test_interior_room_floor_count_plan1(self, plan1: MagicPlanPlan) -> None:
assert len(plan1.interior_room_floors) == 2
def test_interior_room_floor_count_plan2(self, plan2: MagicPlanPlan):
def test_interior_room_floor_count_plan2(self, plan2: MagicPlanPlan) -> None:
assert len(plan2.interior_room_floors) == 1
def test_interior_room_floor_uid_matches(self, plan1: MagicPlanPlan):
# interiorRoomPoints floors share uids with the main floors
def test_interior_room_floor_uid_matches(self, plan1: MagicPlanPlan) -> None:
assert plan1.interior_room_floors[0].uid == plan1.floors[0].uid

View file

@ -1,5 +1,224 @@
from backend.magic_plan.models import MagicPlanPlan
import xml.etree.ElementTree as ET
from backend.magic_plan.models import (
MagicPlanDoor,
MagicPlanExploded,
MagicPlanExplodedOpening,
MagicPlanExplodedWall,
MagicPlanFloor,
MagicPlanFurniture,
MagicPlanMainDimension,
MagicPlanPlan,
MagicPlanRoom,
MagicPlanRoomPoint,
MagicPlanSymbolInstance,
MagicPlanWallPoint,
MagicPlanWindow,
)
def _values(el: ET.Element) -> dict[str, str]:
return {v.get("key", ""): (v.text or "") for v in el.findall("values/value")}
def _parse_room_point(el: ET.Element) -> MagicPlanRoomPoint:
return MagicPlanRoomPoint(
snapped_x=float(el.get("snappedX", "0")),
snapped_y=float(el.get("snappedY", "0")),
height=float(el.get("height", "0")),
uid=el.get("uid", ""),
values=_values(el),
)
def _parse_wall_point(el: ET.Element) -> MagicPlanWallPoint:
return MagicPlanWallPoint(
x=float(el.get("x", "0")),
y=float(el.get("y", "0")),
height=float(el.get("height", "0")),
)
def _parse_door(el: ET.Element) -> MagicPlanDoor:
return MagicPlanDoor(
wall_point_index=int(el.get("point", "0")),
type=el.get("type", ""),
u=float(el.get("u", "0")),
width=float(el.get("width", "0")),
depth=float(el.get("depth", "0")),
height=float(el.get("height", "0")),
orientation=int(el.get("orientation", "0")),
snapped_type=el.get("snappedType", ""),
snapped_position=float(el.get("snappedPosition", "0")),
snapped_width=float(el.get("snappedWidth", "0")),
snapped_depth=float(el.get("snappedDepth", "0")),
snapped_height=float(el.get("snappedHeight", "0")),
snapped_orientation=int(el.get("snappedOrientation", "0")),
inset_x=float(el.get("insetX", "0")),
inset_y=float(el.get("insetY", "0")),
inset_z=float(el.get("insetZ", "0")),
symbol_instance=el.get("symbolInstance", ""),
twin_wall_item_uid=el.get("twinWallItemUid"),
)
def _parse_window(el: ET.Element) -> MagicPlanWindow:
return MagicPlanWindow(
wall_point_index=int(el.get("point", "0")),
type=el.get("type", ""),
u=float(el.get("u", "0")),
width=float(el.get("width", "0")),
depth=float(el.get("depth", "0")),
height=float(el.get("height", "0")),
orientation=int(el.get("orientation", "0")),
snapped_type=el.get("snappedType", ""),
snapped_position=float(el.get("snappedPosition", "0")),
snapped_width=float(el.get("snappedWidth", "0")),
snapped_depth=float(el.get("snappedDepth", "0")),
snapped_height=float(el.get("snappedHeight", "0")),
snapped_orientation=int(el.get("snappedOrientation", "0")),
inset_x=float(el.get("insetX", "0")),
inset_y=float(el.get("insetY", "0")),
inset_z=float(el.get("insetZ", "0")),
symbol_instance=el.get("symbolInstance", ""),
)
def _parse_exploded_opening(el: ET.Element) -> MagicPlanExplodedOpening:
return MagicPlanExplodedOpening(
type=el.get("type", ""),
x1=float(el.get("x1", "0")),
y1=float(el.get("y1", "0")),
x2=float(el.get("x2", "0")),
y2=float(el.get("y2", "0")),
width=float(el.get("width", "0")),
depth=float(el.get("depth", "0")),
height=float(el.get("height", "0")),
inset_x=float(el.get("insetX", "0")),
inset_y=float(el.get("insetY", "0")),
orientation=int(el.get("orientation", "0")),
symbol_instance=el.get("symbolInstance", ""),
)
def _parse_furniture(el: ET.Element) -> MagicPlanFurniture:
return MagicPlanFurniture(
type=el.get("type", ""),
x=float(el.get("x", "0")),
y=float(el.get("y", "0")),
snapped_x=float(el.get("snappedX", "0")),
snapped_y=float(el.get("snappedY", "0")),
angle=float(el.get("angle", "0")),
width=float(el.get("width", "0")),
depth=float(el.get("depth", "0")),
height=float(el.get("height", "0")),
snapped_width=float(el.get("snappedWidth", "0")),
snapped_depth=float(el.get("snappedDepth", "0")),
snapped_height=float(el.get("snappedHeight", "0")),
size_lock_0=el.get("sizeLock0", ""),
size_lock_1=el.get("sizeLock1", ""),
size_lock_2=el.get("sizeLock2", ""),
symbol_instance=el.get("symbolInstance", ""),
)
def _parse_main_dimension(el: ET.Element) -> MagicPlanMainDimension:
return MagicPlanMainDimension(
from_point=int(el.get("from", "0")),
to_point=int(el.get("to", "0")),
dir_x=float(el.get("dir.x", "0")),
dir_y=float(el.get("dir.y", "0")),
value=float(el.get("value", "0")),
actual_value=float(el.get("actualValue", "0")),
is_set=el.get("isSet", "0") == "1",
)
def _parse_exploded_wall(el: ET.Element) -> MagicPlanExplodedWall:
type_el = el.find("type")
return MagicPlanExplodedWall(
wall_type=(type_el.text or "") if type_el is not None else "",
points=[_parse_wall_point(p) for p in el.findall("point")],
)
def _parse_exploded(el: ET.Element) -> MagicPlanExploded:
return MagicPlanExploded(
walls=[_parse_exploded_wall(w) for w in el.findall("wall")],
doors=[_parse_exploded_opening(d) for d in el.findall("door")],
windows=[_parse_exploded_opening(w) for w in el.findall("window")],
furniture=[_parse_furniture(f) for f in el.findall("furniture")],
)
def _parse_symbol_instance(el: ET.Element) -> MagicPlanSymbolInstance:
return MagicPlanSymbolInstance(
id=el.get("id", ""),
uid=el.get("uid", ""),
parent_uid=el.get("parentUid", ""),
symbol=el.get("symbol", ""),
values=_values(el),
)
def _parse_floor(el: ET.Element) -> MagicPlanFloor:
si_el = el.find("symbolInstance")
exploded_el = el.find("exploded")
return MagicPlanFloor(
uid=el.get("uid", ""),
name=el.findtext("name") or "",
floor_type=el.get("floorType", "0"),
rotation=float(el.get("rotation", "0")),
compass_angle=float(el.get("compassAngle", "0")),
area_without_walls=float(el.get("areaWithoutWalls", "0")),
area_with_interior_walls_only=float(el.get("areaWithInteriorWallsOnly", "0")),
area_with_walls=float(el.get("areaWithWalls", "0")),
symbol_instance=_parse_symbol_instance(si_el) if si_el is not None
else MagicPlanSymbolInstance(id="", uid="", parent_uid="", symbol="", values={}),
rooms=[_parse_room(r) for r in el.findall("floorRoom")],
furniture=[_parse_furniture(f) for f in el.findall("furniture")],
exploded=_parse_exploded(exploded_el) if exploded_el is not None
else MagicPlanExploded(walls=[], doors=[], windows=[], furniture=[]),
)
def _parse_room(el: ET.Element) -> MagicPlanRoom:
return MagicPlanRoom(
uid=el.get("uid", ""),
type=el.get("type", ""),
x=float(el.get("x", "0")),
y=float(el.get("y", "0")),
rotation=float(el.get("rotation", "0")),
was_modified=el.get("wasModified", "0") == "1",
linked_room_0=el.get("linkedRoom0", "-1"),
linked_room_1=el.get("linkedRoom1", "-1"),
area=float(el.get("area", "0")),
perimeter=float(el.get("perimeter", "0")),
values=_values(el),
points=[_parse_room_point(p) for p in el.findall("point")],
doors=[_parse_door(d) for d in el.findall("door")],
windows=[_parse_window(w) for w in el.findall("window")],
furniture=[_parse_furniture(f) for f in el.findall("furniture")],
main_dimensions=[_parse_main_dimension(d) for d in el.findall("mainDimension")],
)
def parse_magicplan_xml(xml_str: str) -> MagicPlanPlan:
raise NotImplementedError
root = ET.fromstring(xml_str)
irp_el = root.find("interiorRoomPoints")
return MagicPlanPlan(
id=root.get("id", ""),
uid=root.get("uid", ""),
name=root.get("name", ""),
type=root.get("type", ""),
interior_wall_width=float(root.get("interiorWallWidth", "0")),
exterior_wall_width=float(root.get("exteriorWallWidth", "0")),
schematic=root.get("schematic", "0") == "1",
has_land_survey_address=root.get("hasLandSurveyAddress", "0") == "1",
last_patch_identifier=root.get("lastPatchIdentifier", ""),
last_roll_identifier=root.get("lastRollIdentifier", ""),
values=_values(root),
floors=[_parse_floor(f) for f in root.findall("floor")],
interior_room_floors=[_parse_floor(f) for f in irp_el.findall("floor")]
if irp_el is not None else [],
)

View file

@ -3,6 +3,6 @@ pythonpath = .
log_cli = true
log_cli_level = INFO
addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests datatypes/epc/surveys/tests datatypes/epc/domain/tests backend/ecmk_fetcher/tests/ backend/documents_parser/tests
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests datatypes/epc/surveys/tests datatypes/epc/domain/tests backend/ecmk_fetcher/tests/ backend/documents_parser/tests backend/magic_plan/tests
markers =
integration: mark a test as an integration test