diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f147059..e3ffb82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/compas_xr/_path.py b/src/compas_xr/_path.py new file mode 100644 index 00000000..2c1f347d --- /dev/null +++ b/src/compas_xr/_path.py @@ -0,0 +1,95 @@ +from typing import Union + + +def normalize_path(path: Union[str, list[str], tuple[str, ...]]) -> str: + """Normalize a slash-delimited cloud path. + + Parameters + ---------- + path + 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: 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: + return [] + return normalized.split("/") + + +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 + Normalized path segments. + invalid_chars + 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: Union[str, list[str], tuple[str, ...]], invalid_chars: Union[set[str], None] = None) -> list[str]: + """Normalize and validate a cloud reference path. + + Parameters + ---------- + path + Path as a slash-delimited string or as path segments. + invalid_chars + 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 diff --git a/src/compas_xr/project/project_manager.py b/src/compas_xr/project/project_manager.py index 3bda7632..9f0d9795 100644 --- a/src/compas_xr/project/project_manager.py +++ b/src/compas_xr/project/project_manager.py @@ -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, @@ -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: """ @@ -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: """ @@ -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: """ @@ -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, @@ -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 = [] @@ -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 = [] diff --git a/src/compas_xr/realtime_database/realtime_database.py b/src/compas_xr/realtime_database/realtime_database.py index 0e9a35ca..832d429b 100644 --- a/src/compas_xr/realtime_database/realtime_database.py +++ b/src/compas_xr/realtime_database/realtime_database.py @@ -1,11 +1,13 @@ import json import os from typing import Any -from typing import List +from typing import Callable import pyrebase from compas.data import json_dumps +from compas_xr._path import validate_reference_path + class RealtimeDatabase: """ @@ -30,12 +32,13 @@ class RealtimeDatabase: """ _shared_database = None + _INVALID_KEY_CHARS = set(".#$[]/") 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. @@ -55,81 +58,31 @@ def _ensure_database(self): if not RealtimeDatabase._shared_database: raise Exception("Could not initialize database!") - def construct_reference(self, parentname: str) -> Any: - """ - Constructs a database reference under the specified parent name. - - Parameters - ---------- - parentname - The name of the parent under which the reference will be constructed. - - Returns - ------- - pyrebase.pyrebase.Database - The constructed database reference. - + def construct_reference(self, path: str) -> pyrebase.pyrebase.Database: """ - return RealtimeDatabase._shared_database.child(parentname) - - def construct_child_refrence(self, parentname: str, childname: str) -> Any: - """ - Constructs a database reference under the specified parent name & child name. + Constructs a database reference from a slash-delimited path. Parameters ---------- - parentname - The name of the parent under which the reference will be constructed. - childname - The name of the child under which the reference will be constructed. + path + A database path like "parent/child/grandchild". Returns ------- pyrebase.pyrebase.Database The constructed database reference. - """ - return RealtimeDatabase._shared_database.child(parentname).child(childname) - - def construct_grandchild_refrence(self, parentname: str, childname: str, grandchildname: str) -> Any: - """ - Constructs a database reference under the specified parent name, child name, & grandchild name. - - Parameters - ---------- - parentname - The name of the parent under which the reference will be constructed. - childname - The name of the child under which the reference will be constructed. - grandchildname - The name of the grandchild under which the reference will be constructed. - - Returns - ------- - 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: List[str]) -> Any: - """ - Constructs a database reference under the specified refrences in list order. - - Parameters - ---------- - reference_list - The name of the parent under which the reference will be constructed. - - Returns - ------- - 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: pyrebase.pyrebase.Database) -> None: @@ -166,102 +119,100 @@ 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 upload_data_to_reference(self, data: Any, database_reference: pyrebase.pyrebase.Database): + def stream_data(self, path: str, callback: Callable) -> Any: """ - Method for uploading data to a constructed database reference. + Streams data from the Firebase Realtime Database at the specified path. Parameters ---------- - data - The data to be uploaded. Data should be JSON serializable. - database_reference - Reference to the database location where the data will be uploaded. + path + The path from which data should be streamed. + callback + Callback used by the stream client. + + Returns + ------- + Any + Stream handle/object returned by the underlying implementation. """ - 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)) + database_reference = self.construct_reference(path) + return self.stream_data_from_reference(callback, database_reference) - def upload_data(self, data: Any, reference_name: str) -> None: + def upload_data_to_reference(self, data: Any, database_reference: pyrebase.pyrebase.Database) -> None: """ - Uploads data to the Firebase Realtime Database under specified reference name. + Method for uploading data to a constructed database reference. Parameters ---------- data - The data to be uploaded, needs to be JSON serializable. - reference_name - The name of the reference under which the data will be stored. + The data to be uploaded. Data should be JSON serializable. + database_reference + Reference to the database location where the data will be uploaded. + Returns + ------- + None """ - database_reference = self.construct_reference(reference_name) - self.upload_data_to_reference(data, database_reference) + self._ensure_database() + # 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)) - def upload_data_to_reference_as_child( - self, - data: Any, - reference_name: str, - child_name: str, - ) -> None: + def upload_data(self, data: Any, path: str) -> None: """ - Uploads data to the Firebase Realtime Database under specified reference name & child name. + Uploads data to the Firebase Realtime Database at the specified path. Parameters ---------- data The data to be uploaded, needs to be JSON serializable. - reference_name - The name of the reference under which the child should exist. - child_name - The name of the reference under which the data will be stored. - """ - database_reference = self.construct_child_refrence(reference_name, child_name) - self.upload_data_to_reference(data, database_reference) + path + The path under which the data will be stored. - def upload_data_to_deep_reference(self, data: Any, reference_list: list[str]) -> None: - """ - Uploads data to the Firebase Realtime Database under specified reference names in list order. + Returns + ------- + None - Parameters - ---------- - data - The data to be uploaded, needs to be JSON serializable. - reference_list - The names in sequence order in which the data should be nested for upload. """ - 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: str, reference_name: str) -> None: + def upload_data_from_file(self, path_local: str, path: str) -> None: """ - 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 The local path in which the data is stored as a json file. - reference_name - The name of the reference under which the data will be stored. + path + The path under which the data will be stored. + + Returns + ------- + None + """ if not os.path.exists(path_local): 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(reference_name) + database_reference = self.construct_reference(path) self.upload_data_to_reference(data, database_reference) - def get_data(self, reference_name: str) -> dict: + def get_data(self, path: str) -> dict: """ - 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 - The name of the reference under which the data is stored. + path + The path under which the data is stored. Returns ------- @@ -269,80 +220,22 @@ def get_data(self, reference_name: str) -> dict: 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: str, child_name: str) -> dict: + def delete_data(self, path: str) -> None: """ - 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 - The name of the reference under which the child exists. - child_name - The name of the reference under which the data is stored. + path + The path that should be deleted. Returns ------- - dict - The retrieved data in dictionary format. + None """ - 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: list[str]) -> dict: - """ - Retreives data from the Firebase Realtime Database under specified reference names in list order. - - Parameters - ---------- - reference_list - The names in sequence order in which the is nested. - - Returns - ------- - 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: str) -> None: - """ - Deletes data from the Firebase Realtime Database under specified reference name. - - Parameters - ---------- - reference_name - The name of the reference under which the child should exist. - """ - database_reference = self.construct_reference(reference_name) - self.delete_data_from_reference(database_reference) - - def delete_data_from_child_reference(self, reference_name: str, child_name: str) -> None: - """ - Deletes data from the Firebase Realtime Database under specified reference name & child name. - - Parameters - ---------- - reference_name - The name of the reference under which the child should exist. - child_name - The name of the reference under which the data will be stored. - """ - database_reference = self.construct_child_refrence(reference_name, child_name) - self.delete_data_from_reference(database_reference) - - def delete_data_from_deep_reference(self, reference_list: list[str]): - """ - Deletes data from the Firebase Realtime Database under specified reference names in list order. - - Parameters - ---------- - reference_list - The names in sequence order in which the data should be nested for upload. - """ - database_reference = self.construct_reference_from_list(reference_list) + 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 74034a80..ecbc744c 100644 --- a/src/compas_xr/storage/storage.py +++ b/src/compas_xr/storage/storage.py @@ -9,6 +9,8 @@ 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 except ImportError: @@ -39,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. @@ -58,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) @@ -74,50 +76,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: str) -> pyrebase.pyrebase.Storage: - """ - Constructs a storage reference for the specified cloud file name. - - Parameters - ---------- - cloud_file_name - The name of the cloud file. - - Returns - ------- - pyrebase.pyrebase.Storage - The constructed storage reference. - - """ - return Storage._shared_storage.child(cloud_file_name) - - def construct_reference_with_folder(self, cloud_folder_name: str, cloud_file_name: str) -> pyrebase.pyrebase.Storage: + def construct_reference(self, path: str) -> pyrebase.pyrebase.Storage: """ - Constructs a storage reference for the specified cloud folder name and file name. + Constructs a storage reference from a slash-delimited path. Parameters ---------- - cloud_folder_name - The name of the cloud folder. - cloud_file_name - The name of the cloud file. - - Returns - ------- - 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: list[str]) -> pyrebase.pyrebase.Storage: - """ - Constructs a storage reference for consecutive cloud folders in list order. - - Parameters - ---------- - cloud_path_list - The list of cloud path names. + path + A storage path like "folder/subfolder/file.json". Returns ------- @@ -125,9 +91,11 @@ def construct_reference_from_list(self, cloud_path_list: list[str]) -> pyrebase. 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: pyrebase.pyrebase.Storage) -> Union[dict, Data]: @@ -189,23 +157,28 @@ 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: Any, cloud_file_name: str, pretty: bool = True) -> None: + def upload_data(self, data: Any, path: str, pretty: bool = True) -> None: """ - Uploads data to the Firebase Storage under specified cloud file name. + Uploads data to the Firebase Storage at the specified path. Parameters ---------- data The data to be uploaded, needs to be JSON serializable. - cloud_file_name - The name of the reference under which the data will be stored file type should be specified.(ex: .json) + path + The path under which the data will be stored. pretty - A boolean that determines if the data should be formatted for readability. + A boolean that determines if the data should be formatted for readability. Default is True. + + Returns + ------- + 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: 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. @@ -225,40 +198,6 @@ 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_data_to_folder(self, data: Any, cloud_folder_name: str, cloud_file_name: str, pretty: bool = True): - """ - Uploads data to the Firebase Storage under specified cloud folder name in cloud file name. - - Parameters - ---------- - data - The data to be uploaded, needs to be JSON serializable. - cloud_folder_name - The name of the folder under which the data will be stored. - cloud_file_name - The name of the reference under which the data will be stored file type should be specified.(ex: .json) - pretty - A boolean that determines if the data should be formatted for readability. - """ - 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: Any, cloud_path_list: list[str], pretty: bool = True): - """ - Uploads data to the Firebase Storage under specified reference names in list order. - - Parameters - ---------- - data - The data to be uploaded, needs to be JSON serializable. - cloud_path_list - The list of reference names under which the data will be stored file type should be specified.(ex: .json) - pretty - A boolean that determines if the data should be formatted for readability. - """ - 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: str) -> None: """ Uploads a file as bytes to the Firebase Storage. @@ -274,16 +213,16 @@ def upload_file_as_bytes(self, file_path: str) -> None: 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: str, cloud_path_list: list[str]): + def upload_file_as_bytes_to_path(self, file_path: str, path: str) -> None: """ - 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 The local path of the file to be uploaded. - cloud_path_list - The list of reference names under which the file will be stored. + path + The path under which the file will be stored. Returns ------- @@ -292,81 +231,17 @@ def upload_file_as_bytes_to_deep_reference(self, file_path: str, 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: str, cloud_path_list: list[str]): - """ - Uploads all files in specified directory as bytes to the Firebase Storage at specified cloud path in list order. - - Parameters - ---------- - directory_path - The local path of the directory in which files are stored. - cloud_path_list - 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: str): - """ - Retrieves data from the Firebase Storage for specified cloud file name. - - Parameters - ---------- - cloud_file_name - 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: str, cloud_file_name: str) -> Union[dict, Data]: - """ - Retrieves data from the Firebase Storage for specified cloud folder name and cloud file name. - - Parameters - ---------- - cloud_folder_name - The name of the cloud folder. - cloud_file_name - The name of the cloud file. - - Returns - ------- - Union[dict, Data] - 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: str) -> Union[dict, Data]: + def get_data(self, path: str) -> Union[dict, Data]: """ - 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_path_list - The list of reference names under which the file is stored. + path + The storage path. Returns ------- @@ -374,17 +249,17 @@ def get_data_from_deep_reference(self, cloud_path_list: str) -> Union[dict, Data 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: str, path_local: str, pretty: bool = True): + def download_data_to_json(self, path: str, path_local: str, pretty: bool = True) -> None: """ - 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 - The name of the cloud file. + path + The storage path. path_local The local path at which the JSON file will be stored. pretty @@ -395,7 +270,7 @@ def download_data_to_json(self, cloud_file_name: str, path_local: str, pretty: b 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)) 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"