import re from dataclasses import dataclass from typing import Any, Optional def _camel_to_snake(name: str) -> str: return re.sub(r"(? dict[str, str]: result: dict[str, str] = {} for f in fields: if f.get("type_as_string") == "sectionTitle": continue v: dict[str, Any] = f.get("value") or {} if not v.get("has_value"): continue result[_camel_to_snake(str(f["id"]))] = str(v["value"]) return result # --------------------------------------------------------------------------- # XML dataclasses (sourced from MagicPlan Exchange XML format) # --------------------------------------------------------------------------- @dataclass class MagicPlanXMLRoomPoint: snapped_x: float snapped_y: float height: float uid: str values: dict[str, str] @dataclass class MagicPlanXMLWallPoint: """Point in — absolute coords, no uid or values.""" x: float y: float height: float @dataclass class MagicPlanXMLDoor: """Door in context — wall-relative position.""" wall_point_index: int type: str u: float width: float depth: float height: float orientation: int snapped_type: str snapped_position: float snapped_width: float snapped_depth: float snapped_height: float snapped_orientation: int inset_x: float inset_y: float inset_z: float symbol_instance: str twin_wall_item_uid: Optional[str] = None @dataclass class MagicPlanXMLWindow: """Window in context — wall-relative position.""" wall_point_index: int type: str u: float width: float depth: float height: float orientation: int snapped_type: str snapped_position: float snapped_width: float snapped_depth: float snapped_height: float snapped_orientation: int inset_x: float inset_y: float inset_z: float symbol_instance: str @dataclass class MagicPlanXMLExplodedOpening: """Door or window in context — absolute coords, no snapped* fields.""" type: str x1: float y1: float x2: float y2: float width: float depth: float height: float inset_x: float inset_y: float orientation: int symbol_instance: str @dataclass class MagicPlanXMLFurniture: type: str x: float y: float snapped_x: float snapped_y: float angle: float width: float depth: float height: float snapped_width: float snapped_depth: float snapped_height: float size_lock_0: str size_lock_1: str size_lock_2: str symbol_instance: str @dataclass class MagicPlanXMLMainDimension: from_point: int to_point: int dir_x: float dir_y: float value: float actual_value: float is_set: bool @dataclass class MagicPlanXMLExplodedWall: wall_type: str points: list[MagicPlanXMLWallPoint] @dataclass class MagicPlanXMLExploded: walls: list[MagicPlanXMLExplodedWall] doors: list[MagicPlanXMLExplodedOpening] windows: list[MagicPlanXMLExplodedOpening] furniture: list[MagicPlanXMLFurniture] @dataclass class MagicPlanXMLSymbolInstance: id: str uid: str parent_uid: str symbol: str values: dict[str, str] @dataclass class MagicPlanXMLRoom: uid: str type: str x: float y: float rotation: float was_modified: bool linked_room_0: str linked_room_1: str area: float perimeter: float values: dict[str, str] points: list[MagicPlanXMLRoomPoint] doors: list[MagicPlanXMLDoor] windows: list[MagicPlanXMLWindow] furniture: list[MagicPlanXMLFurniture] main_dimensions: list[MagicPlanXMLMainDimension] @dataclass class MagicPlanXMLFloor: uid: str name: str floor_type: str rotation: float compass_angle: float area_without_walls: float area_with_interior_walls_only: float area_with_walls: float symbol_instance: MagicPlanXMLSymbolInstance rooms: list[MagicPlanXMLRoom] furniture: list[MagicPlanXMLFurniture] exploded: MagicPlanXMLExploded @dataclass class MagicPlanXMLSummary: """Plan metadata returned by the list-plans API endpoint (old string-address format).""" id: str project_id: str name: str address: str creation_date: str update_date: str workgroup_id: str team_id: str created_by: str thumbnail_url: str public_url: str cloud_url: str url_3d: str @dataclass class MagicPlanXMLDetail: """Full plan response: summary metadata + parsed XML plan.""" summary: MagicPlanXMLSummary plan_xml: "MagicPlanXMLPlan" @dataclass class MagicPlanXMLPlan: id: str uid: str name: str type: str interior_wall_width: float exterior_wall_width: float schematic: bool has_land_survey_address: bool last_patch_identifier: str last_roll_identifier: str values: dict[str, str] floors: list[MagicPlanXMLFloor] interior_room_floors: list[MagicPlanXMLFloor] # --------------------------------------------------------------------------- # JSON dataclasses (sourced from GET Plan API JSON response) # --------------------------------------------------------------------------- @dataclass class MagicPlanWall: uid: str symbol_id: str length: float fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanWallItem: """Door or window — distinguished by symbol_id.""" uid: str symbol_id: str symbol_name: str width: float depth: float height: float pos_x: float pos_y: float pos_z: float rotation_z: float fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanFurniture: uid: str symbol_id: str symbol_name: str width: float depth: float height: float pos_x: float pos_y: float pos_z: float rotation_z: float fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanRoom: uid: str name: str area: float perimeter: float height: float width: float volume: float dimensions: str door_count: int window_count: int walls_surface: float walls_surface_without_openings: float doors_surface: float windows_surface: float wall_items: list[MagicPlanWallItem] furnitures: list[MagicPlanFurniture] walls: list[MagicPlanWall] fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanFloor: uid: str name: str floor_type: str area: float perimeter: float area_without_walls: float area_with_interior_walls_only: float area_with_walls: float wall_count: int wall_count_with_interior_walls: int door_count: int window_count: int room_count: int furniture_count: int doors_surface: float walls_surface: float walls_surface_without_openings: float windows_surface: float volume: float rooms: list[MagicPlanRoom] furnitures: list[MagicPlanFurniture] symbol_instances: list[MagicPlanFurniture] fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanPlan: """GET /plans/{id} — merged data.plan + data.plan_detail.plan.""" id: str project_id: str uid: str name: str area: float perimeter: float area_without_walls: float area_with_interior_walls_only: float area_with_walls: float wall_count: int door_count: int window_count: int room_count: int furniture_count: int floor_count: int doors_surface: float walls_surface: float walls_surface_without_openings: float windows_surface: float volume: float living_area: float below_grade_living_area: float above_grade_living_area: float address_street: Optional[str] address_postal_code: Optional[str] address_city: Optional[str] address_country: Optional[str] address_longitude: Optional[float] address_latitude: Optional[float] creation_date: str update_date: str workgroup_id: str team_id: str created_by_id: str created_by_firstname: Optional[str] created_by_lastname: Optional[str] created_by_email: str thumbnail_url: str public_url: str cloud_url: str url_3d: str floors: list[MagicPlanFloor] fields: dict[str, str] custom_fields: dict[str, str] @dataclass class MagicPlanSummary: """GET /plans list — lightweight, flat address and creator fields.""" id: str project_id: str name: str address_street: Optional[str] address_postal_code: Optional[str] address_city: Optional[str] address_country: Optional[str] address_longitude: Optional[float] address_latitude: Optional[float] creation_date: str update_date: str workgroup_id: str team_id: str created_by_id: str created_by_firstname: Optional[str] created_by_lastname: Optional[str] created_by_email: str thumbnail_url: str public_url: str cloud_url: str url_3d: str