diff --git a/backend/magic_plan/__init__.py b/backend/magic_plan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/magic_plan/tests/test_xml_parser.py b/backend/magic_plan/tests/test_xml_parser.py index 2dcf6b63..7c4f4d82 100644 --- a/backend/magic_plan/tests/test_xml_parser.py +++ b/backend/magic_plan/tests/test_xml_parser.py @@ -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 diff --git a/backend/magic_plan/xml_parser.py b/backend/magic_plan/xml_parser.py index 13d30cdd..878eb1fd 100644 --- a/backend/magic_plan/xml_parser.py +++ b/backend/magic_plan/xml_parser.py @@ -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 [], + ) diff --git a/pytest.ini b/pytest.ini index 33231c61..064a3a59 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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