Model/backend/magic_plan/models.py
2026-05-01 16:20:55 +00:00

417 lines
9.1 KiB
Python

import re
from dataclasses import dataclass
from typing import Any, Optional
def _camel_to_snake(name: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
def parse_displayable_fields(fields: list[dict[str, Any]]) -> 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 <exploded><wall> — absolute coords, no uid or values."""
x: float
y: float
height: float
@dataclass
class MagicPlanXMLDoor:
"""Door in <floorRoom> 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 <floorRoom> 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 <exploded> 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