Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed

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

Expand Down
92 changes: 92 additions & 0 deletions src/compas_xr/_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
def normalize_path(path):
"""Normalize a slash-delimited cloud path.

Parameters
----------
path : str | list[str] | tuple[str]
Comment thread
jckenny59 marked this conversation as resolved.
Outdated
Path as a slash-delimited string or as path segments.

Returns
-------
str
Normalized path with single slashes and no leading/trailing slash.
"""
if isinstance(path, str):
raw_parts = path.strip("/").split("/")
elif isinstance(path, (list, tuple)):
raw_parts = path
else:
raise TypeError("path must be a string, list, or tuple")

parts = []
for part in raw_parts:
if not isinstance(part, str):
raise TypeError("all path parts must be strings")
stripped = part.strip("/")
if stripped:
parts.append(stripped)

return "/".join(parts)


def path_to_parts(path):
"""Convert a path string or path parts to normalized path segments."""
normalized = normalize_path(path)
if not normalized:
return []
return normalized.split("/")


def validate_reference_parts(parts, invalid_chars=None):
"""Validate normalized path segments for cloud references.

Parameters
----------
parts : list[str] | tuple[str]
Normalized path segments.
invalid_chars : set[str] | None, optional
Comment thread
jckenny59 marked this conversation as resolved.
Outdated
Characters that are not allowed in each path segment.

Returns
-------
None

Raises
------
ValueError
If the path is empty or contains invalid characters.
"""
if not parts:
raise ValueError("path must not be empty")

invalid_chars = invalid_chars or set()
for part in parts:
if any(char in part for char in invalid_chars):
raise ValueError("invalid path segment '{}': contains one of {}".format(part, " ".join(sorted(invalid_chars))))
if any(ord(char) < 32 or ord(char) == 127 for char in part):
raise ValueError("invalid path segment '{}': contains control characters".format(part))


def validate_reference_path(path, invalid_chars=None):
"""Normalize and validate a cloud reference path.

Parameters
----------
path : str | list[str] | tuple[str]
Path as a slash-delimited string or as path segments.
invalid_chars : set[str] | None, optional
Characters that are not allowed in each path segment.

Returns
-------
list[str]
Normalized and validated path segments.

Raises
------
ValueError
If the path is empty or contains invalid characters.
"""
parts = path_to_parts(path)
validate_reference_parts(parts, invalid_chars=invalid_chars)
return parts
35 changes: 22 additions & 13 deletions src/compas_xr/project/project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ def upload_data_to_project(self, data: Any, project_name: str, data_name: str) -
data_name
The name of the child in which data will be stored.
"""
self.database.upload_data_to_reference_as_child(data, project_name, data_name)
path = "{}/{}".format(project_name, data_name)
self.database.upload_data(data, path)

def upload_project_data_from_compas(
self,
Expand Down Expand Up @@ -143,7 +144,8 @@ def upload_qr_frames_to_project(self, project_name: str, qr_frames_list: list[Fr
"""
qr_assembly = AssemblyExtensions().create_qr_assembly(qr_frames_list)
data = qr_assembly.__data__
self.database.upload_data_to_reference_as_child(data, project_name, "QRFrames")
path = "{}/{}".format(project_name, "QRFrames")
self.database.upload_data(data, path)

def upload_obj_to_storage(self, path_local: str, storage_folder_name: str) -> None:
"""
Expand All @@ -156,8 +158,9 @@ def upload_obj_to_storage(self, path_local: str, storage_folder_name: str) -> No
storage_folder_name
The name of the storage folder where the .obj file will be uploaded.
"""
storage_folder_list = ["obj_storage", storage_folder_name]
self.storage.upload_file_as_bytes_to_deep_reference(path_local, storage_folder_list)
file_name = os.path.basename(path_local)
storage_path = "obj_storage/{}/{}".format(storage_folder_name, file_name)
self.storage.upload_file_as_bytes_to_path(path_local, storage_path)

def upload_objs_from_directory_to_storage(self, local_directory: str, storage_folder_name: str) -> None:
"""
Expand All @@ -170,8 +173,14 @@ def upload_objs_from_directory_to_storage(self, local_directory: str, storage_fo
storage_folder_name
The name of the storage folder where the .obj files will be uploaded.
"""
storage_folder_list = ["obj_storage", storage_folder_name]
self.storage.upload_files_as_bytes_from_directory_to_deep_reference(local_directory, storage_folder_list)
if not os.path.exists(local_directory) or not os.path.isdir(local_directory):
raise FileNotFoundError("Directory not found: {}".format(local_directory))

for file_name in os.listdir(local_directory):
local_path = os.path.join(local_directory, file_name)
if os.path.isfile(local_path):
storage_path = "obj_storage/{}/{}".format(storage_folder_name, file_name)
self.storage.upload_file_as_bytes_to_path(local_path, storage_path)

def get_project_data(self, project_name: str) -> dict:
"""
Expand Down Expand Up @@ -248,13 +257,13 @@ def edit_step_on_database(
The priority of the step.

"""
database_reference_list = [project_name, "building_plan", "data", "steps", key, "data"]
current_data = self.database.get_data_from_deep_reference(database_reference_list)
database_path = "{}/building_plan/data/steps/{}/data".format(project_name, key)
current_data = self.database.get_data(database_path)
current_data["actor"] = actor
current_data["is_built"] = is_built
current_data["is_planned"] = is_planned
current_data["priority"] = priority
self.database.upload_data_to_deep_reference(current_data, database_reference_list)
self.database.upload_data(current_data, database_path)

def visualize_project_state_timbers(
self,
Expand Down Expand Up @@ -288,8 +297,8 @@ def visualize_project_state_timbers(

"""
nodes = timber_assembly.graph.__data__["node"]
buiding_plan_data_reference_list = [project_name, "building_plan", "data"]
current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list)
building_plan_data_path = "{}/building_plan/data".format(project_name)
current_state_data = self.database.get_data(building_plan_data_path)

built_human = []
unbuilt_human = []
Expand Down Expand Up @@ -364,8 +373,8 @@ def visualize_project_state(self, assembly: Assembly, project_name: str):
The parts that have not been built by a robot.

"""
buiding_plan_data_reference_list = [project_name, "building_plan", "data"]
current_state_data = self.database.get_data_from_deep_reference(buiding_plan_data_reference_list)
building_plan_data_path = "{}/building_plan/data".format(project_name)
current_state_data = self.database.get_data(building_plan_data_path)
nodes = assembly.graph.__data__["node"]

built_human = []
Expand Down
Loading
Loading