From 29204cd5ecd449040420afc9414bb37de4895205 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 13 Mar 2026 18:56:57 -0400 Subject: [PATCH 01/11] update RealtimeDatabase to take path This is simply updating the RealtimeDatabase class to take paths instead the previous API for children and reference names. --- src/compas_xr/_path.py | 98 +++++++ .../realtime_database/realtime_database.py | 248 ++++-------------- 2 files changed, 151 insertions(+), 195 deletions(-) create mode 100644 src/compas_xr/_path.py diff --git a/src/compas_xr/_path.py b/src/compas_xr/_path.py new file mode 100644 index 00000000..1f98d2ac --- /dev/null +++ b/src/compas_xr/_path.py @@ -0,0 +1,98 @@ +def normalize_path(path): + """Normalize a slash-delimited cloud path. + + Parameters + ---------- + path : str | list[str] | tuple[str] + 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 + 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 \ No newline at end of file diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index 818bd3c6..abc0044f 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -7,6 +7,7 @@ import pyrebase from compas.data import json_dumps +from compas_xr._path import validate_reference_path class RealtimeDatabase: @@ -32,6 +33,7 @@ class RealtimeDatabase: """ _shared_database = None + _INVALID_KEY_CHARS = set(".#$[]/") def __init__(self, config_path): self.config_path = config_path @@ -57,81 +59,31 @@ def _ensure_database(self): if not RealtimeDatabase._shared_database: raise Exception("Could not initialize database!") - def construct_reference(self, parentname): + def construct_reference(self, path): """ - Constructs a database reference under the specified parent name. + Constructs a database reference from a slash-delimited path. Parameters ---------- - parentname : str - The name of the parent under which the reference will be constructed. + path : str + A database path like "parent/child/grandchild". Returns ------- :class: 'pyrebase.pyrebase.Database' The constructed database reference. - """ - return RealtimeDatabase._shared_database.child(parentname) - - def construct_child_refrence(self, parentname, childname): - """ - Constructs a database reference under the specified parent name & child name. - - Parameters - ---------- - parentname : str - The name of the parent under which the reference will be constructed. - childname : str - The name of the child under which the reference will be constructed. - - Returns - ------- - :class: 'pyrebase.pyrebase.Database' - The constructed database reference. - - """ - return RealtimeDatabase._shared_database.child(parentname).child(childname) - - def construct_grandchild_refrence(self, parentname, childname, grandchildname): - """ - Constructs a database reference under the specified parent name, child name, & grandchild name. - - Parameters - ---------- - parentname : str - The name of the parent under which the reference will be constructed. - childname : str - The name of the child under which the reference will be constructed. - grandchildname : str - The name of the grandchild under which the reference will be constructed. - - Returns - ------- - :class: 'pyrebase.pyrebase.Database' - The constructed database reference. - - """ - return RealtimeDatabase._shared_database.child(parentname).child(childname).child(grandchildname) - - def construct_reference_from_list(self, reference_list): - """ - Constructs a database reference under the specified refrences in list order. - - Parameters - ---------- - reference_list : list of str - The name of the parent under which the reference will be constructed. - - Returns - ------- - :class: 'pyrebase.pyrebase.Database' - The constructed database reference. + Raises + ------ + ValueError + If the path is empty or contains invalid Firebase key characters. """ + self._ensure_database() + parts = validate_reference_path(path, invalid_chars=RealtimeDatabase._INVALID_KEY_CHARS) reference = RealtimeDatabase._shared_database - for ref in reference_list: - reference = reference.child(ref) + for part in parts: + reference = reference.child(part) return reference def delete_data_from_reference(self, database_reference): @@ -174,95 +126,75 @@ def get_data_from_reference(self, database_reference): def stream_data_from_reference(self, callback, database_reference): raise NotImplementedError("Function Under Developement") - def upload_data_to_reference(self, data, database_reference): - """ - Method for uploading data to a constructed database reference. - - Parameters - ---------- - data : Any - The data to be uploaded. Data should be JSON serializable. - database_reference: 'pyrebase.pyrebase.Database' - Reference to the database location where the data will be uploaded. - - Returns - ------- - None + def stream_data(self, path, callback): """ - self._ensure_database() - # TODO: Check if this is stupid... it provides the functionality of making it work with compas objects and consistency across both child classes - json_string = json_dumps(data) - database_reference.set(json.loads(json_string)) - - def upload_data(self, data, reference_name): - """ - Uploads data to the Firebase Realtime Database under specified reference name. + Streams data from the Firebase Realtime Database at the specified path. Parameters ---------- - data : Any - The data to be uploaded, needs to be JSON serializable. - reference_name : str - The name of the reference under which the data will be stored. + path : str + The path from which data should be streamed. + callback : callable + Callback used by the stream client. Returns ------- - None + Any + Stream handle/object returned by the underlying implementation. """ - database_reference = self.construct_reference(reference_name) - self.upload_data_to_reference(data, database_reference) + database_reference = self.construct_reference(path) + return self.stream_data_from_reference(callback, database_reference) - def upload_data_to_reference_as_child(self, data, reference_name, child_name): + def upload_data_to_reference(self, data, database_reference): """ - Uploads data to the Firebase Realtime Database under specified reference name & child name. + Method for uploading data to a constructed database reference. Parameters ---------- data : Any - The data to be uploaded, needs to be JSON serializable. - reference_name : str - The name of the reference under which the child should exist. - child_name : str - The name of the reference under which the data will be stored. + The data to be uploaded. Data should be JSON serializable. + database_reference: 'pyrebase.pyrebase.Database' + Reference to the database location where the data will be uploaded. Returns ------- None - """ - database_reference = self.construct_child_refrence(reference_name, child_name) - self.upload_data_to_reference(data, database_reference) + self._ensure_database() + # TODO: Check if this is stupid... it provides the functionality of making it work with compas objects and consistency across both child classes + json_string = json_dumps(data) + database_reference.set(json.loads(json_string)) - def upload_data_to_deep_reference(self, data, reference_list): + def upload_data(self, data, path): """ - Uploads data to the Firebase Realtime Database under specified reference names in list order. + Uploads data to the Firebase Realtime Database at the specified path. Parameters ---------- data : Any The data to be uploaded, needs to be JSON serializable. - reference_list : list of str - The names in sequence order in which the data should be nested for upload. + path : str + The path under which the data will be stored. Returns ------- None """ - database_reference = self.construct_reference_from_list(reference_list) + database_reference = self.construct_reference(path) self.upload_data_to_reference(data, database_reference) - def upload_data_from_file(self, path_local, refernce_name): + def upload_data_from_file(self, path_local, path): """ - Uploads data to the Firebase Realtime Database under specified reference name from a file. + Uploads data to the Firebase Realtime Database at the specified path from a file. Parameters ---------- path_local : str The local path in which the data is stored as a json file. - reference_name : str - The name of the reference under which the data will be stored. + path : str + The path under which the data will be stored. Returns ------- @@ -273,17 +205,17 @@ def upload_data_from_file(self, path_local, refernce_name): raise Exception("path does not exist {}".format(path_local)) with open(path_local) as config_file: data = json.load(config_file) - database_reference = self.construct_reference(refernce_name) + database_reference = self.construct_reference(path) self.upload_data_to_reference(data, database_reference) - def get_data(self, reference_name): + def get_data(self, path): """ - Retrieves data from the Firebase Realtime Database under the specified reference name. + Retrieves data from the Firebase Realtime Database at the specified path. Parameters ---------- - reference_name : str - The name of the reference under which the data is stored. + path : str + The path under which the data is stored. Returns ------- @@ -291,97 +223,23 @@ def get_data(self, reference_name): The retrieved data in dictionary format. """ - database_reference = self.construct_reference(reference_name) + database_reference = self.construct_reference(path) return self.get_data_from_reference(database_reference) - def get_data_from_child_reference(self, reference_name, child_name): + def delete_data(self, path): """ - Retreives data from the Firebase Realtime Database under specified reference name & child name. + Deletes data from the Firebase Realtime Database at the specified path. Parameters ---------- - reference_name : str - The name of the reference under which the child exists. - child_name : str - The name of the reference under which the data is stored. - - Returns - ------- - data : dict - The retrieved data in dictionary format. - - """ - database_reference = self.construct_child_refrence(reference_name, child_name) - return self.get_data_from_reference(database_reference) - - def get_data_from_deep_reference(self, reference_list): - """ - Retreives data from the Firebase Realtime Database under specified reference names in list order. - - Parameters - ---------- - data : Any - The data to be uploaded, needs to be JSON serializable. - reference_list : list of str - The names in sequence order in which the is nested. - - Returns - ------- - data : dict - The retrieved data in dictionary format. - """ - database_reference = self.construct_reference_from_list(reference_list) - return self.get_data_from_reference(database_reference) - - def delete_data(self, reference_name): - """ - Deletes data from the Firebase Realtime Database under specified reference name. - - Parameters - ---------- - reference_name : str - The name of the reference under which the child should exist. - - Returns - ------- - None - - """ - database_reference = self.construct_reference(reference_name) - self.delete_data_from_reference(database_reference) - - def delete_data_from_child_reference(self, reference_name, child_name): - """ - Deletes data from the Firebase Realtime Database under specified reference name & child name. - - Parameters - ---------- - reference_name : str - The name of the reference under which the child should exist. - child_name : str - The name of the reference under which the data will be stored. + path : str + The path that should be deleted. Returns ------- None """ - database_reference = self.construct_child_refrence(reference_name, child_name) + database_reference = self.construct_reference(path) self.delete_data_from_reference(database_reference) - def delete_data_from_deep_reference(self, reference_list): - """ - Deletes data from the Firebase Realtime Database under specified reference names in list order. - - Parameters - ---------- - reference_list : list of str - The names in sequence order in which the data should be nested for upload. - - Returns - ------- - None - - """ - database_reference = self.construct_reference_from_list(reference_list) - self.delete_data_from_reference(database_reference) From 863696df948434e2eb2222411e52cab5fd4a277a Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 13 Mar 2026 19:26:38 -0400 Subject: [PATCH 02/11] update storage to path based based reference This is updating to a path based reference other than making something that uploads the previous child based methods. --- src/compas_xr/storage/storage.py | 201 +++++-------------------------- 1 file changed, 29 insertions(+), 172 deletions(-) diff --git a/src/compas_xr/storage/storage.py b/src/compas_xr/storage/storage.py index 8af646ad..d815d618 100644 --- a/src/compas_xr/storage/storage.py +++ b/src/compas_xr/storage/storage.py @@ -5,6 +5,7 @@ import pyrebase from compas.data import json_dumps from compas.data import json_loads +from compas_xr._path import validate_reference_path try: # from urllib.request import urlopen @@ -74,50 +75,14 @@ def _get_file_from_remote(self, url): else: raise Exception("unable to get file from url {}".format(url)) - def construct_reference(self, cloud_file_name): + def construct_reference(self, path): """ - Constructs a storage reference for the specified cloud file name. + Constructs a storage reference from a slash-delimited path. Parameters ---------- - cloud_file_name : str - The name of the cloud file. - - Returns - ------- - :class: 'pyrebase.pyrebase.Storage' - The constructed storage reference. - - """ - return Storage._shared_storage.child(cloud_file_name) - - def construct_reference_with_folder(self, cloud_folder_name, cloud_file_name): - """ - Constructs a storage reference for the specified cloud folder name and file name. - - Parameters - ---------- - cloud_folder_name : str - The name of the cloud folder. - cloud_file_name : str - The name of the cloud file. - - Returns - ------- - :class: 'pyrebase.pyrebase.Storage' - The constructed storage reference. - - """ - return Storage._shared_storage.child(cloud_folder_name).child(cloud_file_name) - - def construct_reference_from_list(self, cloud_path_list): - """ - Constructs a storage reference for consecutive cloud folders in list order. - - Parameters - ---------- - cloud_path_list : list of str - The list of cloud path names. + path : str + A storage path like "folder/subfolder/file.json". Returns ------- @@ -125,9 +90,11 @@ def construct_reference_from_list(self, cloud_path_list): The constructed storage reference. """ + self._ensure_storage() + parts = validate_reference_path(path) storage_reference = Storage._shared_storage - for path in cloud_path_list: - storage_reference = storage_reference.child(path) + for part in parts: + storage_reference = storage_reference.child(part) return storage_reference def get_data_from_reference(self, storage_reference): @@ -194,16 +161,16 @@ def upload_data_to_reference(self, data, storage_reference, pretty=True): file_object = io.BytesIO(serialized_data.encode()) storage_reference.put(file_object) - def upload_data(self, data, cloud_file_name, pretty=True): + def upload_data(self, data, path, pretty=True): """ - Uploads data to the Firebase Storage under specified cloud file name. + Uploads data to the Firebase Storage at the specified path. Parameters ---------- data : Any The data to be uploaded, needs to be JSON serializable. - cloud_file_name : str - The name of the reference under which the data will be stored file type should be specified.(ex: .json) + path : str + The path under which the data will be stored. pretty : bool, optional A boolean that determines if the data should be formatted for readability. Default is True. @@ -212,7 +179,7 @@ def upload_data(self, data, cloud_file_name, pretty=True): None """ - storage_reference = self.construct_reference(cloud_file_name) + storage_reference = self.construct_reference(path) self.upload_data_to_reference(data, storage_reference, pretty) def upload_data_from_json(self, path_local, pretty=True): @@ -239,50 +206,6 @@ def upload_data_from_json(self, path_local, pretty=True): storage_reference = self.construct_reference(cloud_file_name) self.upload_data_to_reference(data, storage_reference, pretty) - def upload_data_to_folder(self, data, cloud_folder_name, cloud_file_name, pretty=True): - """ - Uploads data to the Firebase Storage under specified cloud folder name in cloud file name. - - Parameters - ---------- - data : Any - The data to be uploaded, needs to be JSON serializable. - cloud_folder_name : str - The name of the folder under which the data will be stored. - cloud_file_name : str - The name of the reference under which the data will be stored file type should be specified.(ex: .json) - pretty : bool, optional - A boolean that determines if the data should be formatted for readability. Default is True. - - Returns - ------- - None - - """ - storage_reference = self.construct_reference_with_folder(cloud_folder_name, cloud_file_name) - self.upload_data_to_reference(data, storage_reference, pretty) - - def upload_data_to_deep_reference(self, data, cloud_path_list, pretty=True): - """ - Uploads data to the Firebase Storage under specified reference names in list order. - - Parameters - ---------- - data : Any - The data to be uploaded, needs to be JSON serializable. - cloud_path_list : list of str - The list of reference names under which the data will be stored file type should be specified.(ex: .json) - pretty : bool, optional - A boolean that determines if the data should be formatted for readability. Default is True. - - Returns - ------- - None - - """ - storage_reference = self.construct_reference_from_list(cloud_path_list) - self.upload_data_to_reference(data, storage_reference, pretty) - def upload_file_as_bytes(self, file_path): """ Uploads a file as bytes to the Firebase Storage. @@ -303,16 +226,16 @@ def upload_file_as_bytes(self, file_path): storage_reference = self.construct_reference(file_name) self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) - def upload_file_as_bytes_to_deep_reference(self, file_path, cloud_path_list): + def upload_file_as_bytes_to_path(self, file_path, path): """ - Uploads a file as bytes to the Firebase Storage to specified cloud path. + Uploads a file as bytes to the Firebase Storage to the specified path. Parameters ---------- file_path : str The local path of the file to be uploaded. - cloud_path_list : list of str - The list of reference names under which the file will be stored. + path : str + The path under which the file will be stored. Returns ------- @@ -321,83 +244,17 @@ def upload_file_as_bytes_to_deep_reference(self, file_path, cloud_path_list): """ if not os.path.exists(file_path): raise FileNotFoundError("File not found: {}".format(file_path)) - file_name = os.path.basename(file_path) - new_path_list = list(cloud_path_list) - new_path_list.append(file_name) - - storage_reference = self.construct_reference_from_list(new_path_list) + storage_reference = self.construct_reference(path) self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) - def upload_files_as_bytes_from_directory_to_deep_reference(self, directory_path, cloud_path_list): - """ - Uploads all files in specified directory as bytes to the Firebase Storage at specified cloud path in list order. - - Parameters - ---------- - directory_path : str - The local path of the directory in which files are stored. - cloud_path_list : list of str - The list of reference names under which the file will be stored. - - Returns - ------- - None - - """ - if not os.path.exists(directory_path) or not os.path.isdir(directory_path): - raise FileNotFoundError("Directory not found: {}".format(directory_path)) - for file_name in os.listdir(directory_path): - file_path = os.path.join(directory_path, file_name) - self.upload_file_as_bytes_to_deep_reference(file_path, cloud_path_list) - - def get_data(self, cloud_file_name): - """ - Retrieves data from the Firebase Storage for specified cloud file name. - - Parameters - ---------- - cloud_file_name : str - The name of the cloud file. - - Returns - ------- - data : dict or Compas Class Object - The retrieved data in dictionary format or as Compas Class Object. - - """ - storage_reference = self.construct_reference(cloud_file_name) - return self.get_data_from_reference(storage_reference) - - def get_data_from_folder(self, cloud_folder_name, cloud_file_name): - """ - Retrieves data from the Firebase Storage for specified cloud folder name and cloud file name. - - Parameters - ---------- - cloud_folder_name : str - The name of the cloud folder. - cloud_file_name : str - The name of the cloud file. - - Returns - ------- - data : dict or Compas Class Object - The retrieved data in dictionary format or as Compas Class Object. - - """ - storage_reference = self.construct_reference_with_folder(cloud_folder_name, cloud_file_name) - return self.get_data_from_reference(storage_reference) - - def get_data_from_deep_reference(self, cloud_path_list): + def get_data(self, path): """ - Retrieves data from the Firebase Storage for specified cloud folder name and cloud file name. + Retrieves data from the Firebase Storage for the specified path. Parameters ---------- - cloud_folder_name : str - The name of the cloud folder. - cloud_file_name : str - The name of the cloud file. + path : str + The storage path. Returns ------- @@ -405,17 +262,17 @@ def get_data_from_deep_reference(self, cloud_path_list): The retrieved data in dictionary format or as Compas Class Object. """ - storage_reference = self.construct_reference_from_list(cloud_path_list) + storage_reference = self.construct_reference(path) return self.get_data_from_reference(storage_reference) - def download_data_to_json(self, cloud_file_name, path_local, pretty=True): + def download_data_to_json(self, path, path_local, pretty=True): """ - Downloads data from the Firebase Storage for specified cloud file name. + Downloads data from the Firebase Storage for the specified path. Parameters ---------- - cloud_file_name : str - The name of the cloud file. + path : str + The storage path. path_local : str (path) The local path at which the JSON file will be stored. pretty : bool, optional @@ -426,7 +283,7 @@ def download_data_to_json(self, cloud_file_name, path_local, pretty=True): None """ - data = self.get_data(cloud_file_name) + data = self.get_data(path) directory_name = os.path.dirname(path_local) if not os.path.exists(directory_name): raise FileNotFoundError("Directory {} does not exist!".format(directory_name)) From d7c7e7824555375db2e44f84eb337cbe3a555a16 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 13 Mar 2026 19:32:19 -0400 Subject: [PATCH 03/11] Update Project Manager to support new path based writing This includes updating the Project Manager class to the path based referencing. --- src/compas_xr/project/project_manager.py | 35 +++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/compas_xr/project/project_manager.py b/src/compas_xr/project/project_manager.py index 5c8918bd..a7d3ecc7 100644 --- a/src/compas_xr/project/project_manager.py +++ b/src/compas_xr/project/project_manager.py @@ -109,7 +109,8 @@ def upload_data_to_project(self, data, project_name, data_name): None """ - 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, project_name, assembly, building_plan, qr_frames_list): """ @@ -152,7 +153,8 @@ def upload_qr_frames_to_project(self, project_name, qr_frames_list): """ 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, storage_folder_name): """ @@ -170,8 +172,9 @@ def upload_obj_to_storage(self, path_local, storage_folder_name): None """ - 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, storage_folder_name): """ @@ -189,8 +192,14 @@ def upload_objs_from_directory_to_storage(self, local_directory, storage_folder_ None """ - 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): """ @@ -268,13 +277,13 @@ def edit_step_on_database(self, project_name, key, actor, is_built, is_planned, None """ - 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, timber_assembly, project_name): """ @@ -304,8 +313,8 @@ def visualize_project_state_timbers(self, timber_assembly, project_name): """ 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 = [] @@ -380,8 +389,8 @@ def visualize_project_state(self, assembly, project_name): 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 = [] From beccf0b0c2db2c2604da023e2c19218fa57a811e Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 13 Mar 2026 19:42:08 -0400 Subject: [PATCH 04/11] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 162513aa..67b374dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,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. ## [1.0.0] 2024-06-26 From fdc2cef2459ef05f03b8dd880904aff4116be8f6 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 13 Mar 2026 19:46:42 -0400 Subject: [PATCH 05/11] linting and formatting --- src/compas_xr/_path.py | 12 +++--------- src/compas_xr/realtime_database/realtime_database.py | 2 +- src/compas_xr/storage/storage.py | 1 + 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/compas_xr/_path.py b/src/compas_xr/_path.py index 1f98d2ac..af8ac447 100644 --- a/src/compas_xr/_path.py +++ b/src/compas_xr/_path.py @@ -62,15 +62,9 @@ def validate_reference_parts(parts, invalid_chars=None): 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)) - ) - ) + 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) - ) + raise ValueError("invalid path segment '{}': contains control characters".format(part)) def validate_reference_path(path, invalid_chars=None): @@ -95,4 +89,4 @@ def validate_reference_path(path, invalid_chars=None): """ parts = path_to_parts(path) validate_reference_parts(parts, invalid_chars=invalid_chars) - return parts \ No newline at end of file + return parts diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index abc0044f..226caf22 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -7,6 +7,7 @@ import pyrebase from compas.data import json_dumps + from compas_xr._path import validate_reference_path @@ -242,4 +243,3 @@ def delete_data(self, path): """ database_reference = self.construct_reference(path) self.delete_data_from_reference(database_reference) - diff --git a/src/compas_xr/storage/storage.py b/src/compas_xr/storage/storage.py index d815d618..15c37fd6 100644 --- a/src/compas_xr/storage/storage.py +++ b/src/compas_xr/storage/storage.py @@ -5,6 +5,7 @@ import pyrebase from compas.data import json_dumps from compas.data import json_loads + from compas_xr._path import validate_reference_path try: From a984b105d4afddaea53f2c22e6b31f639ff22c6d Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Sat, 14 Mar 2026 12:58:49 -0400 Subject: [PATCH 06/11] Annotate RealtimeDatabase Method Signatures This is updating the method signatures in the overall class. As most of this had to be undone in the conflict resolution. --- .../realtime_database/realtime_database.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index f9f97f49..2da8c620 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -1,6 +1,7 @@ import json import os from typing import Any +from typing import Callable from typing import List import pyrebase @@ -38,7 +39,7 @@ def __init__(self, config_path: str): self.config_path = config_path self._ensure_database() - def _ensure_database(self): + def _ensure_database(self) -> None: """ Ensures that the database connection is established. If the connection is not yet established, it initializes it. @@ -58,7 +59,7 @@ def _ensure_database(self): if not RealtimeDatabase._shared_database: raise Exception("Could not initialize database!") - def construct_reference(self, path): + def construct_reference(self, path: str) -> pyrebase.pyrebase.Database: """ Constructs a database reference from a slash-delimited path. @@ -119,10 +120,10 @@ def get_data_from_reference(self, database_reference: pyrebase.pyrebase.Database data_dict = dict(data) return data_dict - def stream_data_from_reference(self, callback, database_reference): + def stream_data_from_reference(self, callback: Callable, database_reference: pyrebase.pyrebase.Database) -> Any: raise NotImplementedError("Function Under Developement") - def stream_data(self, path, callback): + def stream_data(self, path: str, callback: Callable) -> Any: """ Streams data from the Firebase Realtime Database at the specified path. @@ -142,7 +143,7 @@ def stream_data(self, path, callback): database_reference = self.construct_reference(path) return self.stream_data_from_reference(callback, database_reference) - def upload_data_to_reference(self, data, database_reference): + def upload_data_to_reference(self, data: Any, database_reference: pyrebase.pyrebase.Database) -> None: """ Method for uploading data to a constructed database reference. @@ -162,7 +163,7 @@ def upload_data_to_reference(self, data, database_reference): json_string = json_dumps(data) database_reference.set(json.loads(json_string)) - def upload_data(self, data, path): + def upload_data(self, data: Any, path: str) -> None: """ Uploads data to the Firebase Realtime Database at the specified path. @@ -181,7 +182,7 @@ def upload_data(self, data, path): database_reference = self.construct_reference(path) self.upload_data_to_reference(data, database_reference) - def upload_data_from_file(self, path_local, path): + def upload_data_from_file(self, path_local: str, path: str) -> None: """ Uploads data to the Firebase Realtime Database at the specified path from a file. @@ -204,7 +205,7 @@ def upload_data_from_file(self, path_local, path): database_reference = self.construct_reference(path) self.upload_data_to_reference(data, database_reference) - def get_data(self, path): + def get_data(self, path: str) -> dict: """ Retrieves data from the Firebase Realtime Database at the specified path. @@ -222,7 +223,7 @@ def get_data(self, path): database_reference = self.construct_reference(path) return self.get_data_from_reference(database_reference) - def delete_data(self, path): + def delete_data(self, path: str) -> None: """ Deletes data from the Firebase Realtime Database at the specified path. From fed87796a786cb74298e0a230967b0c3e92af5f6 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Sat, 14 Mar 2026 13:00:31 -0400 Subject: [PATCH 07/11] Annotate method signatures inside of Storage Class. This is simple as well, and does the same to update method signatures in because of conflict resolution. --- src/compas_xr/storage/storage.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/compas_xr/storage/storage.py b/src/compas_xr/storage/storage.py index a57cfa9b..1b2ac608 100644 --- a/src/compas_xr/storage/storage.py +++ b/src/compas_xr/storage/storage.py @@ -41,7 +41,7 @@ def __init__(self, config_path: str): self.config_path = config_path self._ensure_storage() - def _ensure_storage(self): + def _ensure_storage(self) -> None: """ Ensures that the storage connection is established. If the connection is not yet established, it initializes it. @@ -60,7 +60,7 @@ def _ensure_storage(self): if not Storage._shared_storage: raise Exception("Could not initialize storage!") - def _get_file_from_remote(self, url): + def _get_file_from_remote(self, url: str) -> str: """ This function is used to get the information form the source url and returns a string It also checks if the data is None or == null (firebase return if no data) @@ -76,7 +76,7 @@ def _get_file_from_remote(self, url): else: raise Exception("unable to get file from url {}".format(url)) - def construct_reference(self, path): + def construct_reference(self, path: str) -> pyrebase.pyrebase.Storage: """ Constructs a storage reference from a slash-delimited path. @@ -157,7 +157,7 @@ def upload_data_to_reference(self, data: Any, storage_reference: Any, pretty: bo file_object = io.BytesIO(serialized_data.encode()) storage_reference.put(file_object) - def upload_data(self, data, path, pretty=True): + def upload_data(self, data: Any, path: str, pretty: bool = True) -> None: """ Uploads data to the Firebase Storage at the specified path. @@ -178,7 +178,7 @@ def upload_data(self, data, path, pretty=True): storage_reference = self.construct_reference(path) self.upload_data_to_reference(data, storage_reference, pretty) - def upload_data_from_json(self, path_local: str, pretty: bool = True): + def upload_data_from_json(self, path_local: str, pretty: bool = True) -> None: """ Uploads data to the Firebase Storage from JSON file. @@ -198,7 +198,7 @@ def upload_data_from_json(self, path_local: str, pretty: bool = True): storage_reference = self.construct_reference(cloud_file_name) self.upload_data_to_reference(data, storage_reference, pretty) - def upload_file_as_bytes(self, file_path): + def upload_file_as_bytes(self, file_path: str) -> None: """ Uploads a file as bytes to the Firebase Storage. @@ -213,7 +213,7 @@ def upload_file_as_bytes(self, file_path): storage_reference = self.construct_reference(file_name) self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) - def upload_file_as_bytes_to_path(self, file_path, path): + def upload_file_as_bytes_to_path(self, file_path: str, path: str) -> None: """ Uploads a file as bytes to the Firebase Storage to the specified path. @@ -234,7 +234,7 @@ def upload_file_as_bytes_to_path(self, file_path, path): storage_reference = self.construct_reference(path) self.upload_bytes_to_reference_from_local_file(file_path, storage_reference) - def get_data(self, path): + def get_data(self, path: str) -> Union[dict, Data]: """ Retrieves data from the Firebase Storage for the specified path. @@ -252,7 +252,7 @@ def get_data(self, path): storage_reference = self.construct_reference(path) return self.get_data_from_reference(storage_reference) - def download_data_to_json(self, path, path_local, pretty=True): + def download_data_to_json(self, path: str, path_local: str, pretty: bool = True) -> None: """ Downloads data from the Firebase Storage for the specified path. From ab05c54da396fe94b6fda603ae19b13abf0771fc Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Sat, 14 Mar 2026 13:42:07 -0400 Subject: [PATCH 08/11] linting and formatting --- .../realtime_database/realtime_database.py | 1 - uv.lock | 84 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index 2da8c620..2319ba30 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -2,7 +2,6 @@ import os from typing import Any from typing import Callable -from typing import List import pyrebase from compas.data import json_dumps diff --git a/uv.lock b/uv.lock index 7f50c79a..2cdd366d 100644 --- a/uv.lock +++ b/uv.lock @@ -572,6 +572,8 @@ dev = [ { name = "bump-my-version" }, { name = "compas-invocations2", extra = ["mkdocs"] }, { name = "invoke" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pythonnet" }, { name = "rhino-stubs" }, { name = "ruff" }, @@ -591,6 +593,7 @@ requires-dist = [ { name = "compas-timber", specifier = "~=0.7.0" }, { name = "invoke", marker = "extra == 'dev'", specifier = ">=0.14" }, { name = "pyrebase4", specifier = ">=4.7.1" }, + { name = "pytest", marker = "extra == 'dev'" }, { name = "pythonnet", marker = "extra == 'dev'" }, { name = "rhino-stubs", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'" }, @@ -996,6 +999,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "invoke" version = "2.2.1" @@ -2128,6 +2158,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -2490,6 +2529,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/8c/c103832d7df2a48985c17cbac3b8d1acdca7b31eac91d627ec6e6e76e01e/pyrebase4-4.9.0-py3-none-any.whl", hash = "sha256:e7eb0c248d0170c834a8dabbc0fe4f14aefdfa904e93fa06ed37f6fa0b864ce3", size = 9126, upload-time = "2026-01-19T17:58:24.271Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From 3484c921d465de57afbbe65ea6a9770bf83f61cd Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 27 Mar 2026 11:17:51 -0400 Subject: [PATCH 09/11] update doc strings for method type_hint This is simple, removing information in doc strings because of the type hint. --- .../realtime_database/realtime_database.py | 18 +++++++++--------- src/compas_xr/storage/storage.py | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index 2319ba30..f4e4cc46 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -64,7 +64,7 @@ def construct_reference(self, path: str) -> pyrebase.pyrebase.Database: Parameters ---------- - path : str + path A database path like "parent/child/grandchild". Returns @@ -128,9 +128,9 @@ def stream_data(self, path: str, callback: Callable) -> Any: Parameters ---------- - path : str + path The path from which data should be streamed. - callback : callable + callback Callback used by the stream client. Returns @@ -148,9 +148,9 @@ def upload_data_to_reference(self, data: Any, database_reference: pyrebase.pyreb Parameters ---------- - data : Any + data The data to be uploaded. Data should be JSON serializable. - database_reference: 'pyrebase.pyrebase.Database' + database_reference Reference to the database location where the data will be uploaded. Returns @@ -170,7 +170,7 @@ def upload_data(self, data: Any, path: str) -> None: ---------- data The data to be uploaded, needs to be JSON serializable. - path : str + path The path under which the data will be stored. Returns @@ -189,7 +189,7 @@ def upload_data_from_file(self, path_local: str, path: str) -> None: ---------- path_local The local path in which the data is stored as a json file. - path : str + path The path under which the data will be stored. Returns @@ -210,7 +210,7 @@ def get_data(self, path: str) -> dict: Parameters ---------- - path : str + path The path under which the data is stored. Returns @@ -228,7 +228,7 @@ def delete_data(self, path: str) -> None: Parameters ---------- - path : str + path The path that should be deleted. Returns diff --git a/src/compas_xr/storage/storage.py b/src/compas_xr/storage/storage.py index 1b2ac608..ecbc744c 100644 --- a/src/compas_xr/storage/storage.py +++ b/src/compas_xr/storage/storage.py @@ -82,7 +82,7 @@ def construct_reference(self, path: str) -> pyrebase.pyrebase.Storage: Parameters ---------- - path : str + path A storage path like "folder/subfolder/file.json". Returns @@ -165,9 +165,9 @@ def upload_data(self, data: Any, path: str, pretty: bool = True) -> None: ---------- data The data to be uploaded, needs to be JSON serializable. - path : str + path The path under which the data will be stored. - pretty : bool, optional + pretty A boolean that determines if the data should be formatted for readability. Default is True. Returns @@ -221,7 +221,7 @@ def upload_file_as_bytes_to_path(self, file_path: str, path: str) -> None: ---------- file_path The local path of the file to be uploaded. - path : str + path The path under which the file will be stored. Returns @@ -240,7 +240,7 @@ def get_data(self, path: str) -> Union[dict, Data]: Parameters ---------- - path : str + path The storage path. Returns @@ -258,9 +258,9 @@ def download_data_to_json(self, path: str, path_local: str, pretty: bool = True) Parameters ---------- - path : str + path The storage path. - path_local : str (path) + path_local The local path at which the JSON file will be stored. pretty A boolean that determines if the data should be formatted for readability. From 6ec3df2d6e0408d689945074c3c1a8a83b19fb54 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 27 Mar 2026 11:22:25 -0400 Subject: [PATCH 10/11] update doc string for type_hint using Union() Update --- src/compas_xr/_path.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/compas_xr/_path.py b/src/compas_xr/_path.py index af8ac447..2c1f347d 100644 --- a/src/compas_xr/_path.py +++ b/src/compas_xr/_path.py @@ -1,9 +1,12 @@ -def normalize_path(path): +from typing import Union + + +def normalize_path(path: Union[str, list[str], tuple[str, ...]]) -> str: """Normalize a slash-delimited cloud path. Parameters ---------- - path : str | list[str] | tuple[str] + path Path as a slash-delimited string or as path segments. Returns @@ -29,7 +32,7 @@ def normalize_path(path): return "/".join(parts) -def path_to_parts(path): +def path_to_parts(path: Union[str, list[str], tuple[str, ...]]) -> list[str]: """Convert a path string or path parts to normalized path segments.""" normalized = normalize_path(path) if not normalized: @@ -37,14 +40,14 @@ def path_to_parts(path): return normalized.split("/") -def validate_reference_parts(parts, invalid_chars=None): +def validate_reference_parts(parts: Union[list[str], tuple[str, ...]], invalid_chars: Union[set[str], None] = None) -> None: """Validate normalized path segments for cloud references. Parameters ---------- - parts : list[str] | tuple[str] + parts Normalized path segments. - invalid_chars : set[str] | None, optional + invalid_chars Characters that are not allowed in each path segment. Returns @@ -67,14 +70,14 @@ def validate_reference_parts(parts, invalid_chars=None): raise ValueError("invalid path segment '{}': contains control characters".format(part)) -def validate_reference_path(path, invalid_chars=None): +def validate_reference_path(path: Union[str, list[str], tuple[str, ...]], invalid_chars: Union[set[str], None] = None) -> list[str]: """Normalize and validate a cloud reference path. Parameters ---------- - path : str | list[str] | tuple[str] + path Path as a slash-delimited string or as path segments. - invalid_chars : set[str] | None, optional + invalid_chars Characters that are not allowed in each path segment. Returns From 256376f81c71394c7bd7603daa619a78f6072429 Mon Sep 17 00:00:00 2001 From: Joseph Kenny Date: Fri, 27 Mar 2026 11:39:44 -0400 Subject: [PATCH 11/11] update comment in rtdb for reference next time. --- src/compas_xr/realtime_database/realtime_database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index f4e4cc46..832d429b 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -158,7 +158,8 @@ def upload_data_to_reference(self, data: Any, database_reference: pyrebase.pyreb None """ self._ensure_database() - # TODO: Check if this is stupid... it provides the functionality of making it work with compas objects and consistency across both child classes + # Convert COMPAS/complex objects into plain JSON-compatible Python structures. + # Firebase set() expects dict/list/str/int/float/bool/None payloads. json_string = json_dumps(data) database_reference.set(json.loads(json_string))