diff --git a/.gitignore b/.gitignore index 971015c59e..eceadd6efb 100644 --- a/.gitignore +++ b/.gitignore @@ -112,7 +112,7 @@ install-pyenv-win.ps1 # Developer tools src/tools/dev_* .github_changelog_generator - +.run/custom_pre.run.xml # Addons ######## diff --git a/.run/custom.run.xml b/.run/custom.run.xml new file mode 100644 index 0000000000..32baacd8c0 --- /dev/null +++ b/.run/custom.run.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/src/poetry.lock b/src/poetry.lock index a60f208042..7582babf34 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -400,6 +400,28 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.13.1" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "beautifulsoup4-4.13.1-py3-none-any.whl", hash = "sha256:72465267014897bb10ca749bb632bde6c2d20f3254afd5458544bd74e6c2e6d8"}, + {file = "beautifulsoup4-4.13.1.tar.gz", hash = "sha256:741c8b6903a1e4ae8ba32b9c9ae7510dab7a197fdbadcf9fcdeb0891ef5ec66a"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "bidict" version = "0.23.1" @@ -1301,6 +1323,16 @@ requests = ">=2,<3" six = ">=1.13.0,<2" websocket-client = ">=0.40.0,<1" +[[package]] +name = "fusepy" +version = "3.0.1" +description = "Simple ctypes bindings for FUSE" +optional = false +python-versions = "*" +files = [ + {file = "fusepy-3.0.1.tar.gz", hash = "sha256:72ff783ec2f43de3ab394e3f7457605bf04c8cf288a2f4068b4cde141d4ee6bd"}, +] + [[package]] name = "future" version = "0.18.3" @@ -1473,6 +1505,44 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +name = "htmllistparse" +version = "0.6.1" +description = "Python parser for Apache/nginx-style HTML directory listing." +optional = false +python-versions = "*" +files = [ + {file = "htmllistparse-0.6.1-py3-none-any.whl", hash = "sha256:ed027107de47bf18c7059db156075267947a828d3d72ab02823fbef0f39481a9"}, + {file = "htmllistparse-0.6.1.tar.gz", hash = "sha256:6dc8a6bf03c843b9d325843a26a2351a795b573cd92a2c9b8271621019c64082"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +fusepy = "*" +html5lib = "*" +requests = "*" + [[package]] name = "httplib2" version = "0.22.0" @@ -3618,6 +3688,17 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "speedcopy" version = "2.1.5" @@ -4032,6 +4113,17 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + [[package]] name = "websocket-client" version = "0.59.0" @@ -4289,4 +4381,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = ">=3.9.13,<3.10" -content-hash = "7e3e5af5d7714940cff4d2c5d7445cead9e342b0403ef710b13f5a43aac67538" +content-hash = "d641dd9761c037ef96d60b332ca38ca88578fe055b02d0b3f42bcd8b4bbeabe8" diff --git a/src/pyproject.toml b/src/pyproject.toml index 18ebf97e31..b19c251476 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -84,6 +84,7 @@ opentimelineio = "^0.15.0" colorama = "0.4.6" fastapi = ">=0.112.0,<0.113.0" uvicorn = "^0.30.6" +htmllistparse = "^0.6.1" [tool.poetry.dev-dependencies] flake8 = "^6.0" diff --git a/src/quadpype/hosts/blender/api/__init__.py b/src/quadpype/hosts/blender/api/__init__.py index 70824fe326..e7b709687f 100644 --- a/src/quadpype/hosts/blender/api/__init__.py +++ b/src/quadpype/hosts/blender/api/__init__.py @@ -41,6 +41,13 @@ from .render_lib import prepare_rendering +from .template_resolving import ( + get_resolved_name, + get_entity_collection_template, + get_task_collection_template, + update_parent_data_with_entity_prefix +) + __all__ = [ "install", @@ -71,4 +78,10 @@ "capture", # "unique_name", "prepare_rendering", + + #Templates for working: + "get_resolved_name", + "get_entity_collection_template", + "get_task_collection_template", + "update_parent_data_with_entity_prefix" ] diff --git a/src/quadpype/hosts/blender/api/template_resolving.py b/src/quadpype/hosts/blender/api/template_resolving.py new file mode 100644 index 0000000000..6fcd84568e --- /dev/null +++ b/src/quadpype/hosts/blender/api/template_resolving.py @@ -0,0 +1,178 @@ +from collections import OrderedDict + +from quadpype.settings import get_project_settings +from quadpype.lib import ( + filter_profiles, + Logger, + StringTemplate, +) + +def get_resolved_name(data, template): + """Resolve template_collections_naming with entered data. + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + template (list): template to solve + Returns: + str: Resolved template + """ + template_obj = StringTemplate(template) + # Resolve the template + output = template_obj.format_strict(data) + return output.normalized() + +def _get_project_name_by_data(data): + """ + Retrieve the project name depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The project name (str) and a bool to specify if this is from anatomy or project (bool) + """ + project_name = None + is_from_anatomy = False + + if data.get("project"): + project_name = data["project"]["name"] + elif data.get("anatomyData"): + is_from_anatomy = True + project_name = data["anatomyData"]["project"]["name"] + + return project_name, is_from_anatomy + +def _get_app_name_by_data(data): + """ + Retrieve the app name depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The app name (str) and a bool to specify if this is from anatomy or project (bool) + """ + app_name = None + is_from_anatomy = False + + if data.get("app"): + app_name = data["project"]["app"] + elif data.get("anatomyData"): + is_from_anatomy = True + app_name = data["anatomyData"]["app"] + + return app_name, is_from_anatomy + +def _get_parent_by_data(data): + """ + Retrieve the parent asset name depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The parent name (str) and a bool to specify if this is from anatomy or project (bool) + """ + parent_name = None + is_from_anatomy = False + + if data.get("parent"): + parent_name = data["parent"] + elif data.get("anatomyData"): + is_from_anatomy = True + parent_name = data["anatomyData"]["parent"] + + return parent_name, is_from_anatomy + +def _get_profiles(setting_key, data, project_settings=None): + + project_name, is_anatomy_data = _get_project_name_by_data(data) + app_name, is_anatomy_data = _get_app_name_by_data(data) + + if not project_settings: + project_settings = get_project_settings(project_name) + + # Get Entity Type Name Matcher Profiles + try: + profiles = ( + project_settings + [app_name] + ["templated_workfile_build"] + [setting_key] + ["profiles"] + ) + + except Exception: + raise KeyError("Project has no profiles set for {}".format(setting_key)) + + # By default, profiles = [], so we must stop if there's no profiles set + if not profiles: + raise KeyError("Project has no profiles set for {}".format(setting_key)) + + return profiles + +def _get_entity_prefix(data): + """Retrieve the asset_type (entity_type) short name for proper blender naming + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A string corresponding to the short name for entered entity type + bool: bool to specify if this is from anatomy or project (bool) + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("entity_type_name_matcher", data) + parent, is_anatomy = _get_parent_by_data(data) + + profile_key = {"entity_types": parent} + profile = filter_profiles(profiles, profile_key) + # If a profile is found, return the prefix + return profile.get("entity_prefix"), is_anatomy + +def update_parent_data_with_entity_prefix(data): + """ + Will update the input data dict to change the value of the ["parent"] key + to become the corresponding prefix + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + """ + parent_prefix, is_anatomy = _get_entity_prefix(data) + + if not parent_prefix: + return + + if is_anatomy: + data["anatomyData"]["parent"] = parent_prefix + else: + data["parent"] = parent_prefix + +def get_entity_collection_template(data): + """Retrieve the template for the collection depending on the entity type + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A template that can be solved later + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("collections_templates_by_entity_type", data) + parent, is_anatomy = _get_parent_by_data(data) + profile_key = {"entity_types": parent} + profile = filter_profiles(profiles, profile_key) + # If a profile is found, return the template + return profile.get("template") + + +def get_task_collection_template(data): + """Retrieve the template for the collection depending on the task type + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A template that can be solved later + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("working_collections_templates_by_tasks", data) + profile_key = {"task_types": data["task"]} + profile = filter_profiles(profiles, profile_key) + + if not profile: + return None + # If a profile is found, return the template + if data.get("variant", None) == "Main": + return profile["main_template"] + + return profile["variant_template"] diff --git a/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py b/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py new file mode 100644 index 0000000000..89c06fbf27 --- /dev/null +++ b/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py @@ -0,0 +1,91 @@ +import inspect +import bpy + +from quadpype.hosts.blender.api import plugin, action + +from quadpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError, + RepairAction +) + +from quadpype.hosts.blender.api import ( + get_resolved_name, + get_task_collection_template +) + +class ValidateModelContents(plugin.BlenderInstancePlugin): + """Validates Model instance contents. + + A Model instance should have everything that is in the {parent}-{asset} collection + """ + + order = ValidateContentsOrder + families = ['model'] + hosts = ['blender'] + label = 'Validate Model Contents' + + actions = [action.SelectInvalidAction, RepairAction] + + @staticmethod + def get_invalid(instance): + # Get collection template from task/variant + template = get_task_collection_template(instance.data) + coll_name = get_resolved_name(instance.data, template) + + # Get collection + asset_model_coll = bpy.data.collections.get(coll_name) + if not asset_model_coll: + raise RuntimeError("No collection found with name :" + "{}".format(asset_model_coll)) + + objects = [obj for obj in instance] + asset_model_list = asset_model_coll.objects + + # Get objects in instance + objects = [obj for obj in instance] + + # Compare obj in instance and obj in scene model collection + invalid = [missing_obj for missing_obj in asset_model_list if missing_obj not in objects] + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + names = ", ".join(obj.name for obj in invalid) + raise PublishValidationError( + "Objects found in collection which are not" + f" in instance: {names}", + description=self.get_description() + ) + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + if not invalid: + return + + instance_object = instance.data.get("transientData").get("instance_node") + if not instance_object: + raise RuntimeError ("No instance object found for {}".format(instance.name)) + + for object in invalid: + object.parent = instance_object + + def get_description(self): + return inspect.cleandoc( + """## Some objects are missing in the publish instance. + + Based on the variant name, it appears that all the objects + in the model collection are not present in the + publish instance. + + You can either: + - Select them with the Select Invalid button + - Auto Repair and put them in the corresponding publish instance + """ + ) diff --git a/src/quadpype/lib/version.py b/src/quadpype/lib/version.py index ab810723f5..dbd82f9c02 100644 --- a/src/quadpype/lib/version.py +++ b/src/quadpype/lib/version.py @@ -4,13 +4,16 @@ import shutil import hashlib import platform +import tempfile + +import semver +import requests +from htmllistparse import fetch_listing from pathlib import Path from zipfile import ZipFile, BadZipFile from typing import Union, List, Tuple, Any, Optional, Dict -import semver - ADDONS_SETTINGS_KEY = "addons" _NOT_SET = object() @@ -21,16 +24,15 @@ _PACKAGE_MANAGER = None -class PackageVersion(semver.VersionInfo): - """Class for storing information about version. +class SourceURL(str): + """Simple subclass of the str class to detect type with ease""" - Attributes: - path (str): path - """ +class PackageVersion(semver.VersionInfo): + """Class for storing information about a version.""" def __init__(self, *args, **kwargs): - """Create version. + """Create a version instance. Args: major (int): version when you make incompatible API changes. @@ -41,10 +43,12 @@ def __init__(self, *args, **kwargs): build (str): an optional build string version (str): if set, it will be parsed and will override parameters like `major`, `minor` and so on. - path (Path): path to version location. + location (Path, SourceURL): location to the version. """ - self.path = None + + self.location = None + self.download_required = False self.is_archive = False if "version" in kwargs: @@ -58,36 +62,48 @@ def __init__(self, *args, **kwargs): kwargs["prerelease"] = v.prerelease kwargs["build"] = v.build - if "path" in kwargs: - path_value = kwargs.pop("path") - if isinstance(path_value, str): - path_value = Path(path_value) - self.path = path_value + if "location" in kwargs: + location_value = kwargs.pop("location") + if isinstance(location_value, (SourceURL, Path)): + self.set_location(location_value) + else: + raise ValueError("Invalid location type, can only be Path or SourceURL") if args or kwargs: super().__init__(*args, **kwargs) def __repr__(self): - return f"<{self.__class__.__name__}: {str(self)} - path={self.path}>" + return f"<{self.__class__.__name__}: {str(self)} - path={str(self.location)}>" def __lt__(self, other): result = super().__lt__(other) - # prefer path over no path - if self == other and not self.path and other.path: - return True - - if self == other and self.path and other.path and \ - other.path.is_dir() and self.path.is_file(): + # prefer the one with a specified location over no location + if self == other and not self.location and other.location: return True - if self.finalize_version() == other.finalize_version() and \ - self.prerelease == other.prerelease: - return True + if self == other and self.location and other.location: + if isinstance(other.location, Path) and isinstance(self.location, SourceURL): + return True + elif isinstance(other.location, Path) and isinstance(other.location, Path) \ + and other.location.is_dir() and self.location.is_file(): + return True return result def __hash__(self): - return hash(self.path) if self.path else hash(str(self)) + return hash(str(self.location)) if self.location else hash(str(self)) + + def set_location(self, location): + self.location = location + self.is_archive = False + self.download_required = False + + if self.location: + if str(self.location).endswith(".zip"): + self.is_archive = True + + if isinstance(self.location, SourceURL): + self.download_required = True def compare_major_minor_patch(self, other) -> bool: return self.finalize_version() == other.finalize_version() @@ -109,7 +125,7 @@ def is_compatible(self, version): class PackageVersionExists(Exception): - """Exception for handling existing package version.""" + """Exception for handling a not existing package version.""" pass @@ -157,7 +173,7 @@ def sanitize_long_path(path): def sha256sum(filename): - """Calculate sha256 for content of the file. + """Calculate sha256 for the content of the file. Args: filename (str): Path to file. @@ -189,7 +205,7 @@ class PackageHandler: def __init__(self, pkg_name: str, local_dir_path: Union[str, Path, None], - remote_dir_paths: List[Union[str, Path]], + remote_sources: List[Union[str, Path]], running_version_str: str, retrieve_locally: bool = True, install_dir_path: Union[str, Path, None] = None): @@ -199,14 +215,8 @@ def __init__(self, if isinstance(local_dir_path, str): local_dir_path = Path(local_dir_path) - if isinstance(remote_dir_paths, str): - remote_dir_paths = [Path(remote_dir_paths)] - - # Ensure paths are Path objects - remote_dir_paths = [Path(curr_path) for curr_path in remote_dir_paths] - if retrieve_locally and not local_dir_path: - raise ValueError("local_dir_path cannot be None if retrieve_locally = True") + raise ValueError("local_dir_path cannot be None if retrieve_locally is set to True") self._local_dir_path = local_dir_path @@ -216,9 +226,9 @@ def __init__(self, except Exception: # noqa raise RuntimeError(f"Local directory path for package \"{pkg_name}\" is not accessible.") - self._remote_dir_paths = remote_dir_paths - # remote_dir_paths can be None in case the path to package version isn't specified - # This can happen only for the QuadPype app package + if not remote_sources: + remote_sources = [self._local_dir_path] + self._remote_sources = self._conform_remote_sources(pkg_name, remote_sources) self._running_version = None @@ -233,7 +243,7 @@ def __init__(self, self._name, self._install_dir_path ), - path=self._install_dir_path + location=self._install_dir_path ) if not running_version_str: @@ -267,10 +277,11 @@ def __init__(self, running_version = self.find_version(running_version_str) if not running_version: if self._install_dir_path: - self.get_package_version_from_dir(self._name, self._install_dir_path) - raise ValueError(f"Specified version \"{running_version_str}\" is not available locally and on the remote path directory.") + running_version_str = self.get_package_version_from_dir(self._name, self._install_dir_path) + raise ValueError( + f"Specified version \"{running_version_str}\" is not available locally and on the remote path directory.") - if retrieve_locally: + if retrieve_locally or running_version.download_required: self._running_version = self.retrieve_version_locally(running_version_str) else: # We are about to use a remote version @@ -307,30 +318,70 @@ def is_local_dir_path_accessible(self) -> bool: return self._local_dir_path and isinstance(self._local_dir_path, Path) and self._local_dir_path.exists() @property - def remote_dir_paths(self): - return self._remote_dir_paths + def remote_sources(self): + return self._remote_sources - def change_remote_dir_paths(self, remote_dir_paths: Union[List[Union[str, Path]], None]): - """Set the remote directory path.""" - if isinstance(remote_dir_paths, str): - remote_dir_paths = [Path(remote_dir_paths)] - elif not remote_dir_paths: - # If the remote_dir-path is unset we use the local_dir_path - remote_dir_paths = [self._local_dir_path] + @staticmethod + def conform_remote_source(remote_source): + if isinstance(remote_source, str): + if remote_source.startswith("http"): + # Yes, there is no complex url validator + conform_source = SourceURL(remote_source) + else: + conform_source = Path(remote_source) + elif isinstance(remote_source, Path): + conform_source = remote_source + else: + raise ValueError(f"All the remote source path must be a string or a Path object.") + + return conform_source - # Ensure paths are Path objects - remote_dir_paths = [Path(curr_path) for curr_path in remote_dir_paths] + @staticmethod + def _conform_remote_sources(pkg_name, remote_sources): + conformed_remote_sources = [] + + if not remote_sources: + # remote_sources_paths can be None in case the path to package version isn't specified + # This can happen only for the QuadPype app package + if pkg_name.lower() != "quadpype": + raise ValueError("remote_sources cannot be empty.") + return conformed_remote_sources + + if isinstance(remote_sources, (str, Path)): + # A single string source has been passed + remote_sources = [remote_sources] + + for remote_source in remote_sources: + conformed_remote_sources.append( + PackageHandler.conform_remote_source(remote_source) + ) - self._remote_dir_paths = remote_dir_paths + return conformed_remote_sources - def get_accessible_remote_dir_path(self): - """Get the first accessible remote directory path (if any).""" - if not self._remote_dir_paths: + def change_remote_sources(self, remote_sources: Union[List[Union[str, Path]], None]): + """Set the remote directory path.""" + if not remote_sources: + remote_sources = [self._local_dir_path] + self._remote_sources = self._conform_remote_sources(self._name, remote_sources) + + def get_accessible_remote_source(self): + """Get the first accessible remote source path (if any).""" + if not self._remote_sources: return None - for remote_dir_path in self._remote_dir_paths: - if remote_dir_path and isinstance(remote_dir_path, Path) and remote_dir_path.exists(): - return remote_dir_path + for remote_source in self._remote_sources: + if not remote_source: + continue + + if isinstance(remote_source, Path) and remote_source.exists(): + return remote_source + elif isinstance(remote_source, SourceURL): + try: + response = requests.head(remote_source) + if response.ok: + return remote_source + except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError): + continue return None @@ -339,7 +390,7 @@ def running_version(self): return self._running_version @classmethod - def validate_version_str(cls, version_str:str) -> Union[str, None]: + def validate_version_str(cls, version_str: str) -> Union[str, None]: # Strip the .zip extension (if present) input_string = re.sub(r"\.zip$", "", version_str, flags=re.IGNORECASE) @@ -392,7 +443,7 @@ def get_package_version_from_dir(cls, pkg_name: str, dir_path: Union[str, Path]) return version['__version__'] @classmethod - def compare_version_with_package_dir(cls, pkg_name:str, dir_path: Path, version_obj) -> Tuple[bool, str]: + def compare_version_with_package_dir(cls, pkg_name: str, dir_path: Path, version_obj) -> Tuple[bool, str]: if not dir_path or not isinstance(dir_path, Path) or not dir_path.exists() or not dir_path.is_dir(): raise ValueError("Invalid directory path") @@ -408,8 +459,8 @@ def compare_version_with_package_dir(cls, pkg_name:str, dir_path: Path, version_ "doesn't match. Skipping.") return True, "Versions match" - @ classmethod - def compare_version_with_package_zip(cls, pkg_name:str, zip_path: Path, version_obj) -> Tuple[bool, str]: + @classmethod + def compare_version_with_package_zip(cls, pkg_name: str, zip_path: Path, version_obj) -> Tuple[bool, str]: if not zip_path or not isinstance(zip_path, Path) or not zip_path.exists() or not zip_path.is_file(): raise ValueError("Invalid ZIP file path.") @@ -473,14 +524,16 @@ def get_available_versions(self, from_local: bool = None, from_remote: bool = No return sorted(list(versions.values())) @classmethod - def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archives = False, excluded_str_versions: Optional[List[str]] = None, parent_version: Optional[PackageVersion] = None) -> List: + def get_versions_from_dir(cls, pkg_name: str, source_path: Path, priority_to_archives=False, + excluded_str_versions: Optional[List[str]] = None, + parent_version: Optional[PackageVersion] = None) -> List: """Get all detected PackageVersions in directory. Args: - pkg_name (str): Name of the package. - dir_path (Path): Directory to scan. + pkg_name (str): Name of the package. + source_path (Path): Directory to scan. priority_to_archives (bool, optional): If True, look for archives in priority. - (if only a dir exists it will still be added). + (if only a dir exists, it will still be added). excluded_str_versions (List[str]): List of excluded versions as strings. parent_version (PackageVersion): Parent version to use for nested directories. @@ -488,7 +541,7 @@ def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archiv List[PackageVersion]: List of detected PackageVersions. Throws: - ValueError: if invalid path is specified. + ValueError: if an invalid path is specified. """ if excluded_str_versions is None: @@ -497,11 +550,11 @@ def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archiv versions = [] # Ensure the directory exists and is valid - if not dir_path or not dir_path.exists() or not dir_path.is_dir(): + if not source_path or not source_path.exists() or not source_path.is_dir(): return versions # Iterate over directory at the first level - for item in dir_path.iterdir(): + for item in source_path.iterdir(): # If the item is a directory with a major.minor version format, dive deeper if item.is_dir() and re.match(r"^v?\d+\.\d+$", item.name) and parent_version is None: parent_version_str = f"{item.name.removeprefix('v')}.0" @@ -516,11 +569,14 @@ def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archiv if detected_versions: versions.extend(detected_versions) + continue + # If it's a file, process its name (stripped of extension) name = item.name if item.is_dir() else item.stem version = cls.get_version_from_str(name) - if not version or (parent_version and (version.major != parent_version.major or version.minor != parent_version.minor)): + if not version or (parent_version and ( + version.major != parent_version.major or version.minor != parent_version.minor)): continue # If it's a directory, check if version is valid within it @@ -531,8 +587,7 @@ def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archiv if item.is_file() and not cls.compare_version_with_package_zip(pkg_name, item, version)[0]: continue - version.path = item.resolve() - version.is_archive = item.is_file() + version.set_location(item.resolve()) if str(version) not in excluded_str_versions: versions.append(version) @@ -552,16 +607,125 @@ def get_versions_from_dir(cls, pkg_name: str, dir_path: Path, priority_to_archiv return list(sorted(versions_correlation.values())) @classmethod - def get_versions_from_dirs(cls, pkg_name: str, dir_paths: List[Path], priority_to_archives = False, excluded_str_versions: Optional[List[str]] = None) -> List: + def get_versions_from_url(cls, pkg_name: str, source_url: SourceURL, priority_to_archives=False, + excluded_str_versions: Optional[List[str]] = None, + parent_version: Optional[PackageVersion] = None) -> List: + """Get all detected PackageVersions in directory. + + Args: + pkg_name (str): Name of the package. + source_url (SourceURL): Web page to scan. + priority_to_archives (bool, optional): If True, look for archives in priority. + (if only a dir exists, it will still be added). + excluded_str_versions (List[str]): List of excluded versions as strings. + parent_version (PackageVersion): Parent version to use for nested directories. + + Returns: + List[PackageVersion]: List of detected PackageVersions. + + Throws: + ValueError: if an invalid path is specified. + + """ + if excluded_str_versions is None: + excluded_str_versions = [] + + versions = [] + + # Ensure the web location is valid + if not source_url: + return versions + + response = requests.head(source_url) + page_content_type = "text/html" + allowed_content_type = [ + "application/zip", + "application/x-zip-compressed", + "application/octet-stream" + ] + if not response.status_code != 200 or not response.headers.get("content-type") == page_content_type: + return versions + + # Parse the HTML content to extract links using htmllistparse + cwd: str + cwd, listing = fetch_listing(source_url) + if not cwd.endswith("/"): + cwd = cwd + "/" + + # Iterate over webpage at the first level + for item in listing: + item_full_url = SourceURL(f"{cwd}{item.name}") + + # If the item is a directory with a major.minor version format, dive deeper + if item.name.endswith("/") and re.match(r"^v?\d+\.\d+/$", item.name) and parent_version is None: + parent_version_str = f"{item.name.removeprefix('v')[:-1]}.0" + detected_versions = cls.get_versions_from_url( + pkg_name, + item_full_url, + priority_to_archives, + excluded_str_versions, + PackageVersion(version=parent_version_str) + ) + + if detected_versions: + versions.extend(detected_versions) + + continue + + response = requests.head(item_full_url) + if not response.status_code != 200 or not response.headers.get("content-type") == allowed_content_type: + continue + + # If it's a file, process its name (stripped of extension) + name = Path(item.name).stem + version = cls.get_version_from_str(name) + + if not version or (parent_version and ( + version.major != parent_version.major or version.minor != parent_version.minor)): + continue + + version.set_location(item_full_url) + if str(version) not in excluded_str_versions: + versions.append(version) + + # Correlation dict (key is version str, value is version obj) + versions_correlation = {} + + # Loop to get in priority what was requested (archives or dir) + for curr_version in versions: + if str(curr_version) not in versions_correlation: + versions_correlation[str(curr_version)] = curr_version + + return list(sorted(versions_correlation.values())) + + @classmethod + def get_versions_from_sources(cls, pkg_name: str, source_locations: List[Union[SourceURL, Path, str]], + priority_to_archives=False, + excluded_str_versions: Optional[List[str]] = None) -> List: versions_set = set() - for dir_path in dir_paths: - if not dir_path: + dir_source_locations = [] + web_source_locations = [] + + for source_location in source_locations: + if not source_location: continue - if isinstance(dir_path, str): - dir_path = Path(dir_path) + if isinstance(source_location, str): + source_location = PackageHandler.conform_remote_source(source_location) + + if isinstance(source_location, SourceURL): + web_source_locations.append(source_location) + else: + dir_source_locations.append(source_location) + + for dir_source_location in dir_source_locations: + found_versions = cls.get_versions_from_dir(pkg_name, dir_source_location, priority_to_archives, + excluded_str_versions) + versions_set.update(found_versions) - found_versions = cls.get_versions_from_dir(pkg_name, dir_path, priority_to_archives, excluded_str_versions) + for web_source_location in web_source_locations: + found_versions = cls.get_versions_from_url(pkg_name, web_source_location, priority_to_archives, + excluded_str_versions) versions_set.update(found_versions) return sorted(versions_set) @@ -619,14 +783,15 @@ def get_remote_versions(self, excluded_str_versions: Optional[List[str]] = None) # If the goal is to retrieve the code, we want archives priority_to_archives = self.retrieve_locally - return self.get_versions_from_dirs( + return self.get_versions_from_sources( self._name, - self._remote_dir_paths, + self._remote_sources, priority_to_archives=priority_to_archives, excluded_str_versions=excluded_str_versions ) - def find_version(self, version: Union[PackageVersion, str], from_local: bool = False) -> Union[PackageVersion, None]: + def find_version(self, version: Union[PackageVersion, str], from_local: bool = False) -> Union[ + PackageVersion, None]: """Get a specific version from the local or remote dir if available.""" if isinstance(version, str): version = PackageVersion(version=version) @@ -639,6 +804,17 @@ def find_version(self, version: Union[PackageVersion, str], from_local: bool = F return None + @staticmethod + def _download_version(remote_version: PackageVersion, dest_archive_path: Path): + response = requests.get(remote_version.location, stream=True) + + if response.status_code == 200: + with open(dest_archive_path, "wb") as file: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + else: + raise Exception(f"Failed to download {remote_version.location}. HTTP status code: {response.status_code}") + def retrieve_version_locally(self, version: Union[str, PackageVersion, None] = None): """Retrieve the version specified available from remote.""" if isinstance(version, str): @@ -664,17 +840,26 @@ def retrieve_version_locally(self, version: Union[str, PackageVersion, None] = N destination_path = destination_dir.joinpath(str(remote_version)) destination_path.mkdir(parents=True, exist_ok=True) - if remote_version.path.suffix == ".zip": - # Copy locally first - shutil.copy2(remote_version.path, destination_dir, follow_symlinks=True) + if remote_version.download_required: + archive_temp_path = Path(tempfile.gettempdir()).joinpath(f"{str(remote_version)}.zip") + self._download_version(remote_version, archive_temp_path) + remote_version.set_location(archive_temp_path) + + if remote_version.is_archive: + archive_local_path = destination_dir.joinpath(remote_version.location.name) + if remote_version.location != archive_local_path: + # Copy locally first (delete existing archive if present) + if archive_local_path.exists(): + archive_local_path.unlink() + shutil.copy2(remote_version.location, destination_dir, follow_symlinks=True) # Unzip the local copy - with ZipFile(destination_dir.joinpath(remote_version.path.name), 'r') as zip_ref: + with ZipFile(archive_local_path, 'r') as zip_ref: zip_ref.extractall(destination_path) else: - shutil.copytree(remote_version.path, destination_path, dirs_exist_ok=True) + shutil.copytree(remote_version.location, destination_path, dirs_exist_ok=True) - return PackageVersion(version=str(version), path=destination_path) + return PackageVersion(version=str(version), location=destination_path) def validate_checksums(self, base_version_path: Union[str, None] = None) -> tuple: """Validate checksums in a given path. @@ -684,7 +869,7 @@ def validate_checksums(self, base_version_path: Union[str, None] = None) -> tupl and str in a tuple. """ - dir_path = self._running_version.path + dir_path = self._running_version.location if base_version_path: dir_path = base_version_path @@ -703,7 +888,7 @@ def validate_checksums(self, base_version_path: Union[str, None] = None) -> tupl for line in checksums_data.split("\n") if line ] - # compare content of the package / folder against list of files from checksum file. + # Compare the content of the package / folder against the list of files from the checksum file. # If difference exists, something is wrong and we invalidate directly package_path = dir_path.joinpath(self._name) files_in_dir = set( @@ -736,11 +921,12 @@ def validate_checksums(self, base_version_path: Union[str, None] = None) -> tupl return True, "All ok" def _add_package_path_to_env(self): - """Add package path to environment.""" - if not self._running_version.path: - raise ValueError("Installation dir path not specified in running_version. Please call first retrieve_version_locally.") + """Add the package path to the environment.""" + if not self._running_version.location: + raise ValueError( + "Installation dir path not specified in running_version. Please call first retrieve_version_locally.") - version_path = self._running_version.path.resolve().as_posix() + version_path = self._running_version.location.resolve().as_posix() sys.path.insert(0, version_path) @staticmethod @@ -748,12 +934,15 @@ def ensure_version_is_dir(version_obj): if not version_obj.is_archive: return version_obj + if version_obj.download_required: + raise RuntimeError("Cannot unzip package version that is stored on a web server.") + # Unzip - destination_path = version_obj.path.parent.joinpath(str(version_obj)) - with ZipFile(version_obj.path, 'r') as zip_ref: + destination_path = version_obj.location.parent.joinpath(str(version_obj)) + with ZipFile(version_obj.location, 'r') as zip_ref: zip_ref.extractall(destination_path) - version_obj.path = destination_path + version_obj.set_location(destination_path) return version_obj @@ -811,7 +1000,7 @@ def get_package(package_name: str) -> PackageHandler: return _PACKAGE_MANAGER[package_name] -def get_packages(package_type: Union[str, None]=None) -> List[PackageHandler]: +def get_packages(package_type: Union[str, None] = None) -> List[PackageHandler]: global _PACKAGE_MANAGER if _PACKAGE_MANAGER is None: raise RuntimeError("Package Manager is not initialized") diff --git a/src/quadpype/lib/version_utils.py b/src/quadpype/lib/version_utils.py index 8ca6b1f62f..629634bff5 100644 --- a/src/quadpype/lib/version_utils.py +++ b/src/quadpype/lib/version_utils.py @@ -131,9 +131,24 @@ def get_available_versions(*args, **kwargs): return get_package("quadpype").get_available_versions(*args, **kwargs) -def is_remote_versions_dir_accessible(): - """QuadPype version repository path can be accessed.""" - return os.getenv("QUADPYPE_PATH") and Path(os.getenv("QUADPYPE_PATH")).exists() +def is_remote_versions_location_accessible(): + """Is QuadPype version repository location accessible?""" + repo_location = os.getenv("QUADPYPE_PATH") + if not repo_location: + return False + + if repo_location.startswith("http"): + import requests + try: + response = requests.head(repo_location) + if response.ok: + return True + except Exception: # noqa + return False + elif Path(os.getenv("QUADPYPE_PATH")).exists(): + return True + + return False def get_local_versions(): @@ -180,7 +195,7 @@ def is_current_version_studio_latest(): # control or path to folder with zip files is not accessible if ( is_running_locally() - or not is_remote_versions_dir_accessible() + or not is_remote_versions_location_accessible() ): return output @@ -206,7 +221,7 @@ def is_current_version_higher_than_expected(): # control or path to folder with zip files is not accessible if ( is_running_locally() - or not is_remote_versions_dir_accessible() + or not is_remote_versions_location_accessible() ): return output diff --git a/src/quadpype/modules/base.py b/src/quadpype/modules/base.py index 3870008378..e960eccece 100644 --- a/src/quadpype/modules/base.py +++ b/src/quadpype/modules/base.py @@ -231,13 +231,13 @@ def get_dynamic_modules_dirs(): version_key = "staging_version" if is_staging_enabled() else "version" - remote_dir_paths = addon_setting.get("package_remote_dirs", {}).get(platform.system().lower(), []) - remote_dir_paths = [Path(curr_path_str) for curr_path_str in remote_dir_paths] + remote_sources = addon_setting.get("package_remote_sources", {}).get(platform.system().lower(), []) + remote_sources = [Path(curr_source_str) for curr_source_str in remote_sources] addon_package = AddOnHandler( pkg_name=addon_setting.get("package_name"), local_dir_path=addon_local_dir, - remote_dir_paths=remote_dir_paths, + remote_sources=remote_sources, running_version_str=addon_setting.get(version_key, ""), retrieve_locally=addon_setting.get("retrieve_locally", False), ) @@ -246,10 +246,11 @@ def get_dynamic_modules_dirs(): # Now retrieve the add-ons paths dynamic_modules_dir_paths = [] for package in get_packages("add_on"): - dynamic_modules_dir_paths.append(package.running_version.path) + dynamic_modules_dir_paths.append(package.running_version.location) return dynamic_modules_dir_paths + def get_module_dirs(): """List of paths where QuadPype modules can be found.""" _dir_paths = [] diff --git a/src/quadpype/pipeline/anatomy.py b/src/quadpype/pipeline/anatomy.py index 654d785fdb..18f5ff6513 100644 --- a/src/quadpype/pipeline/anatomy.py +++ b/src/quadpype/pipeline/anatomy.py @@ -447,7 +447,7 @@ def get_sync_server_addon(cls): if cls._sync_server_addon_cache.is_outdated: manager = ModulesManager() cls._sync_server_addon_cache.update_data( - manager.get_enabled_module("sitesync") + manager.get_enabled_module("sync_server") ) return cls._sync_server_addon_cache.data diff --git a/src/quadpype/settings/constants.py b/src/quadpype/settings/constants.py index 3ab0e8aea2..fa8ed8e7f9 100644 --- a/src/quadpype/settings/constants.py +++ b/src/quadpype/settings/constants.py @@ -50,7 +50,7 @@ CORE_KEYS = { - "remote_versions_dirs", + "remote_sources", "local_versions_dir", "log_to_server", "disk_mapping", diff --git a/src/quadpype/settings/defaults/global_settings/core.json b/src/quadpype/settings/defaults/global_settings/core.json index a6b0e59369..6fde50f375 100644 --- a/src/quadpype/settings/defaults/global_settings/core.json +++ b/src/quadpype/settings/defaults/global_settings/core.json @@ -14,7 +14,7 @@ "protect_anatomy_attributes": false }, "local_env_white_list": [], - "remote_versions_dirs": { + "remote_sources": { "windows": [], "darwin": [], "linux": [] diff --git a/src/quadpype/settings/defaults/project_settings/blender.json b/src/quadpype/settings/defaults/project_settings/blender.json index d7f1abdb06..b4c8f02a51 100644 --- a/src/quadpype/settings/defaults/project_settings/blender.json +++ b/src/quadpype/settings/defaults/project_settings/blender.json @@ -35,6 +35,18 @@ "create_first_version": false, "custom_templates": [] }, + "templated_workfile_build": { + "profiles": [], + "entity_type_name_matcher":{ + "profiles": [] + }, + "collections_templates_by_entity_type": { + "profiles": [] + }, + "working_collections_templates_by_tasks": { + "profiles": [] + } + }, "publish": { "ValidateCameraZeroKeyframe": { "enabled": true, diff --git a/src/quadpype/settings/entities/schemas/global_schema/schema_addons.json b/src/quadpype/settings/entities/schemas/global_schema/schema_addons.json index c7f33fe7c9..1ceb508ea0 100644 --- a/src/quadpype/settings/entities/schemas/global_schema/schema_addons.json +++ b/src/quadpype/settings/entities/schemas/global_schema/schema_addons.json @@ -12,7 +12,7 @@ "require_restart": true, "object_type": { "type": "dict", - "required_keys": ["package_name", "package_remote_dirs"], + "required_keys": ["package_name", "package_remote_sources"], "children": [ { "type": "text", @@ -25,7 +25,7 @@ }, { "type": "path", - "key": "package_remote_dirs", + "key": "package_remote_sources", "label": "Versions Repository", "multiplatform": true, "multipath": true diff --git a/src/quadpype/settings/entities/schemas/global_schema/schema_core.json b/src/quadpype/settings/entities/schemas/global_schema/schema_core.json index 540c478c89..ed6b6d6adc 100644 --- a/src/quadpype/settings/entities/schemas/global_schema/schema_core.json +++ b/src/quadpype/settings/entities/schemas/global_schema/schema_core.json @@ -176,7 +176,7 @@ }, { "type": "path", - "key": "remote_versions_dirs", + "key": "remote_sources", "label": "Versions Repository", "multiplatform": true, "multipath": true, diff --git a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json index 58a271edb6..dd9660e57c 100644 --- a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json +++ b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json @@ -234,6 +234,149 @@ "workfile_builder/builder_on_start", "workfile_builder/profiles" ] + }, + { + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "path", + "label": "Path to template", + "type": "path", + "multiplatform": false, + "multipath": false + }, + { + "key": "keep_placeholder", + "label": "Keep placeholders", + "type": "boolean", + "default": true + }, + { + "key": "create_first_version", + "label": "Create first version", + "type": "boolean", + "default": true + }, + { + "key": "autobuild_first_version", + "label": "Autobuild first version", + "type": "boolean", + "default": true + } + ] + } + }, + { + "type": "dict", + "collapsible": true, + "key": "entity_type_name_matcher", + "label": "Entity Type Name Matcher", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key":"entity_types", + "label":"Entity Types" + }, + { + "type": "text", + "key": "entity_prefix", + "label": "Entity Prefix" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "collections_templates_by_entity_type", + "label": "Collections Templates By Entity Type", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key":"entity_types", + "label":"Entity Types" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "working_collections_templates_by_tasks", + "label": "Working Collections Templates By Tasks", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "type": "text", + "key": "main_template", + "label": "Main Template" + }, + { + "type": "text", + "key": "variant_template", + "label": "Variant Template" + } + ] + } + } + ] + } + ] }, { "type": "schema", diff --git a/src/quadpype/settings/handlers.py b/src/quadpype/settings/handlers.py index da61ab12d2..321b77db26 100644 --- a/src/quadpype/settings/handlers.py +++ b/src/quadpype/settings/handlers.py @@ -926,7 +926,7 @@ def _apply_core_settings_changes_to_package_instance(self, new_core_settings): # Keys to monitor keys_to_check = [ - ("remote_versions_dirs", package.change_remote_dir_paths), + ("remote_sources", package.change_remote_sources), ("local_versions_dir", package.change_local_dir_path) ] diff --git a/src/quadpype/settings/lib.py b/src/quadpype/settings/lib.py index 5016582121..ec5ee42510 100644 --- a/src/quadpype/settings/lib.py +++ b/src/quadpype/settings/lib.py @@ -1541,7 +1541,7 @@ def get_quadpype_local_dir_path(settings: dict) -> Union[Path, None]: return path if path else Path(user_data_dir("quadpype", "quad")) -def get_quadpype_remote_dir_paths(settings: dict) -> List[str]: +def get_quadpype_remote_sources(settings: dict) -> List[str]: """Get QuadPype path from global settings. Args: @@ -1552,7 +1552,7 @@ def get_quadpype_remote_dir_paths(settings: dict) -> List[str]: """ paths = ( settings - .get("remote_versions_dirs", {}) + .get("remote_sources", {}) .get(platform.system().lower()) ) or [] # For cases when it's a single path diff --git a/src/quadpype/tools/settings/settings/item_widgets.py b/src/quadpype/tools/settings/settings/item_widgets.py index edbb5bc5bd..f9a78d5bc8 100644 --- a/src/quadpype/tools/settings/settings/item_widgets.py +++ b/src/quadpype/tools/settings/settings/item_widgets.py @@ -577,12 +577,12 @@ def _update_value_hints(self): versions = package.get_available_versions() else: # Could not find a loaded package with this name yet - # Try to get the versions directly from the remote dir paths - remote_dirs_entity = self.entity_widget.entity.non_gui_children.get("package_remote_dirs") - if remote_dirs_entity: - remote_dir_paths = remote_dirs_entity.value.get(platform.system().lower()) - if remote_dir_paths: - versions = PackageHandler.get_versions_from_dirs(package_name, remote_dir_paths) + # Try to get the versions directly from the remote source paths + remote_sources_entity = self.entity_widget.entity.non_gui_children.get("package_remote_sources") + if remote_sources_entity: + remote_sources = remote_sources_entity.value.get(platform.system().lower()) + if remote_sources: + versions = PackageHandler.get_versions_from_sources(package_name, remote_sources) self.entity.value_hints = [str(version) for version in versions] if self.entity.value_hints: diff --git a/src/quadpype/version.py b/src/quadpype/version.py index 0644a5aff7..5870ba2c1c 100644 --- a/src/quadpype/version.py +++ b/src/quadpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """File declaring QuadPype version.""" -__version__ = "4.0.23" +__version__ = "4.0.24" diff --git a/src/start.py b/src/start.py index e688abce81..bf5dff963b 100644 --- a/src/start.py +++ b/src/start.py @@ -692,13 +692,13 @@ def _determine_mongodb() -> str: def _initialize_environment(quadpype_version: PackageVersion) -> None: - version_path = quadpype_version.path - if not version_path: + version_location = quadpype_version.location + if not version_location: _print(f"!!! Version {quadpype_version} doesn't have path set.") raise ValueError("No path set in specified QuadPype version.") os.environ["QUADPYPE_VERSION"] = str(quadpype_version) # set QUADPYPE_REPOS_ROOT to point to currently used QuadPype version. - quadpype_root = os.path.normpath(version_path.as_posix()) + quadpype_root = os.path.normpath(version_location.as_posix()) os.environ["QUADPYPE_REPOS_ROOT"] = quadpype_root split_paths = os.getenv("PYTHONPATH", "").split(os.pathsep) @@ -756,7 +756,7 @@ def _initialize_package_manager(database_url, version_str): from quadpype.settings.lib import ( get_core_settings_no_handler, get_quadpype_local_dir_path, - get_quadpype_remote_dir_paths + get_quadpype_remote_sources ) core_settings = get_core_settings_no_handler(database_url) @@ -766,7 +766,7 @@ def _initialize_package_manager(database_url, version_str): quadpype_package = PackageHandler( pkg_name="quadpype", local_dir_path=get_quadpype_local_dir_path(core_settings), - remote_dir_paths=get_quadpype_remote_dir_paths(core_settings), + remote_sources=get_quadpype_remote_sources(core_settings), running_version_str=version_str, retrieve_locally=True, install_dir_path=os.getenv("QUADPYPE_ROOT") @@ -880,11 +880,11 @@ def boot(): valid = package_manager["quadpype"].validate_checksums(QUADPYPE_ROOT)[0] sys.exit(0 if valid else 1) - if not package_manager["quadpype"].remote_dir_paths: + if not package_manager["quadpype"].remote_sources: _print("*** Cannot get QuadPype patches directory path from database.") - if not os.getenv("QUADPYPE_PATH") and package_manager["quadpype"].remote_dir_paths: - os.environ["QUADPYPE_PATH"] = str(package_manager["quadpype"].get_accessible_remote_dir_path()) + if not os.getenv("QUADPYPE_PATH") and package_manager["quadpype"].remote_sources: + os.environ["QUADPYPE_PATH"] = str(package_manager["quadpype"].get_accessible_remote_source()) if "print_versions" in commands: _boot_print_versions(package_manager["quadpype"]) @@ -932,7 +932,7 @@ def boot(): set_addons_environments() running_version = package_manager["quadpype"].running_version - running_version_fullpath = running_version.path.resolve() + running_version_fullpath = running_version.location.resolve() _print(">>> Check ZXP extensions ...") _update_zxp_extensions(running_version_fullpath, global_settings) diff --git a/src/tools/_lib/database/transfer_settings.js b/src/tools/_lib/database/transfer_settings.js index 7bb0bda882..90c58482ac 100644 --- a/src/tools/_lib/database/transfer_settings.js +++ b/src/tools/_lib/database/transfer_settings.js @@ -61,7 +61,7 @@ function transferSettings(mongoSourceURI, mongoDestinationURI, sourceDbName, tar document.type = "core_settings"; document.data.production_version = ""; document.data.staging_version = ""; - document.data.remote_versions_dirs = document.data.openpype_path; + document.data.remote_sources = document.data.openpype_path; delete document.data.openpype_path; targetSettings.insert(document); return