mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #1050 from Hestia-Homes/feature/magicplan-api-client
MagicPlan: parse API Plan response and map to simple domain objects
This commit is contained in:
commit
c7483fceaa
13 changed files with 137375 additions and 1 deletions
136742
backend/magic_plan/magicplan_api_plan_response_example.json
Normal file
136742
backend/magic_plan/magicplan_api_plan_response_example.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
datatypes/magicplan/__init__.py
Normal file
0
datatypes/magicplan/__init__.py
Normal file
0
datatypes/magicplan/api/__init__.py
Normal file
0
datatypes/magicplan/api/__init__.py
Normal file
280
datatypes/magicplan/api/response.py
Normal file
280
datatypes/magicplan/api/response.py
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
_IGNORE = ConfigDict(extra="ignore")
|
||||||
|
_IGNORE_POPULATE = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Vec3(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
|
class Symbol(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
valid: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FieldValue(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
index: int
|
||||||
|
has_value: bool
|
||||||
|
is_array: bool
|
||||||
|
value: Union[str, list[str]]
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyField(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
id: str
|
||||||
|
type: int
|
||||||
|
type_as_string: str
|
||||||
|
is_required: bool
|
||||||
|
label: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
list_values: list[str] = []
|
||||||
|
value: FieldValue
|
||||||
|
|
||||||
|
|
||||||
|
class ImageMapEntry(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
symbol_id: str
|
||||||
|
uid: str
|
||||||
|
owner_uid: str
|
||||||
|
type: int
|
||||||
|
coordinates: list[int]
|
||||||
|
|
||||||
|
|
||||||
|
class FormattedDimensions(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
width: Optional[str] = None
|
||||||
|
depth: Optional[str] = None
|
||||||
|
height: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FormattedMeasures(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
width: Optional[str] = None
|
||||||
|
depth: Optional[str] = None
|
||||||
|
height: Optional[str] = None
|
||||||
|
area: Optional[str] = None
|
||||||
|
area_without_walls: Optional[str] = None
|
||||||
|
area_with_interior_walls_only: Optional[str] = None
|
||||||
|
area_with_walls: Optional[str] = None
|
||||||
|
doors_surface: Optional[str] = None
|
||||||
|
walls_surface: Optional[str] = None
|
||||||
|
walls_surface_without_openings: Optional[str] = None
|
||||||
|
windows_surface: Optional[str] = None
|
||||||
|
perimeter: Optional[str] = None
|
||||||
|
ground_perimeter: Optional[str] = None
|
||||||
|
living_area: Optional[str] = None
|
||||||
|
below_grade_living_area: Optional[str] = None
|
||||||
|
above_grade_living_area: Optional[str] = None
|
||||||
|
exterior_perimeter: Optional[str] = None
|
||||||
|
volume: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Address(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
street: Optional[str] = None
|
||||||
|
street_number: Optional[str] = None
|
||||||
|
postal_code: Optional[str] = None
|
||||||
|
city: Optional[str] = None
|
||||||
|
country: Optional[str] = None
|
||||||
|
longitude: Optional[float] = None
|
||||||
|
latitude: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CreatedBy(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
id: str
|
||||||
|
email: str
|
||||||
|
firstname: Optional[str] = None
|
||||||
|
lastname: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Location(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
valid: bool
|
||||||
|
longitude: float
|
||||||
|
latitude: float
|
||||||
|
altitude: float
|
||||||
|
|
||||||
|
|
||||||
|
class ItemBase(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
uid: str
|
||||||
|
symbol: Symbol
|
||||||
|
size: Vec3
|
||||||
|
position: Vec3
|
||||||
|
rotation: Vec3
|
||||||
|
formatted: Optional[FormattedDimensions] = None
|
||||||
|
images: list[Any] = []
|
||||||
|
notes: Optional[str] = None
|
||||||
|
displayable_fields: list[SurveyField] = []
|
||||||
|
custom_displayable_fields: list[SurveyField] = []
|
||||||
|
|
||||||
|
|
||||||
|
class WallItem(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Furniture(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolInstance(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Wall(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
uid: str
|
||||||
|
symbol: Symbol
|
||||||
|
length: float
|
||||||
|
images: list[Any] = []
|
||||||
|
notes: Optional[str] = None
|
||||||
|
displayable_fields: list[SurveyField] = []
|
||||||
|
custom_displayable_fields: list[SurveyField] = []
|
||||||
|
|
||||||
|
|
||||||
|
class Room(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
name: str
|
||||||
|
uid: str
|
||||||
|
symbol: Optional[Symbol] = None
|
||||||
|
size: Vec3
|
||||||
|
position: Vec3
|
||||||
|
rotation: Vec3
|
||||||
|
area: float
|
||||||
|
perimeter: Optional[float] = None
|
||||||
|
ground_perimeter: Optional[float] = None
|
||||||
|
area_without_walls: Optional[float] = None
|
||||||
|
area_with_interior_walls_only: Optional[float] = None
|
||||||
|
area_with_walls: Optional[float] = None
|
||||||
|
wall_count: Optional[int] = None
|
||||||
|
wall_count_with_interior_walls: Optional[int] = None
|
||||||
|
corner_count_with_interior_walls: Optional[int] = None
|
||||||
|
door_count: Optional[int] = None
|
||||||
|
window_count: Optional[int] = None
|
||||||
|
height: Optional[float] = None
|
||||||
|
volume: Optional[float] = None
|
||||||
|
width: Optional[float] = None
|
||||||
|
doors_surface: Optional[float] = None
|
||||||
|
walls_surface: Optional[float] = None
|
||||||
|
walls_surface_without_openings: Optional[float] = None
|
||||||
|
windows_surface: Optional[float] = None
|
||||||
|
dimensions: Optional[str] = None
|
||||||
|
room_type: Optional[str] = None
|
||||||
|
furniture_count: Optional[int] = None
|
||||||
|
image: Optional[str] = None
|
||||||
|
image_map: list[ImageMapEntry] = []
|
||||||
|
images: list[Any] = []
|
||||||
|
notes: Optional[str] = None
|
||||||
|
formatted: Optional[FormattedMeasures] = None
|
||||||
|
displayable_fields: list[SurveyField] = []
|
||||||
|
custom_displayable_fields: list[SurveyField] = []
|
||||||
|
wall_items: list[WallItem] = []
|
||||||
|
furnitures: list[Furniture] = []
|
||||||
|
walls: list[Wall] = []
|
||||||
|
|
||||||
|
|
||||||
|
class Floor(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
uid: str
|
||||||
|
symbol: Optional[Symbol] = None
|
||||||
|
size: Vec3
|
||||||
|
position: Vec3
|
||||||
|
rotation: Vec3
|
||||||
|
name: Optional[str] = None
|
||||||
|
area: Optional[float] = None
|
||||||
|
perimeter: Optional[float] = None
|
||||||
|
ground_perimeter: Optional[float] = None
|
||||||
|
area_without_walls: Optional[float] = None
|
||||||
|
area_with_interior_walls_only: Optional[float] = None
|
||||||
|
area_with_walls: Optional[float] = None
|
||||||
|
wall_count: Optional[int] = None
|
||||||
|
wall_count_with_interior_walls: Optional[int] = None
|
||||||
|
corner_count_with_interior_walls: Optional[int] = None
|
||||||
|
door_count: Optional[int] = None
|
||||||
|
window_count: Optional[int] = None
|
||||||
|
bathrooms_count: Optional[int] = None
|
||||||
|
bedrooms_count: Optional[int] = None
|
||||||
|
doors_surface: Optional[float] = None
|
||||||
|
floor_type: Optional[Union[int, str]] = None
|
||||||
|
furniture_count: Optional[int] = None
|
||||||
|
height: Optional[float] = None
|
||||||
|
level: Optional[int] = None
|
||||||
|
room_count: Optional[int] = None
|
||||||
|
volume: Optional[float] = None
|
||||||
|
walls_surface: Optional[float] = None
|
||||||
|
walls_surface_without_openings: Optional[float] = None
|
||||||
|
windows_surface: Optional[float] = None
|
||||||
|
image: Optional[str] = None
|
||||||
|
image_map: list[ImageMapEntry] = []
|
||||||
|
images: list[Any] = []
|
||||||
|
notes: Optional[str] = None
|
||||||
|
formatted: Optional[FormattedMeasures] = None
|
||||||
|
displayable_fields: list[SurveyField] = []
|
||||||
|
custom_displayable_fields: list[SurveyField] = []
|
||||||
|
rooms: list[Room] = []
|
||||||
|
furnitures: list[Furniture] = []
|
||||||
|
symbol_instances: list[SymbolInstance] = []
|
||||||
|
|
||||||
|
|
||||||
|
class PlanBody(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
uid: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
symbol: Optional[Symbol] = None
|
||||||
|
size: Vec3
|
||||||
|
position: Vec3
|
||||||
|
rotation: Vec3
|
||||||
|
area: Optional[float] = None
|
||||||
|
location: Location
|
||||||
|
floors: list[Floor] = []
|
||||||
|
images: list[Any] = []
|
||||||
|
notes: Optional[str] = None
|
||||||
|
formatted: Optional[FormattedMeasures] = None
|
||||||
|
displayable_fields: list[SurveyField] = []
|
||||||
|
custom_displayable_fields: list[SurveyField] = []
|
||||||
|
customer: list[Any] = []
|
||||||
|
custom_attributes: list[Any] = []
|
||||||
|
|
||||||
|
|
||||||
|
class PlanDetail(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
extension_version: Optional[str] = None
|
||||||
|
wrapper_version: Optional[str] = None
|
||||||
|
document_version: Optional[str] = None
|
||||||
|
last_modification_date: Optional[Union[int, str]] = None
|
||||||
|
plan: PlanBody
|
||||||
|
|
||||||
|
|
||||||
|
class PlanSummary(BaseModel):
|
||||||
|
model_config = _IGNORE_POPULATE
|
||||||
|
id: str
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
address: Optional[Address] = None
|
||||||
|
creation_date: Optional[str] = None
|
||||||
|
update_date: Optional[str] = None
|
||||||
|
thumbnail_url: Optional[str] = None
|
||||||
|
public_url: Optional[str] = None
|
||||||
|
cloud_url: Optional[str] = None
|
||||||
|
url_3d: Optional[str] = Field(default=None, alias="3d_url")
|
||||||
|
workgroup_id: Optional[str] = None
|
||||||
|
team_id: Optional[str] = None
|
||||||
|
created_by: Optional[CreatedBy] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MagicPlan(BaseModel):
|
||||||
|
model_config = _IGNORE
|
||||||
|
plan: PlanSummary
|
||||||
|
plan_detail: PlanDetail
|
||||||
0
datatypes/magicplan/api/tests/__init__.py
Normal file
0
datatypes/magicplan/api/tests/__init__.py
Normal file
86
datatypes/magicplan/api/tests/test_response.py
Normal file
86
datatypes/magicplan/api/tests/test_response.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from datatypes.magicplan.api.response import MagicPlan
|
||||||
|
|
||||||
|
FIXTURE_DIR = Path(__file__).parents[4] / "backend" / "magic_plan"
|
||||||
|
PLAN_ID = "a7285ed1-878d-47eb-8aa6-85ef9e187516"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def raw_data() -> dict[str, Any]:
|
||||||
|
payload = json.loads(
|
||||||
|
(FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text()
|
||||||
|
)
|
||||||
|
return payload["data"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def mp(raw_data: dict[str, Any]) -> MagicPlan:
|
||||||
|
return MagicPlan.model_validate(raw_data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_validate_does_not_raise(raw_data: dict[str, Any]):
|
||||||
|
# act
|
||||||
|
MagicPlan.model_validate(raw_data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_id(mp: MagicPlan):
|
||||||
|
# assert
|
||||||
|
assert mp.plan.id == PLAN_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_3d_alias(mp: MagicPlan):
|
||||||
|
# assert
|
||||||
|
assert mp.plan.url_3d is not None
|
||||||
|
assert mp.plan.url_3d.startswith("http")
|
||||||
|
|
||||||
|
|
||||||
|
def test_floor_count(mp: MagicPlan):
|
||||||
|
# assert
|
||||||
|
assert len(mp.plan_detail.plan.floors) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_room_name(mp: MagicPlan):
|
||||||
|
# assert
|
||||||
|
assert mp.plan_detail.plan.floors[0].rooms[0].name == "Kitchen"
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_area_is_float(mp: MagicPlan):
|
||||||
|
# arrange
|
||||||
|
room = mp.plan_detail.plan.floors[0].rooms[0]
|
||||||
|
# assert
|
||||||
|
assert isinstance(room.area, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_item_symbol_id(mp: MagicPlan):
|
||||||
|
# arrange
|
||||||
|
room = mp.plan_detail.plan.floors[0].rooms[0]
|
||||||
|
# assert
|
||||||
|
assert room.wall_items[0].symbol.id != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_value_array(mp: MagicPlan):
|
||||||
|
# arrange
|
||||||
|
room = mp.plan_detail.plan.floors[0].rooms[0]
|
||||||
|
array_field = next(f for f in room.displayable_fields if f.value.is_array)
|
||||||
|
# assert
|
||||||
|
assert isinstance(array_field.value.value, list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_value_scalar(mp: MagicPlan):
|
||||||
|
# arrange
|
||||||
|
room = mp.plan_detail.plan.floors[0].rooms[0]
|
||||||
|
scalar_field = next(f for f in room.displayable_fields if not f.value.is_array)
|
||||||
|
# assert
|
||||||
|
assert isinstance(scalar_field.value.value, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_fields_ignored(raw_data: dict[str, Any]):
|
||||||
|
# arrange
|
||||||
|
data_with_extra = {**raw_data, "unknown_future_field": "whatever"}
|
||||||
|
# act
|
||||||
|
MagicPlan.model_validate(data_with_extra)
|
||||||
0
datatypes/magicplan/domain/__init__.py
Normal file
0
datatypes/magicplan/domain/__init__.py
Normal file
53
datatypes/magicplan/domain/mapper.py
Normal file
53
datatypes/magicplan/domain/mapper.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import datatypes.magicplan.api.response as api
|
||||||
|
from datatypes.magicplan.api.response import MagicPlan
|
||||||
|
from datatypes.magicplan.domain.models import Plan, Floor, Room, Window, Door
|
||||||
|
|
||||||
|
|
||||||
|
def map_plan(mp: MagicPlan) -> Plan:
|
||||||
|
return Plan(
|
||||||
|
uid=mp.plan.id,
|
||||||
|
name=mp.plan.name,
|
||||||
|
floors=[_map_floor(f) for f in mp.plan_detail.plan.floors],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _map_floor(f: api.Floor) -> Floor:
|
||||||
|
return Floor(
|
||||||
|
level=f.level,
|
||||||
|
name=f.name,
|
||||||
|
rooms=[_map_room(r) for r in f.rooms],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _map_room(r: api.Room) -> Room:
|
||||||
|
width, length = _parse_dimensions(r.dimensions)
|
||||||
|
return Room(
|
||||||
|
name=r.name,
|
||||||
|
width=width,
|
||||||
|
length=length,
|
||||||
|
area=round(r.area, 2),
|
||||||
|
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")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_dimensions(dimensions: str | None) -> tuple[float, float]:
|
||||||
|
if not dimensions:
|
||||||
|
return 0.0, 0.0
|
||||||
|
parts = dimensions.split(" x ")
|
||||||
|
width = round(float(parts[0].split(" m")[0]), 2)
|
||||||
|
length = round(float(parts[1].split(" m")[0]), 2)
|
||||||
|
return width, length
|
||||||
|
|
||||||
|
|
||||||
|
def _map_window(wi: api.WallItem) -> Window:
|
||||||
|
return Window(
|
||||||
|
width=round(wi.size.x, 2),
|
||||||
|
height=round(wi.size.z, 2),
|
||||||
|
area=round(wi.size.x * wi.size.z, 2),
|
||||||
|
opening_type=wi.symbol.id.removeprefix("window"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _map_door(wi: api.WallItem) -> Door:
|
||||||
|
return Door(width=round(wi.size.x, 2))
|
||||||
38
datatypes/magicplan/domain/models.py
Normal file
38
datatypes/magicplan/domain/models.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Window:
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
area: float
|
||||||
|
opening_type: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Door:
|
||||||
|
width: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Room:
|
||||||
|
name: str
|
||||||
|
width: float
|
||||||
|
length: float
|
||||||
|
area: float
|
||||||
|
windows: list[Window] = field(default_factory=list[Window])
|
||||||
|
doors: list[Door] = field(default_factory=list[Door])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Floor:
|
||||||
|
level: int | None
|
||||||
|
name: str | None
|
||||||
|
rooms: list[Room] = field(default_factory=list[Room])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Plan:
|
||||||
|
uid: str
|
||||||
|
name: str | None
|
||||||
|
floors: list[Floor] = field(default_factory=list[Floor])
|
||||||
0
datatypes/magicplan/domain/tests/__init__.py
Normal file
0
datatypes/magicplan/domain/tests/__init__.py
Normal file
174
datatypes/magicplan/domain/tests/test_mapper.py
Normal file
174
datatypes/magicplan/domain/tests/test_mapper.py
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from datatypes.magicplan.api.response import MagicPlan
|
||||||
|
from datatypes.magicplan.domain.mapper import map_plan
|
||||||
|
from datatypes.magicplan.domain.models import Plan
|
||||||
|
|
||||||
|
FIXTURE_DIR = Path(__file__).parents[4] / "backend" / "magic_plan"
|
||||||
|
PLAN_ID = "a7285ed1-878d-47eb-8aa6-85ef9e187516"
|
||||||
|
PLAN_ID_2 = "9f9889ff-793e-4e9a-a6f0-e22f5b0f5365"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def raw_data() -> dict[str, Any]:
|
||||||
|
payload = json.loads(
|
||||||
|
(FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text()
|
||||||
|
)
|
||||||
|
return payload["data"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def mp(raw_data: dict[str, Any]) -> MagicPlan:
|
||||||
|
return MagicPlan.model_validate(raw_data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def plan(mp: MagicPlan) -> Plan:
|
||||||
|
return map_plan(mp)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_uid(plan: Plan):
|
||||||
|
assert plan.uid == PLAN_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_floor_count(plan: Plan):
|
||||||
|
assert len(plan.floors) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_room_name(plan: Plan):
|
||||||
|
assert plan.floors[0].rooms[0].name == "Kitchen"
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_dimensions_are_floats(plan: Plan):
|
||||||
|
room = plan.floors[0].rooms[0]
|
||||||
|
assert isinstance(room.width, float)
|
||||||
|
assert isinstance(room.length, float)
|
||||||
|
assert isinstance(room.area, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_area_rounded_to_2dp(plan: Plan):
|
||||||
|
room = plan.floors[0].rooms[0]
|
||||||
|
assert room.area == 7.95
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_dimensions_parsed_from_string(plan: Plan):
|
||||||
|
room = plan.floors[0].rooms[0]
|
||||||
|
assert room.width == pytest.approx(2.67)
|
||||||
|
assert room.length == pytest.approx(2.98)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kitchen_has_windows(plan: Plan):
|
||||||
|
room = plan.floors[0].rooms[0]
|
||||||
|
assert len(room.windows) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_fields_are_floats(plan: Plan):
|
||||||
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
|
assert isinstance(window.width, float)
|
||||||
|
assert isinstance(window.height, float)
|
||||||
|
assert isinstance(window.area, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_opening_type_prefix_stripped(plan: Plan):
|
||||||
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
|
assert not window.opening_type.startswith("window")
|
||||||
|
assert window.opening_type == "casement"
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_area_is_width_times_height(plan: Plan):
|
||||||
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
|
assert window.area == pytest.approx(window.width * window.height, rel=1e-2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_dimensions_rounded_to_2dp(plan: Plan):
|
||||||
|
window = plan.floors[0].rooms[0].windows[0]
|
||||||
|
assert window.width == 1.40
|
||||||
|
assert window.height == 1.20
|
||||||
|
assert window.area == 1.68
|
||||||
|
|
||||||
|
|
||||||
|
def test_door_width_rounded_to_2dp(plan: Plan):
|
||||||
|
door = plan.floors[0].rooms[0].doors[0]
|
||||||
|
assert door.width == 0.79
|
||||||
|
|
||||||
|
|
||||||
|
def test_kitchen_has_doors(plan: Plan):
|
||||||
|
room = plan.floors[0].rooms[0]
|
||||||
|
assert len(room.doors) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_door_width_is_float(plan: Plan):
|
||||||
|
door = plan.floors[0].rooms[0].doors[0]
|
||||||
|
assert isinstance(door.width, float)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Fixture 2: magicplan_api_plan_response_example_2.json ---
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def raw_data_2() -> dict[str, Any]:
|
||||||
|
payload = json.loads(
|
||||||
|
(FIXTURE_DIR / "magicplan_api_plan_response_example_2.json").read_text()
|
||||||
|
)
|
||||||
|
return payload["data"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def plan2(raw_data_2: dict[str, Any]) -> Plan:
|
||||||
|
return map_plan(MagicPlan.model_validate(raw_data_2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_uid(plan2: Plan):
|
||||||
|
assert plan2.uid == PLAN_ID_2
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_floor_count(plan2: Plan):
|
||||||
|
assert len(plan2.floors) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_first_room_name(plan2: Plan):
|
||||||
|
assert plan2.floors[0].rooms[0].name == "Toilet"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_room_area_rounded_to_2dp(plan2: Plan):
|
||||||
|
room = plan2.floors[0].rooms[0]
|
||||||
|
assert room.area == 0.96
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_room_dimensions_parsed_from_string(plan2: Plan):
|
||||||
|
room = plan2.floors[0].rooms[0]
|
||||||
|
assert room.width == pytest.approx(1.12)
|
||||||
|
assert room.length == pytest.approx(0.86)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_room_with_no_windows(plan2: Plan):
|
||||||
|
hall = plan2.floors[0].rooms[1]
|
||||||
|
assert hall.name == "Hall"
|
||||||
|
assert hall.windows == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_window_dimensions_rounded_to_2dp(plan2: Plan):
|
||||||
|
window = plan2.floors[0].rooms[0].windows[0]
|
||||||
|
assert window.width == 0.39
|
||||||
|
assert window.height == 0.67
|
||||||
|
assert window.area == 0.26
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_window_opening_type_casement(plan2: Plan):
|
||||||
|
window = plan2.floors[0].rooms[0].windows[0]
|
||||||
|
assert window.opening_type == "casement"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_window_opening_type_hung(plan2: Plan):
|
||||||
|
bathroom1 = plan2.floors[1].rooms[1]
|
||||||
|
assert bathroom1.name == "Bathroom 1"
|
||||||
|
assert bathroom1.windows[0].opening_type == "hung"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan2_door_width_rounded_to_2dp(plan2: Plan):
|
||||||
|
door = plan2.floors[0].rooms[0].doors[0]
|
||||||
|
assert door.width == 0.71
|
||||||
|
|
@ -3,6 +3,6 @@ pythonpath = .
|
||||||
log_cli = true
|
log_cli = true
|
||||||
log_cli_level = INFO
|
log_cli_level = INFO
|
||||||
addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial
|
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/pashub_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/pashub_fetcher/tests backend/documents_parser/tests backend/magic_plan/tests datatypes/magicplan/api/tests datatypes/magicplan/domain/tests
|
||||||
markers =
|
markers =
|
||||||
integration: mark a test as an integration test
|
integration: mark a test as an integration test
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue