Skip to content

Commit 809d9a6

Browse files
authored
Merge pull request #19 from compas-dev/refactor/string-paths-storage-rtdb
Refactor/string paths storage rtdb
2 parents 57c7b1c + 256376f commit 809d9a6

File tree

6 files changed

+314
-357
lines changed

6 files changed

+314
-357
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
### Removed
2020

2121
* Removed Support for IronPython 2.7 support
22+
* Refactored RealtimeDatabase and Storage Classes to path based references.
2223
* Removes dependency on `compas_fab`
2324
* Removed Rhino 7 post installation hooks
2425

src/compas_xr/_path.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from typing import Union
2+
3+
4+
def normalize_path(path: Union[str, list[str], tuple[str, ...]]) -> str:
5+
"""Normalize a slash-delimited cloud path.
6+
7+
Parameters
8+
----------
9+
path
10+
Path as a slash-delimited string or as path segments.
11+
12+
Returns
13+
-------
14+
str
15+
Normalized path with single slashes and no leading/trailing slash.
16+
"""
17+
if isinstance(path, str):
18+
raw_parts = path.strip("/").split("/")
19+
elif isinstance(path, (list, tuple)):
20+
raw_parts = path
21+
else:
22+
raise TypeError("path must be a string, list, or tuple")
23+
24+
parts = []
25+
for part in raw_parts:
26+
if not isinstance(part, str):
27+
raise TypeError("all path parts must be strings")
28+
stripped = part.strip("/")
29+
if stripped:
30+
parts.append(stripped)
31+
32+
return "/".join(parts)
33+
34+
35+
def path_to_parts(path: Union[str, list[str], tuple[str, ...]]) -> list[str]:
36+
"""Convert a path string or path parts to normalized path segments."""
37+
normalized = normalize_path(path)
38+
if not normalized:
39+
return []
40+
return normalized.split("/")
41+
42+
43+
def validate_reference_parts(parts: Union[list[str], tuple[str, ...]], invalid_chars: Union[set[str], None] = None) -> None:
44+
"""Validate normalized path segments for cloud references.
45+
46+
Parameters
47+
----------
48+
parts
49+
Normalized path segments.
50+
invalid_chars
51+
Characters that are not allowed in each path segment.
52+
53+
Returns
54+
-------
55+
None
56+
57+
Raises
58+
------
59+
ValueError
60+
If the path is empty or contains invalid characters.
61+
"""
62+
if not parts:
63+
raise ValueError("path must not be empty")
64+
65+
invalid_chars = invalid_chars or set()
66+
for part in parts:
67+
if any(char in part for char in invalid_chars):
68+
raise ValueError("invalid path segment '{}': contains one of {}".format(part, " ".join(sorted(invalid_chars))))
69+
if any(ord(char) < 32 or ord(char) == 127 for char in part):
70+
raise ValueError("invalid path segment '{}': contains control characters".format(part))
71+
72+
73+
def validate_reference_path(path: Union[str, list[str], tuple[str, ...]], invalid_chars: Union[set[str], None] = None) -> list[str]:
74+
"""Normalize and validate a cloud reference path.
75+
76+
Parameters
77+
----------
78+
path
79+
Path as a slash-delimited string or as path segments.
80+
invalid_chars
81+
Characters that are not allowed in each path segment.
82+
83+
Returns
84+
-------
85+
list[str]
86+
Normalized and validated path segments.
87+
88+
Raises
89+
------
90+
ValueError
91+
If the path is empty or contains invalid characters.
92+
"""
93+
parts = path_to_parts(path)
94+
validate_reference_parts(parts, invalid_chars=invalid_chars)
95+
return parts

src/compas_xr/project/project_manager.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def upload_data_to_project(self, data: Any, project_name: str, data_name: str) -
104104
data_name
105105
The name of the child in which data will be stored.
106106
"""
107-
self.database.upload_data_to_reference_as_child(data, project_name, data_name)
107+
path = "{}/{}".format(project_name, data_name)
108+
self.database.upload_data(data, path)
108109

109110
def upload_project_data_from_compas(
110111
self,
@@ -143,7 +144,8 @@ def upload_qr_frames_to_project(self, project_name: str, qr_frames_list: list[Fr
143144
"""
144145
qr_assembly = AssemblyExtensions().create_qr_assembly(qr_frames_list)
145146
data = qr_assembly.__data__
146-
self.database.upload_data_to_reference_as_child(data, project_name, "QRFrames")
147+
path = "{}/{}".format(project_name, "QRFrames")
148+
self.database.upload_data(data, path)
147149

148150
def upload_obj_to_storage(self, path_local: str, storage_folder_name: str) -> None:
149151
"""
@@ -156,8 +158,9 @@ def upload_obj_to_storage(self, path_local: str, storage_folder_name: str) -> No
156158
storage_folder_name
157159
The name of the storage folder where the .obj file will be uploaded.
158160
"""
159-
storage_folder_list = ["obj_storage", storage_folder_name]
160-
self.storage.upload_file_as_bytes_to_deep_reference(path_local, storage_folder_list)
161+
file_name = os.path.basename(path_local)
162+
storage_path = "obj_storage/{}/{}".format(storage_folder_name, file_name)
163+
self.storage.upload_file_as_bytes_to_path(path_local, storage_path)
161164

162165
def upload_objs_from_directory_to_storage(self, local_directory: str, storage_folder_name: str) -> None:
163166
"""
@@ -170,8 +173,14 @@ def upload_objs_from_directory_to_storage(self, local_directory: str, storage_fo
170173
storage_folder_name
171174
The name of the storage folder where the .obj files will be uploaded.
172175
"""
173-
storage_folder_list = ["obj_storage", storage_folder_name]
174-
self.storage.upload_files_as_bytes_from_directory_to_deep_reference(local_directory, storage_folder_list)
176+
if not os.path.exists(local_directory) or not os.path.isdir(local_directory):
177+
raise FileNotFoundError("Directory not found: {}".format(local_directory))
178+
179+
for file_name in os.listdir(local_directory):
180+
local_path = os.path.join(local_directory, file_name)
181+
if os.path.isfile(local_path):
182+
storage_path = "obj_storage/{}/{}".format(storage_folder_name, file_name)
183+
self.storage.upload_file_as_bytes_to_path(local_path, storage_path)
175184

176185
def get_project_data(self, project_name: str) -> dict:
177186
"""
@@ -248,13 +257,13 @@ def edit_step_on_database(
248257
The priority of the step.
249258
250259
"""
251-
database_reference_list = [project_name, "building_plan", "data", "steps", key, "data"]
252-
current_data = self.database.get_data_from_deep_reference(database_reference_list)
260+
database_path = "{}/building_plan/data/steps/{}/data".format(project_name, key)
261+
current_data = self.database.get_data(database_path)
253262
current_data["actor"] = actor
254263
current_data["is_built"] = is_built
255264
current_data["is_planned"] = is_planned
256265
current_data["priority"] = priority
257-
self.database.upload_data_to_deep_reference(current_data, database_reference_list)
266+
self.database.upload_data(current_data, database_path)
258267

259268
def visualize_project_state_timbers(
260269
self,
@@ -288,8 +297,8 @@ def visualize_project_state_timbers(
288297
289298
"""
290299
nodes = timber_assembly.graph.__data__["node"]
291-
buiding_plan_data_reference_list = [project_name, "building_plan", "data"]
292-
current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list)
300+
building_plan_data_path = "{}/building_plan/data".format(project_name)
301+
current_state_data = self.database.get_data(building_plan_data_path)
293302

294303
built_human = []
295304
unbuilt_human = []
@@ -364,8 +373,8 @@ def visualize_project_state(self, assembly: Assembly, project_name: str):
364373
The parts that have not been built by a robot.
365374
366375
"""
367-
buiding_plan_data_reference_list = [project_name, "building_plan", "data"]
368-
current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list)
376+
building_plan_data_path = "{}/building_plan/data".format(project_name)
377+
current_state_data = self.database.get_data(building_plan_data_path)
369378
nodes = assembly.graph.__data__["node"]
370379

371380
built_human = []

0 commit comments

Comments
 (0)