From 1e911e09a10edf8775b7d106b9f1b0f38dced0f1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 13 Feb 2024 15:01:47 +0200 Subject: [PATCH 001/284] expose RS_outputMultilayerMode in creator tab --- .../plugins/create/create_redshift_rop.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 8e88c690b9..c234ef8399 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -14,6 +14,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): family = "redshift_rop" icon = "magic" ext = "exr" + multi_layered_mode = "No Multi-Layered EXR File" # Default to split export and render jobs split_render = True @@ -54,22 +55,33 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Set the linked rop to the Redshift ROP ipr_rop.parm("linked_rop").set(instance_node.path()) - ext = pre_create_data.get("image_format") - filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( - renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), - subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) - ) + multi_layered_mode = pre_create_data.get("multi_layered_mode") ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3} + multilayer_mode_index = {"No Multi-Layered EXR File": "1", + "Full Multi-Layered EXR File": "2" } + + if multilayer_mode_index[multi_layered_mode] == "1": + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) + ) + + elif multilayer_mode_index[multi_layered_mode] == "2": + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext + ) parms = { # Render frame range "trange": 1, # Redshift ROP settings "RS_outputFileNamePrefix": filepath, - "RS_outputMultilayerMode": "1", # no multi-layered exr + "RS_outputMultilayerMode": multilayer_mode_index[multi_layered_mode], "RS_outputBeautyAOVSuffix": "beauty", "RS_outputFileFormat": ext_format_index[ext], } @@ -110,6 +122,11 @@ class CreateRedshiftROP(plugin.HoudiniCreator): image_format_enum = [ "exr", "tif", "jpg", "png", ] + multi_layered_mode = [ + "No Multi-Layered EXR File", + "Full Multi-Layered EXR File" + ] + return attrs + [ BoolDef("farm", @@ -121,5 +138,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): EnumDef("image_format", image_format_enum, default=self.ext, - label="Image Format Options") + label="Image Format Options"), + EnumDef("multi_layered_mode", + multi_layered_mode, + default=self.multi_layered_mode, + label="Multi-Layered EXR") ] From 9385a0ffd73cefca338374d0e32ca1411c0e7a17 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 13 Feb 2024 15:03:20 +0200 Subject: [PATCH 002/284] avoid adding custom aov path to representation data if full mode enabled --- .../houdini/plugins/publish/collect_redshift_rop.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index 26dd942559..c0927601f5 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -70,6 +70,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): beauty_product)} num_aovs = rop.evalParm("RS_aov") + full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2") for index in range(num_aovs): i = index + 1 @@ -82,11 +83,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): if not aov_prefix: aov_prefix = default_prefix - aov_product = self.get_render_product_name(aov_prefix, aov_suffix) - render_products.append(aov_product) + if not full_exr_mode: + aov_product = self.get_render_product_name(aov_prefix, aov_suffix) + render_products.append(aov_product) - files_by_aov[aov_suffix] = self.generate_expected_files(instance, - aov_product) # noqa + files_by_aov[aov_suffix] = self.generate_expected_files(instance, + aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From 1d85ef2466f48e91ffcdcd5543f231b5f993b26a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 14 Feb 2024 14:12:59 +0200 Subject: [PATCH 003/284] Add multipart parm --- .../hosts/houdini/plugins/create/create_redshift_rop.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index c234ef8399..62f7ae4a3c 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -68,6 +68,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) + multipart = False elif multilayer_mode_index[multi_layered_mode] == "2": filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( @@ -75,6 +76,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): subset_name=subset_name, ext=ext ) + multipart = True parms = { # Render frame range @@ -82,6 +84,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Redshift ROP settings "RS_outputFileNamePrefix": filepath, "RS_outputMultilayerMode": multilayer_mode_index[multi_layered_mode], + "RS_aovMultipart" : multipart, "RS_outputBeautyAOVSuffix": "beauty", "RS_outputFileFormat": ext_format_index[ext], } From a2266e2455aafcfc494f91e7ed6ad10db955f14e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 14 Feb 2024 14:16:34 +0200 Subject: [PATCH 004/284] Kayla's comment - update parms when ext is EXR --- .../hosts/houdini/plugins/create/create_redshift_rop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 62f7ae4a3c..e02c2066c0 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -83,11 +83,12 @@ class CreateRedshiftROP(plugin.HoudiniCreator): "trange": 1, # Redshift ROP settings "RS_outputFileNamePrefix": filepath, - "RS_outputMultilayerMode": multilayer_mode_index[multi_layered_mode], - "RS_aovMultipart" : multipart, "RS_outputBeautyAOVSuffix": "beauty", "RS_outputFileFormat": ext_format_index[ext], } + if ext == "exr": + parms["RS_outputMultilayerMode"] = multilayer_mode_index[multi_layered_mode] + parms["RS_aovMultipart"] = multipart if self.selected_nodes: # set up the render camera from the selected node From f32641e8326e3cd6678a331b9f151c90ad0c7575 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 15 Feb 2024 20:19:01 +0200 Subject: [PATCH 005/284] Add $AOV token back --- .../houdini/plugins/create/create_redshift_rop.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index e02c2066c0..724996faa3 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -62,20 +62,16 @@ class CreateRedshiftROP(plugin.HoudiniCreator): multilayer_mode_index = {"No Multi-Layered EXR File": "1", "Full Multi-Layered EXR File": "2" } - if multilayer_mode_index[multi_layered_mode] == "1": - filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) + + if multilayer_mode_index[multi_layered_mode] == "1": multipart = False elif multilayer_mode_index[multi_layered_mode] == "2": - filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( - renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), - subset_name=subset_name, - ext=ext - ) multipart = True parms = { From 431fa3167898e4d62cdb85cffdf61f53db61b8ca Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Feb 2024 20:51:16 +0800 Subject: [PATCH 006/284] use %AOV% instead of for correct expectedFile name --- .../hosts/houdini/plugins/create/create_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 724996faa3..5ecd5d08ed 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -65,7 +65,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) + fmt="%{aov}%.$F4.{ext}".format(aov="AOV", ext=ext) ) if multilayer_mode_index[multi_layered_mode] == "1": From 5981c09ba7cbe8be49ffeb2131a2a5742becb7b4 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 19 Feb 2024 14:23:14 +0200 Subject: [PATCH 007/284] Add Cryptomatte to expected files by default if existed --- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index c0927601f5..198f0fa960 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -83,7 +83,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): if not aov_prefix: aov_prefix = default_prefix - if not full_exr_mode: + if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \ + not full_exr_mode: + aov_product = self.get_render_product_name(aov_prefix, aov_suffix) render_products.append(aov_product) From 0e6e63e4631ab0ccae1535a9f71ae73a0edabf48 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 22 Feb 2024 17:28:54 +0200 Subject: [PATCH 008/284] use $AOV token instead %AOV% --- .../hosts/houdini/plugins/create/create_redshift_rop.py | 2 +- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 5ecd5d08ed..a36f530820 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -65,7 +65,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="%{aov}%.$F4.{ext}".format(aov="AOV", ext=ext) + fmt="$AOV.$F4.{ext}".format(ext=ext) ) if multilayer_mode_index[multi_layered_mode] == "1": diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index 198f0fa960..8a704aa47c 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -118,7 +118,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): # When AOV is explicitly defined in prefix we just swap it out # directly with the AOV suffix to embed it. - # Note: ${AOV} seems to be evaluated in the parameter as %AOV% + # Note: '$AOV' seems to be evaluated in the parameter as '%AOV%' has_aov_in_prefix = "%AOV%" in prefix if has_aov_in_prefix: # It seems that when some special separator characters are present From b5108045175ff85523ec4395049b347960549ee8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 22 Feb 2024 17:33:37 +0200 Subject: [PATCH 009/284] Ignore beauty suffix when full mode is enabled in the RS ROP node --- .../houdini/plugins/publish/collect_redshift_rop.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index 8a704aa47c..ef4ca2ee85 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -60,17 +60,22 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) - # Default beauty AOV + full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2") + if full_exr_mode: + # Ignore beauty suffix if full mode is enabled + # As this is what the rop does. + beauty_suffix = "" + + # Default beauty/main layer AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=beauty_suffix ) render_products = [beauty_product] files_by_aov = { - "_": self.generate_expected_files(instance, + beauty_suffix: self.generate_expected_files(instance, beauty_product)} num_aovs = rop.evalParm("RS_aov") - full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2") for index in range(num_aovs): i = index + 1 From bb3fa9ceeda426d2696be8eb6e0aacdae4baf7b8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 23 Feb 2024 16:48:18 +0200 Subject: [PATCH 010/284] fix code style --- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index ef4ca2ee85..dab4b07e2f 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -73,7 +73,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products = [beauty_product] files_by_aov = { beauty_suffix: self.generate_expected_files(instance, - beauty_product)} + beauty_product) + } num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): From b908ddcedb7e0c29d9d3ca3df35d5940d8e56d82 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:10:22 +0100 Subject: [PATCH 011/284] split anatomy.py into multiple python files --- client/ayon_core/pipeline/anatomy.py | 1550 ----------------- client/ayon_core/pipeline/anatomy/__init__.py | 17 + client/ayon_core/pipeline/anatomy/anatomy.py | 502 ++++++ .../ayon_core/pipeline/anatomy/exceptions.py | 39 + client/ayon_core/pipeline/anatomy/roots.py | 534 ++++++ .../ayon_core/pipeline/anatomy/templates.py | 523 ++++++ 6 files changed, 1615 insertions(+), 1550 deletions(-) delete mode 100644 client/ayon_core/pipeline/anatomy.py create mode 100644 client/ayon_core/pipeline/anatomy/__init__.py create mode 100644 client/ayon_core/pipeline/anatomy/anatomy.py create mode 100644 client/ayon_core/pipeline/anatomy/exceptions.py create mode 100644 client/ayon_core/pipeline/anatomy/roots.py create mode 100644 client/ayon_core/pipeline/anatomy/templates.py diff --git a/client/ayon_core/pipeline/anatomy.py b/client/ayon_core/pipeline/anatomy.py deleted file mode 100644 index d6e09bad39..0000000000 --- a/client/ayon_core/pipeline/anatomy.py +++ /dev/null @@ -1,1550 +0,0 @@ -import os -import re -import copy -import platform -import collections -import numbers -import time - -import six -import ayon_api - -from ayon_core.lib import Logger, get_local_site_id -from ayon_core.lib.path_templates import ( - TemplateUnsolved, - TemplateResult, - StringTemplate, - TemplatesDict, - FormatObject, -) -from ayon_core.addon import AddonsManager - -log = Logger.get_logger(__name__) - - -class ProjectNotSet(Exception): - """Exception raised when is created Anatomy without project name.""" - - -class RootCombinationError(Exception): - """This exception is raised when templates has combined root types.""" - - def __init__(self, roots): - joined_roots = ", ".join( - ["\"{}\"".format(_root) for _root in roots] - ) - # TODO better error message - msg = ( - "Combination of root with and" - " without root name in AnatomyTemplates. {}" - ).format(joined_roots) - - super(RootCombinationError, self).__init__(msg) - - -class BaseAnatomy(object): - """Anatomy module helps to keep project settings. - - Wraps key project specifications, AnatomyTemplates and Roots. - """ - root_key_regex = re.compile(r"{(root?[^}]+)}") - root_name_regex = re.compile(r"root\[([^]]+)\]") - - def __init__(self, project_entity, root_overrides=None): - project_name = project_entity["name"] - self.project_name = project_name - self.project_code = project_entity["code"] - - self._data = self._prepare_anatomy_data( - project_entity, root_overrides - ) - self._templates_obj = AnatomyTemplates(self) - self._roots_obj = Roots(self) - - # Anatomy used as dictionary - # - implemented only getters returning copy - def __getitem__(self, key): - return copy.deepcopy(self._data[key]) - - def get(self, key, default=None): - return copy.deepcopy(self._data).get(key, default) - - def keys(self): - return copy.deepcopy(self._data).keys() - - def values(self): - return copy.deepcopy(self._data).values() - - def items(self): - return copy.deepcopy(self._data).items() - - def _prepare_anatomy_data(self, project_entity, root_overrides): - """Prepare anatomy data for further processing. - - Method added to replace `{task}` with `{task[name]}` in templates. - """ - - anatomy_data = self._project_entity_to_anatomy_data(project_entity) - - self._apply_local_settings_on_anatomy_data( - anatomy_data, - root_overrides - ) - - return anatomy_data - - @property - def templates(self): - """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" - return self._templates_obj.templates - - @property - def templates_obj(self): - """Return `AnatomyTemplates` object of current Anatomy instance.""" - return self._templates_obj - - def format(self, *args, **kwargs): - """Wrap `format` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format(*args, **kwargs) - - def format_all(self, *args, **kwargs): - """Wrap `format_all` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format_all(*args, **kwargs) - - @property - def roots(self): - """Wrap `roots` property of Anatomy's `roots_obj`.""" - return self._roots_obj.roots - - @property - def roots_obj(self): - """Return `Roots` object of current Anatomy instance.""" - return self._roots_obj - - def root_environments(self): - """Return AYON_PROJECT_ROOT_* environments for current project.""" - return self._roots_obj.root_environments() - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - return self.roots_obj.root_environmets_fill_data(template) - - def find_root_template_from_path(self, *args, **kwargs): - """Wrapper for Roots `find_root_template_from_path`.""" - return self.roots_obj.find_root_template_from_path(*args, **kwargs) - - def path_remapper(self, *args, **kwargs): - """Wrapper for Roots `path_remapper`.""" - return self.roots_obj.path_remapper(*args, **kwargs) - - def all_root_paths(self): - """Wrapper for Roots `all_root_paths`.""" - return self.roots_obj.all_root_paths() - - def set_root_environments(self): - """Set AYON_PROJECT_ROOT_* environments for current project.""" - self._roots_obj.set_root_environments() - - def root_names(self): - """Return root names for current project.""" - return self.root_names_from_templates(self.templates) - - def _root_keys_from_templates(self, data): - """Extract root key from templates in data. - - Args: - data (dict): Data that may contain templates as string. - - Return: - set: Set of all root names from templates as strings. - - Output example: `{"root[work]", "root[publish]"}` - """ - - output = set() - if isinstance(data, dict): - for value in data.values(): - for root in self._root_keys_from_templates(value): - output.add(root) - - elif isinstance(data, str): - for group in re.findall(self.root_key_regex, data): - output.add(group) - - return output - - def root_value_for_template(self, template): - """Returns value of root key from template.""" - root_templates = [] - for group in re.findall(self.root_key_regex, template): - root_templates.append("{" + group + "}") - - if not root_templates: - return None - - return root_templates[0].format(**{"root": self.roots}) - - def root_names_from_templates(self, templates): - """Extract root names form anatomy templates. - - Returns None if values in templates contain only "{root}". - Empty list is returned if there is no "root" in templates. - Else returns all root names from templates in list. - - RootCombinationError is raised when templates contain both root types, - basic "{root}" and with root name specification "{root[work]}". - - Args: - templates (dict): Anatomy templates where roots are not filled. - - Return: - list/None: List of all root names from templates as strings when - multiroot setup is used, otherwise None is returned. - """ - roots = list(self._root_keys_from_templates(templates)) - # Return empty list if no roots found in templates - if not roots: - return roots - - # Raise exception when root keys have roots with and without root name. - # Invalid output example: ["root", "root[project]", "root[render]"] - if len(roots) > 1 and "root" in roots: - raise RootCombinationError(roots) - - # Return None if "root" without root name in templates - if len(roots) == 1 and roots[0] == "root": - return None - - names = set() - for root in roots: - for group in re.findall(self.root_name_regex, root): - names.add(group) - return list(names) - - def fill_root(self, template_path): - """Fill template path where is only "root" key unfilled. - - Args: - template_path (str): Path with "root" key in. - Example path: "{root}/projects/MyProject/Shot01/Lighting/..." - - Return: - str: formatted path - """ - # NOTE does not care if there are different keys than "root" - return template_path.format(**{"root": self.roots}) - - @classmethod - def fill_root_with_path(cls, rootless_path, root_path): - """Fill path without filled "root" key with passed path. - - This is helper to fill root with different directory path than anatomy - has defined no matter if is single or multiroot. - - Output path is same as input path if `rootless_path` does not contain - unfilled root key. - - Args: - rootless_path (str): Path without filled "root" key. Example: - "{root[work]}/MyProject/..." - root_path (str): What should replace root key in `rootless_path`. - - Returns: - str: Path with filled root. - """ - output = str(rootless_path) - for group in re.findall(cls.root_key_regex, rootless_path): - replacement = "{" + group + "}" - output = output.replace(replacement, root_path) - - return output - - def replace_root_with_env_key(self, filepath, template=None): - """Replace root of path with environment key. - - # Example: - ## Project with roots: - ``` - { - "nas": { - "windows": P:/projects", - ... - } - ... - } - ``` - - ## Entered filepath - "P:/projects/project/folder/task/animation_v001.ma" - - ## Entered template - "<{}>" - - ## Output - "/project/folder/task/animation_v001.ma" - - Args: - filepath (str): Full file path where root should be replaced. - template (str): Optional template for environment key. Must - have one index format key. - Default value if not entered: "${}" - - Returns: - str: Path where root is replaced with environment root key. - - Raise: - ValueError: When project's roots were not found in entered path. - """ - success, rootless_path = self.find_root_template_from_path(filepath) - if not success: - raise ValueError( - "{}: Project's roots were not found in path: {}".format( - self.project_name, filepath - ) - ) - - data = self.root_environmets_fill_data(template) - return rootless_path.format(**data) - - def _project_entity_to_anatomy_data(self, project_entity): - """Convert project document to anatomy data. - - Probably should fill missing keys and values. - """ - - output = copy.deepcopy(project_entity["config"]) - # TODO remove AYON convertion - task_types = copy.deepcopy(project_entity["taskTypes"]) - new_task_types = {} - for task_type in task_types: - name = task_type["name"] - new_task_types[name] = task_type - output["tasks"] = new_task_types - output["attributes"] = copy.deepcopy(project_entity["attrib"]) - - return output - - def _apply_local_settings_on_anatomy_data( - self, anatomy_data, root_overrides - ): - """Apply local settings on anatomy data. - - ATM local settings can modify project roots. Project name is required - as local settings have data stored data by project's name. - - Local settings override root values in this order: - 1.) Check if local settings contain overrides for default project and - apply it's values on roots if there are any. - 2.) If passed `project_name` is not None then check project specific - overrides in local settings for the project and apply it's value on - roots if there are any. - - NOTE: Root values of default project from local settings are always - applied if are set. - - Args: - anatomy_data (dict): Data for anatomy. - root_overrides (dict): Data of local settings. - """ - - # Skip processing if roots for current active site are not available in - # local settings - if not root_overrides: - return - - current_platform = platform.system().lower() - - root_data = anatomy_data["roots"] - for root_name, path in root_overrides.items(): - if root_name not in root_data: - continue - anatomy_data["roots"][root_name][current_platform] = ( - path - ) - - -class CacheItem: - """Helper to cache data. - - Helper does not handle refresh of data and does not mark data as outdated. - Who uses the object should check of outdated state on his own will. - """ - - default_lifetime = 10 - - def __init__(self, lifetime=None): - self._data = None - self._cached = None - self._lifetime = lifetime or self.default_lifetime - - @property - def data(self): - """Cached data/object. - - Returns: - Any: Whatever was cached. - """ - - return self._data - - @property - def is_outdated(self): - """Item has outdated cache. - - Lifetime of cache item expired or was not yet set. - - Returns: - bool: Item is outdated. - """ - - if self._cached is None: - return True - return (time.time() - self._cached) > self._lifetime - - def update_data(self, data): - """Update cache of data. - - Args: - data (Any): Data to cache. - """ - - self._data = data - self._cached = time.time() - - -class Anatomy(BaseAnatomy): - _sync_server_addon_cache = CacheItem() - _project_cache = collections.defaultdict(CacheItem) - _default_site_id_cache = collections.defaultdict(CacheItem) - _root_overrides_cache = collections.defaultdict( - lambda: collections.defaultdict(CacheItem) - ) - - def __init__( - self, project_name=None, site_name=None, project_entity=None - ): - if not project_name: - project_name = os.environ.get("AYON_PROJECT_NAME") - - if not project_name: - raise ProjectNotSet(( - "Implementation bug: Project name is not set. Anatomy requires" - " to load data for specific project." - )) - - if not project_entity: - project_entity = self.get_project_entity_from_cache(project_name) - root_overrides = self._get_site_root_overrides( - project_name, site_name - ) - - super(Anatomy, self).__init__(project_entity, root_overrides) - - @classmethod - def get_project_entity_from_cache(cls, project_name): - project_cache = cls._project_cache[project_name] - if project_cache.is_outdated: - project_cache.update_data(ayon_api.get_project(project_name)) - return copy.deepcopy(project_cache.data) - - @classmethod - def get_sync_server_addon(cls): - if cls._sync_server_addon_cache.is_outdated: - manager = AddonsManager() - cls._sync_server_addon_cache.update_data( - manager.get_enabled_addon("sync_server") - ) - return cls._sync_server_addon_cache.data - - @classmethod - def _get_studio_roots_overrides(cls, project_name): - """This would return 'studio' site override by local settings. - - Notes: - This logic handles local overrides of studio site which may be - available even when sync server is not enabled. - Handling of 'studio' and 'local' site was separated as preparation - for AYON development where that will be received from - separated sources. - - Args: - project_name (str): Name of project. - - Returns: - Union[Dict[str, str], None]): Local root overrides. - """ - if not project_name: - return - return ayon_api.get_project_roots_for_site( - project_name, get_local_site_id() - ) - - @classmethod - def _get_site_root_overrides(cls, project_name, site_name): - """Get root overrides for site. - - Args: - project_name (str): Project name for which root overrides should be - received. - site_name (Union[str, None]): Name of site for which root overrides - should be returned. - """ - - # First check if sync server is available and enabled - sync_server = cls.get_sync_server_addon() - if sync_server is None or not sync_server.enabled: - # QUESTION is ok to force 'studio' when site sync is not enabled? - site_name = "studio" - - elif not site_name: - # Use sync server to receive active site name - project_cache = cls._default_site_id_cache[project_name] - if project_cache.is_outdated: - project_cache.update_data( - sync_server.get_active_site_type(project_name) - ) - site_name = project_cache.data - - site_cache = cls._root_overrides_cache[project_name][site_name] - if site_cache.is_outdated: - if site_name == "studio": - # Handle studio root overrides without sync server - # - studio root overrides can be done even without sync server - roots_overrides = cls._get_studio_roots_overrides( - project_name - ) - else: - # Ask sync server to get roots overrides - roots_overrides = sync_server.get_site_root_overrides( - project_name, site_name - ) - site_cache.update_data(roots_overrides) - return site_cache.data - - -class AnatomyTemplateUnsolved(TemplateUnsolved): - """Exception for unsolved template when strict is set to True.""" - - msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" - - -class AnatomyTemplateResult(TemplateResult): - rootless = None - - def __new__(cls, result, rootless_path): - new_obj = super(AnatomyTemplateResult, cls).__new__( - cls, - str(result), - result.template, - result.solved, - result.used_values, - result.missing_keys, - result.invalid_types - ) - new_obj.rootless = rootless_path - return new_obj - - def validate(self): - if not self.solved: - raise AnatomyTemplateUnsolved( - self.template, - self.missing_keys, - self.invalid_types - ) - - def copy(self): - tmp = TemplateResult( - str(self), - self.template, - self.solved, - self.used_values, - self.missing_keys, - self.invalid_types - ) - return self.__class__(tmp, self.rootless) - - def normalized(self): - """Convert to normalized path.""" - - tmp = TemplateResult( - os.path.normpath(self), - self.template, - self.solved, - self.used_values, - self.missing_keys, - self.invalid_types - ) - return self.__class__(tmp, self.rootless) - - -class AnatomyStringTemplate(StringTemplate): - """String template which has access to anatomy.""" - - def __init__(self, anatomy_templates, template): - self.anatomy_templates = anatomy_templates - super(AnatomyStringTemplate, self).__init__(template) - - def format(self, data): - """Format template and add 'root' key to data if not available. - - Args: - data (dict[str, Any]): Formatting data for template. - - Returns: - AnatomyTemplateResult: Formatting result. - """ - - anatomy_templates = self.anatomy_templates - if not data.get("root"): - data = copy.deepcopy(data) - data["root"] = anatomy_templates.anatomy.roots - result = StringTemplate.format(self, data) - rootless_path = anatomy_templates.rootless_path_from_result(result) - return AnatomyTemplateResult(result, rootless_path) - - -class AnatomyTemplates(TemplatesDict): - inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") - inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") - - def __init__(self, anatomy): - super(AnatomyTemplates, self).__init__() - self.anatomy = anatomy - self.loaded_project = None - - def reset(self): - self._raw_templates = None - self._templates = None - self._objected_templates = None - - @property - def project_name(self): - return self.anatomy.project_name - - @property - def roots(self): - return self.anatomy.roots - - @property - def templates(self): - self._validate_discovery() - return self._templates - - @property - def objected_templates(self): - self._validate_discovery() - return self._objected_templates - - def _validate_discovery(self): - if self.project_name != self.loaded_project: - self.reset() - - if self._templates is None: - self._discover() - self.loaded_project = self.project_name - - def _format_value(self, value, data): - if isinstance(value, RootItem): - return self._solve_dict(value, data) - return super(AnatomyTemplates, self)._format_value(value, data) - - @staticmethod - def _ayon_template_conversion(templates): - def _convert_template_item(template_item): - # Change 'directory' to 'folder' - if "directory" in template_item: - template_item["folder"] = template_item["directory"] - - if ( - "path" not in template_item - and "file" in template_item - and "folder" in template_item - ): - template_item["path"] = "/".join( - (template_item["folder"], template_item["file"]) - ) - - def _get_default_template_name(templates): - default_template = None - for name, template in templates.items(): - if name == "default": - return "default" - - if default_template is None: - default_template = name - - return default_template - - def _fill_template_category(templates, cat_templates, cat_key): - default_template_name = _get_default_template_name(cat_templates) - for template_name, cat_template in cat_templates.items(): - _convert_template_item(cat_template) - if template_name == default_template_name: - templates[cat_key] = cat_template - else: - new_name = "{}_{}".format(cat_key, template_name) - templates["others"][new_name] = cat_template - - others_templates = templates.pop("others", None) or {} - new_others_templates = {} - templates["others"] = new_others_templates - for name, template in others_templates.items(): - _convert_template_item(template) - new_others_templates[name] = template - - for key in ( - "work", - "publish", - "hero", - ): - cat_templates = templates.pop(key) - _fill_template_category(templates, cat_templates, key) - - delivery_templates = templates.pop("delivery", None) or {} - new_delivery_templates = {} - for name, delivery_template in delivery_templates.items(): - new_delivery_templates[name] = "/".join( - (delivery_template["directory"], delivery_template["file"]) - ) - templates["delivery"] = new_delivery_templates - - def set_templates(self, templates): - if not templates: - self.reset() - return - - templates = copy.deepcopy(templates) - # TODO remove AYON convertion - self._ayon_template_conversion(templates) - - self._raw_templates = copy.deepcopy(templates) - v_queue = collections.deque() - v_queue.append(templates) - while v_queue: - item = v_queue.popleft() - if not isinstance(item, dict): - continue - - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, dict): - v_queue.append(value) - - elif ( - isinstance(value, six.string_types) - and "{task}" in value - ): - item[key] = value.replace("{task}", "{task[name]}") - - solved_templates = self.solve_template_inner_links(templates) - self._templates = solved_templates - self._objected_templates = self.create_objected_templates( - solved_templates - ) - - def _create_template_object(self, template): - return AnatomyStringTemplate(self, template) - - def default_templates(self): - """Return default templates data with solved inner keys.""" - return self.solve_template_inner_links( - self.anatomy["templates"] - ) - - def _discover(self): - """ Loads anatomy templates from yaml. - Default templates are loaded if project is not set or project does - not have set it's own. - TODO: create templates if not exist. - - Returns: - TemplatesResultDict: Contain templates data for current project of - default templates. - """ - - if self.project_name is None: - # QUESTION create project specific if not found? - raise AssertionError(( - "Project \"{0}\" does not have his own templates." - " Trying to use default." - ).format(self.project_name)) - - self.set_templates(self.anatomy["templates"]) - - @classmethod - def replace_inner_keys(cls, matches, value, key_values, key): - """Replacement of inner keys in template values.""" - for match in matches: - anatomy_sub_keys = ( - cls.inner_key_name_pattern.findall(match) - ) - if key in anatomy_sub_keys: - raise ValueError(( - "Unsolvable recursion in inner keys, " - "key: \"{}\" is in his own value." - " Can't determine source, please check Anatomy templates." - ).format(key)) - - for anatomy_sub_key in anatomy_sub_keys: - replace_value = key_values.get(anatomy_sub_key) - if replace_value is None: - raise KeyError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`." - ).format(key, anatomy_sub_key)) - - if not ( - isinstance(replace_value, numbers.Number) - or isinstance(replace_value, six.string_types) - ): - raise ValueError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`" - " with value `{2}`." - ).format(key, anatomy_sub_key, str(replace_value))) - - value = value.replace(match, str(replace_value)) - - return value - - @classmethod - def prepare_inner_keys(cls, key_values): - """Check values of inner keys. - - Check if inner key exist in template group and has valid value. - It is also required to avoid infinite loop with unsolvable recursion - when first inner key's value refers to second inner key's value where - first is used. - """ - keys_to_solve = set(key_values.keys()) - while True: - found = False - for key in tuple(keys_to_solve): - value = key_values[key] - - if isinstance(value, six.string_types): - matches = cls.inner_key_pattern.findall(value) - if not matches: - keys_to_solve.remove(key) - continue - - found = True - key_values[key] = cls.replace_inner_keys( - matches, value, key_values, key - ) - continue - - elif not isinstance(value, dict): - keys_to_solve.remove(key) - continue - - subdict_found = False - for _key, _value in tuple(value.items()): - matches = cls.inner_key_pattern.findall(_value) - if not matches: - continue - - subdict_found = True - found = True - key_values[key][_key] = cls.replace_inner_keys( - matches, _value, key_values, - "{}.{}".format(key, _key) - ) - - if not subdict_found: - keys_to_solve.remove(key) - - if not found: - break - - return key_values - - @classmethod - def solve_template_inner_links(cls, templates): - """Solve templates inner keys identified by "{@*}". - - Process is split into 2 parts. - First is collecting all global keys (keys in top hierarchy where value - is not dictionary). All global keys are set for all group keys (keys - in top hierarchy where value is dictionary). Value of a key is not - overridden in group if already contain value for the key. - - In second part all keys with "at" symbol in value are replaced with - value of the key afterward "at" symbol from the group. - - Args: - templates (dict): Raw templates data. - - Example: - templates:: - key_1: "value_1", - key_2: "{@key_1}/{filling_key}" - - group_1: - key_3: "value_3/{@key_2}" - - group_2: - key_2": "value_2" - key_4": "value_4/{@key_2}" - - output:: - key_1: "value_1" - key_2: "value_1/{filling_key}" - - group_1: { - key_1: "value_1" - key_2: "value_1/{filling_key}" - key_3: "value_3/value_1/{filling_key}" - - group_2: { - key_1: "value_1" - key_2: "value_2" - key_4: "value_3/value_2" - """ - default_key_values = templates.pop("common", {}) - for key, value in tuple(templates.items()): - if isinstance(value, dict): - continue - default_key_values[key] = templates.pop(key) - - # Pop "others" key before before expected keys are processed - other_templates = templates.pop("others") or {} - - keys_by_subkey = {} - for sub_key, sub_value in templates.items(): - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - for sub_key, sub_value in other_templates.items(): - if sub_key in keys_by_subkey: - log.warning(( - "Key \"{}\" is duplicated in others. Skipping." - ).format(sub_key)) - continue - - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) - - for key, value in default_keys_by_subkeys.items(): - keys_by_subkey[key] = value - - return keys_by_subkey - - @classmethod - def _dict_to_subkeys_list(cls, subdict, pre_keys=None): - if pre_keys is None: - pre_keys = [] - output = [] - for key in subdict: - value = subdict[key] - result = list(pre_keys) - result.append(key) - if isinstance(value, dict): - for item in cls._dict_to_subkeys_list(value, result): - output.append(item) - else: - output.append(result) - return output - - def _keys_to_dicts(self, key_list, value): - if not key_list: - return None - if len(key_list) == 1: - return {key_list[0]: value} - return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} - - @classmethod - def rootless_path_from_result(cls, result): - """Calculate rootless path from formatting result. - - Args: - result (TemplateResult): Result of StringTemplate formatting. - - Returns: - str: Rootless path if result contains one of anatomy roots. - """ - - used_values = result.used_values - missing_keys = result.missing_keys - template = result.template - invalid_types = result.invalid_types - if ( - "root" not in used_values - or "root" in missing_keys - or "{root" not in template - ): - return - - for invalid_type in invalid_types: - if "root" in invalid_type: - return - - root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) - if not root_keys: - return - - output = str(result) - for used_root_keys in root_keys: - if not used_root_keys: - continue - - used_value = used_values - root_key = None - for key in used_root_keys: - used_value = used_value[key] - if root_key is None: - root_key = key - else: - root_key += "[{}]".format(key) - - root_key = "{" + root_key + "}" - output = output.replace(str(used_value), root_key) - - return output - - def format(self, data, strict=True): - copy_data = copy.deepcopy(data) - roots = self.roots - if roots: - copy_data["root"] = roots - result = super(AnatomyTemplates, self).format(copy_data) - result.strict = strict - return result - - def format_all(self, in_data, only_keys=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to False so accessing unfilled keys in templates - won't raise any exceptions. - """ - return self.format(in_data, strict=False) - - -class RootItem(FormatObject): - """Represents one item or roots. - - Holds raw data of root item specification. Raw data contain value - for each platform, but current platform value is used when object - is used for formatting of template. - - Args: - root_raw_data (dict): Dictionary containing root values by platform - names. ["windows", "linux" and "darwin"] - name (str, optional): Root name which is representing. Used with - multi root setup otherwise None value is expected. - parent_keys (list, optional): All dictionary parent keys. Values of - `parent_keys` are used for get full key which RootItem is - representing. Used for replacing root value in path with - formattable key. e.g. parent_keys == ["work"] -> {root[work]} - parent (object, optional): It is expected to be `Roots` object. - Value of `parent` won't affect code logic much. - """ - - def __init__( - self, root_raw_data, name=None, parent_keys=None, parent=None - ): - lowered_platform_keys = {} - for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value - self.raw_data = lowered_platform_keys - self.cleaned_data = self._clean_roots(lowered_platform_keys) - self.name = name - self.parent_keys = parent_keys or [] - self.parent = parent - - self.available_platforms = list(lowered_platform_keys.keys()) - self.value = lowered_platform_keys.get(platform.system().lower()) - self.clean_value = self.clean_root(self.value) - - def __format__(self, *args, **kwargs): - return self.value.__format__(*args, **kwargs) - - def __str__(self): - return str(self.value) - - def __repr__(self): - return self.__str__() - - def __getitem__(self, key): - if isinstance(key, numbers.Number): - return self.value[key] - - additional_info = "" - if self.parent and self.parent.project_name: - additional_info += " for project \"{}\"".format( - self.parent.project_name - ) - - raise AssertionError( - "Root key \"{}\" is missing{}.".format( - key, additional_info - ) - ) - - def full_key(self): - """Full key value for dictionary formatting in template. - - Returns: - str: Return full replacement key for formatting. This helps when - multiple roots are set. In that case e.g. `"root[work]"` is - returned. - """ - if not self.name: - return "root" - - joined_parent_keys = "".join( - ["[{}]".format(key) for key in self.parent_keys] - ) - return "root{}".format(joined_parent_keys) - - def clean_path(self, path): - """Just replace backslashes with forward slashes.""" - return str(path).replace("\\", "/") - - def clean_root(self, root): - """Makes sure root value does not end with slash.""" - if root: - root = self.clean_path(root) - while root.endswith("/"): - root = root[:-1] - return root - - def _clean_roots(self, raw_data): - """Clean all values of raw root item values.""" - cleaned = {} - for key, value in raw_data.items(): - cleaned[key] = self.clean_root(value) - return cleaned - - def path_remapper(self, path, dst_platform=None, src_platform=None): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - cleaned_path = self.clean_path(path) - if dst_platform: - dst_root_clean = self.cleaned_data.get(dst_platform) - if not dst_root_clean: - key_part = "" - full_key = self.full_key() - if full_key != "root": - key_part += "\"{}\" ".format(full_key) - - log.warning( - "Root {}miss platform \"{}\" definition.".format( - key_part, dst_platform - ) - ) - return None - - if cleaned_path.startswith(dst_root_clean): - return cleaned_path - - if src_platform: - src_root_clean = self.cleaned_data.get(src_platform) - if src_root_clean is None: - log.warning( - "Root \"{}\" miss platform \"{}\" definition.".format( - self.full_key(), src_platform - ) - ) - return None - - if not cleaned_path.startswith(src_root_clean): - return None - - subpath = cleaned_path[len(src_root_clean):] - if dst_platform: - # `dst_root_clean` is used from upper condition - return dst_root_clean + subpath - return self.clean_value + subpath - - result, template = self.find_root_template_from_path(path) - if not result: - return None - - def parent_dict(keys, value): - if not keys: - return value - - key = keys.pop(0) - return {key: parent_dict(keys, value)} - - if dst_platform: - format_value = parent_dict(list(self.parent_keys), dst_root_clean) - else: - format_value = parent_dict(list(self.parent_keys), self.value) - - return template.format(**{"root": format_value}) - - def find_root_template_from_path(self, path): - """Replaces known root value with formattable key in path. - - All platform values are checked for this replacement. - - Args: - path (str): Path where root value should be found. - - Returns: - tuple: Tuple contain 2 values: `success` (bool) and `path` (str). - When success it True then path should contain replaced root - value with formattable key. - - Example: - When input path is:: - "C:/windows/path/root/projects/my_project/file.ext" - - And raw data of item looks like:: - { - "windows": "C:/windows/path/root", - "linux": "/mount/root" - } - - Output will be:: - (True, "{root}/projects/my_project/file.ext") - - If any of raw data value wouldn't match path's root output is:: - (False, "C:/windows/path/root/projects/my_project/file.ext") - """ - result = False - output = str(path) - - mod_path = self.clean_path(path) - for root_os, root_path in self.cleaned_data.items(): - # Skip empty paths - if not root_path: - continue - - _mod_path = mod_path # reset to original cleaned value - if root_os == "windows": - root_path = root_path.lower() - _mod_path = _mod_path.lower() - - if _mod_path.startswith(root_path): - result = True - replacement = "{" + self.full_key() + "}" - output = replacement + mod_path[len(root_path):] - break - - return (result, output) - - -class Roots: - """Object which should be used for formatting "root" key in templates. - - Args: - anatomy Anatomy: Anatomy object created for a specific project. - """ - - env_prefix = "AYON_PROJECT_ROOT" - roots_filename = "roots.json" - - def __init__(self, anatomy): - self.anatomy = anatomy - self.loaded_project = None - self._roots = None - - def __format__(self, *args, **kwargs): - return self.roots.__format__(*args, **kwargs) - - def __getitem__(self, key): - return self.roots[key] - - def reset(self): - """Reset current roots value.""" - self._roots = None - - def path_remapper( - self, path, dst_platform=None, src_platform=None, roots=None - ): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - if roots is None: - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if "{root" in path: - path = path.format(**{"root": roots}) - # If `dst_platform` is not specified then return else continue. - if not dst_platform: - return path - - if isinstance(roots, RootItem): - return roots.path_remapper(path, dst_platform, src_platform) - - for _root in roots.values(): - result = self.path_remapper( - path, dst_platform, src_platform, _root - ) - if result is not None: - return result - - def find_root_template_from_path(self, path, roots=None): - """Find root value in entered path and replace it with formatting key. - - Args: - path (str): Source path where root will be searched. - roots (Roots/dict, optional): It is possible to use different - roots than instance where method was triggered has. - - Returns: - tuple: Output contains tuple with bool representing success as - first value and path with or without replaced root with - formatting key as second value. - - Raises: - ValueError: When roots are not entered and can't be loaded. - """ - if roots is None: - log.debug( - "Looking for matching root in path \"{}\".".format(path) - ) - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if isinstance(roots, RootItem): - return roots.find_root_template_from_path(path) - - for root_name, _root in roots.items(): - success, result = self.find_root_template_from_path(path, _root) - if success: - log.info("Found match in root \"{}\".".format(root_name)) - return success, result - - log.warning("No matching root was found in current setting.") - return (False, path) - - def set_root_environments(self): - """Set root environments for current project.""" - for key, value in self.root_environments().items(): - os.environ[key] = value - - def root_environments(self): - """Use root keys to create unique keys for environment variables. - - Concatenates prefix "AYON_PROJECT_ROOT_" with root keys to create - unique keys. - - Returns: - dict: Result is `{(str): (str)}` dicitonary where key represents - unique key concatenated by keys and value is root value of - current platform root. - - Example: - With raw root values:: - "work": { - "windows": "P:/projects/work", - "linux": "/mnt/share/projects/work", - "darwin": "/darwin/path/work" - }, - "publish": { - "windows": "P:/projects/publish", - "linux": "/mnt/share/projects/publish", - "darwin": "/darwin/path/publish" - } - - Result on windows platform:: - { - "AYON_PROJECT_ROOT_WORK": "P:/projects/work", - "AYON_PROJECT_ROOT_PUBLISH": "P:/projects/publish" - } - - """ - return self._root_environments() - - def all_root_paths(self, roots=None): - """Return all paths for all roots of all platforms.""" - if roots is None: - roots = self.roots - - output = [] - if isinstance(roots, RootItem): - for value in roots.raw_data.values(): - output.append(value) - return output - - for _roots in roots.values(): - output.extend(self.all_root_paths(_roots)) - return output - - def _root_environments(self, keys=None, roots=None): - if not keys: - keys = [] - if roots is None: - roots = self.roots - - if isinstance(roots, RootItem): - key_items = [self.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - - key = "_".join(key_items) - # Make sure key and value does not contain unicode - # - can happen in Python 2 hosts - return {str(key): str(roots.value)} - - output = {} - for _key, _value in roots.items(): - _keys = list(keys) - _keys.append(_key) - output.update(self._root_environments(_keys, _value)) - return output - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - if template is None: - template = "${}" - return self._root_environmets_fill_data(template) - - def _root_environmets_fill_data(self, template, keys=None, roots=None): - if keys is None and roots is None: - return { - "root": self._root_environmets_fill_data( - template, [], self.roots - ) - } - - if isinstance(roots, RootItem): - key_items = [Roots.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - key = "_".join(key_items) - return template.format(key) - - output = {} - for key, value in roots.items(): - _keys = list(keys) - _keys.append(key) - output[key] = self._root_environmets_fill_data( - template, _keys, value - ) - return output - - @property - def project_name(self): - """Return project name which will be used for loading root values.""" - return self.anatomy.project_name - - @property - def roots(self): - """Property for filling "root" key in templates. - - This property returns roots for current project or default root values. - Warning: - Default roots value may cause issues when project use different - roots settings. That may happen when project use multiroot - templates but default roots miss their keys. - """ - if self.project_name != self.loaded_project: - self._roots = None - - if self._roots is None: - self._roots = self._discover() - self.loaded_project = self.project_name - return self._roots - - def _discover(self): - """ Loads current project's roots or default. - - Default roots are loaded if project override's does not contain roots. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - - return self._parse_dict(self.anatomy["roots"], parent=self) - - @staticmethod - def _parse_dict(data, key=None, parent_keys=None, parent=None): - """Parse roots raw data into RootItem or dictionary with RootItems. - - Converting raw roots data to `RootItem` helps to handle platform keys. - This method is recursive to be able handle multiroot setup and - is static to be able to load default roots without creating new object. - - Args: - data (dict): Should contain raw roots data to be parsed. - key (str, optional): Current root key. Set by recursion. - parent_keys (list): Parent dictionary keys. Set by recursion. - parent (Roots, optional): Parent object set in `RootItem` - helps to keep RootItem instance updated with `Roots` object. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - if not parent_keys: - parent_keys = [] - is_last = False - for value in data.values(): - if isinstance(value, six.string_types): - is_last = True - break - - if is_last: - return RootItem(data, key, parent_keys, parent=parent) - - output = {} - for _key, value in data.items(): - _parent_keys = list(parent_keys) - _parent_keys.append(_key) - output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) - return output diff --git a/client/ayon_core/pipeline/anatomy/__init__.py b/client/ayon_core/pipeline/anatomy/__init__.py new file mode 100644 index 0000000000..336d09ccaa --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/__init__.py @@ -0,0 +1,17 @@ +from .exceptions import ( + ProjectNotSet, + RootCombinationError, + TemplateMissingKey, + AnatomyTemplateUnsolved, +) +from .anatomy import Anatomy + + +__all__ = ( + "ProjectNotSet", + "RootCombinationError", + "TemplateMissingKey", + "AnatomyTemplateUnsolved", + + "Anatomy", +) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py new file mode 100644 index 0000000000..5eaf918663 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -0,0 +1,502 @@ +import os +import re +import copy +import platform +import collections +import time + +import ayon_api + +from ayon_core.lib import Logger, get_local_site_id +from ayon_core.addon import AddonsManager + +from .exceptions import RootCombinationError, ProjectNotSet +from .roots import Roots +from .templates import AnatomyTemplates + +log = Logger.get_logger(__name__) + + +class BaseAnatomy(object): + """Anatomy module helps to keep project settings. + + Wraps key project specifications, AnatomyTemplates and Roots. + """ + root_key_regex = re.compile(r"{(root?[^}]+)}") + root_name_regex = re.compile(r"root\[([^]]+)\]") + + def __init__(self, project_entity, root_overrides=None): + project_name = project_entity["name"] + self.project_name = project_name + self.project_code = project_entity["code"] + + self._data = self._prepare_anatomy_data( + project_entity, root_overrides + ) + self._templates_obj = AnatomyTemplates(self) + self._roots_obj = Roots(self) + + # Anatomy used as dictionary + # - implemented only getters returning copy + def __getitem__(self, key): + return copy.deepcopy(self._data[key]) + + def get(self, key, default=None): + return copy.deepcopy(self._data).get(key, default) + + def keys(self): + return copy.deepcopy(self._data).keys() + + def values(self): + return copy.deepcopy(self._data).values() + + def items(self): + return copy.deepcopy(self._data).items() + + def _prepare_anatomy_data(self, project_entity, root_overrides): + """Prepare anatomy data for further processing. + + Method added to replace `{task}` with `{task[name]}` in templates. + """ + + anatomy_data = self._project_entity_to_anatomy_data(project_entity) + + self._apply_local_settings_on_anatomy_data( + anatomy_data, + root_overrides + ) + + return anatomy_data + + @property + def templates(self): + """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" + return self._templates_obj.templates + + @property + def templates_obj(self): + """Return `AnatomyTemplates` object of current Anatomy instance.""" + return self._templates_obj + + def format(self, *args, **kwargs): + """Wrap `format` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format(*args, **kwargs) + + def format_all(self, *args, **kwargs): + """Wrap `format_all` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format_all(*args, **kwargs) + + @property + def roots(self): + """Wrap `roots` property of Anatomy's `roots_obj`.""" + return self._roots_obj.roots + + @property + def roots_obj(self): + """Return `Roots` object of current Anatomy instance.""" + return self._roots_obj + + def root_environments(self): + """Return AYON_PROJECT_ROOT_* environments for current project.""" + return self._roots_obj.root_environments() + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + return self.roots_obj.root_environmets_fill_data(template) + + def find_root_template_from_path(self, *args, **kwargs): + """Wrapper for Roots `find_root_template_from_path`.""" + return self.roots_obj.find_root_template_from_path(*args, **kwargs) + + def path_remapper(self, *args, **kwargs): + """Wrapper for Roots `path_remapper`.""" + return self.roots_obj.path_remapper(*args, **kwargs) + + def all_root_paths(self): + """Wrapper for Roots `all_root_paths`.""" + return self.roots_obj.all_root_paths() + + def set_root_environments(self): + """Set AYON_PROJECT_ROOT_* environments for current project.""" + self._roots_obj.set_root_environments() + + def root_names(self): + """Return root names for current project.""" + return self.root_names_from_templates(self.templates) + + def _root_keys_from_templates(self, data): + """Extract root key from templates in data. + + Args: + data (dict): Data that may contain templates as string. + + Return: + set: Set of all root names from templates as strings. + + Output example: `{"root[work]", "root[publish]"}` + """ + + output = set() + if isinstance(data, dict): + for value in data.values(): + for root in self._root_keys_from_templates(value): + output.add(root) + + elif isinstance(data, str): + for group in re.findall(self.root_key_regex, data): + output.add(group) + + return output + + def root_value_for_template(self, template): + """Returns value of root key from template.""" + root_templates = [] + for group in re.findall(self.root_key_regex, template): + root_templates.append("{" + group + "}") + + if not root_templates: + return None + + return root_templates[0].format(**{"root": self.roots}) + + def root_names_from_templates(self, templates): + """Extract root names form anatomy templates. + + Returns None if values in templates contain only "{root}". + Empty list is returned if there is no "root" in templates. + Else returns all root names from templates in list. + + RootCombinationError is raised when templates contain both root types, + basic "{root}" and with root name specification "{root[work]}". + + Args: + templates (dict): Anatomy templates where roots are not filled. + + Return: + list/None: List of all root names from templates as strings when + multiroot setup is used, otherwise None is returned. + """ + roots = list(self._root_keys_from_templates(templates)) + # Return empty list if no roots found in templates + if not roots: + return roots + + # Raise exception when root keys have roots with and without root name. + # Invalid output example: ["root", "root[project]", "root[render]"] + if len(roots) > 1 and "root" in roots: + raise RootCombinationError(roots) + + # Return None if "root" without root name in templates + if len(roots) == 1 and roots[0] == "root": + return None + + names = set() + for root in roots: + for group in re.findall(self.root_name_regex, root): + names.add(group) + return list(names) + + def fill_root(self, template_path): + """Fill template path where is only "root" key unfilled. + + Args: + template_path (str): Path with "root" key in. + Example path: "{root}/projects/MyProject/Shot01/Lighting/..." + + Return: + str: formatted path + """ + # NOTE does not care if there are different keys than "root" + return template_path.format(**{"root": self.roots}) + + @classmethod + def fill_root_with_path(cls, rootless_path, root_path): + """Fill path without filled "root" key with passed path. + + This is helper to fill root with different directory path than anatomy + has defined no matter if is single or multiroot. + + Output path is same as input path if `rootless_path` does not contain + unfilled root key. + + Args: + rootless_path (str): Path without filled "root" key. Example: + "{root[work]}/MyProject/..." + root_path (str): What should replace root key in `rootless_path`. + + Returns: + str: Path with filled root. + """ + output = str(rootless_path) + for group in re.findall(cls.root_key_regex, rootless_path): + replacement = "{" + group + "}" + output = output.replace(replacement, root_path) + + return output + + def replace_root_with_env_key(self, filepath, template=None): + """Replace root of path with environment key. + + # Example: + ## Project with roots: + ``` + { + "nas": { + "windows": P:/projects", + ... + } + ... + } + ``` + + ## Entered filepath + "P:/projects/project/folder/task/animation_v001.ma" + + ## Entered template + "<{}>" + + ## Output + "/project/folder/task/animation_v001.ma" + + Args: + filepath (str): Full file path where root should be replaced. + template (str): Optional template for environment key. Must + have one index format key. + Default value if not entered: "${}" + + Returns: + str: Path where root is replaced with environment root key. + + Raise: + ValueError: When project's roots were not found in entered path. + """ + success, rootless_path = self.find_root_template_from_path(filepath) + if not success: + raise ValueError( + "{}: Project's roots were not found in path: {}".format( + self.project_name, filepath + ) + ) + + data = self.root_environmets_fill_data(template) + return rootless_path.format(**data) + + def _project_entity_to_anatomy_data(self, project_entity): + """Convert project document to anatomy data. + + Probably should fill missing keys and values. + """ + + output = copy.deepcopy(project_entity["config"]) + # TODO remove AYON convertion + task_types = copy.deepcopy(project_entity["taskTypes"]) + new_task_types = {} + for task_type in task_types: + name = task_type["name"] + new_task_types[name] = task_type + output["tasks"] = new_task_types + output["attributes"] = copy.deepcopy(project_entity["attrib"]) + + return output + + def _apply_local_settings_on_anatomy_data( + self, anatomy_data, root_overrides + ): + """Apply local settings on anatomy data. + + ATM local settings can modify project roots. Project name is required + as local settings have data stored data by project's name. + + Local settings override root values in this order: + 1.) Check if local settings contain overrides for default project and + apply it's values on roots if there are any. + 2.) If passed `project_name` is not None then check project specific + overrides in local settings for the project and apply it's value on + roots if there are any. + + NOTE: Root values of default project from local settings are always + applied if are set. + + Args: + anatomy_data (dict): Data for anatomy. + root_overrides (dict): Data of local settings. + """ + + # Skip processing if roots for current active site are not available in + # local settings + if not root_overrides: + return + + current_platform = platform.system().lower() + + root_data = anatomy_data["roots"] + for root_name, path in root_overrides.items(): + if root_name not in root_data: + continue + anatomy_data["roots"][root_name][current_platform] = ( + path + ) + + +class CacheItem: + """Helper to cache data. + + Helper does not handle refresh of data and does not mark data as outdated. + Who uses the object should check of outdated state on his own will. + """ + + default_lifetime = 10 + + def __init__(self, lifetime=None): + self._data = None + self._cached = None + self._lifetime = lifetime or self.default_lifetime + + @property + def data(self): + """Cached data/object. + + Returns: + Any: Whatever was cached. + """ + + return self._data + + @property + def is_outdated(self): + """Item has outdated cache. + + Lifetime of cache item expired or was not yet set. + + Returns: + bool: Item is outdated. + """ + + if self._cached is None: + return True + return (time.time() - self._cached) > self._lifetime + + def update_data(self, data): + """Update cache of data. + + Args: + data (Any): Data to cache. + """ + + self._data = data + self._cached = time.time() + + +class Anatomy(BaseAnatomy): + _sync_server_addon_cache = CacheItem() + _project_cache = collections.defaultdict(CacheItem) + _default_site_id_cache = collections.defaultdict(CacheItem) + _root_overrides_cache = collections.defaultdict( + lambda: collections.defaultdict(CacheItem) + ) + + def __init__( + self, project_name=None, site_name=None, project_entity=None + ): + if not project_name: + project_name = os.environ.get("AYON_PROJECT_NAME") + + if not project_name: + raise ProjectNotSet(( + "Implementation bug: Project name is not set. Anatomy requires" + " to load data for specific project." + )) + + if not project_entity: + project_entity = self.get_project_entity_from_cache(project_name) + root_overrides = self._get_site_root_overrides( + project_name, site_name + ) + + super(Anatomy, self).__init__(project_entity, root_overrides) + + @classmethod + def get_project_entity_from_cache(cls, project_name): + project_cache = cls._project_cache[project_name] + if project_cache.is_outdated: + project_cache.update_data(ayon_api.get_project(project_name)) + return copy.deepcopy(project_cache.data) + + @classmethod + def get_sync_server_addon(cls): + if cls._sync_server_addon_cache.is_outdated: + manager = AddonsManager() + cls._sync_server_addon_cache.update_data( + manager.get_enabled_addon("sync_server") + ) + return cls._sync_server_addon_cache.data + + @classmethod + def _get_studio_roots_overrides(cls, project_name): + """This would return 'studio' site override by local settings. + + Notes: + This logic handles local overrides of studio site which may be + available even when sync server is not enabled. + Handling of 'studio' and 'local' site was separated as preparation + for AYON development where that will be received from + separated sources. + + Args: + project_name (str): Name of project. + + Returns: + Union[Dict[str, str], None]): Local root overrides. + """ + if not project_name: + return + return ayon_api.get_project_roots_for_site( + project_name, get_local_site_id() + ) + + @classmethod + def _get_site_root_overrides(cls, project_name, site_name): + """Get root overrides for site. + + Args: + project_name (str): Project name for which root overrides should be + received. + site_name (Union[str, None]): Name of site for which root overrides + should be returned. + """ + + # First check if sync server is available and enabled + sync_server = cls.get_sync_server_addon() + if sync_server is None or not sync_server.enabled: + # QUESTION is ok to force 'studio' when site sync is not enabled? + site_name = "studio" + + elif not site_name: + # Use sync server to receive active site name + project_cache = cls._default_site_id_cache[project_name] + if project_cache.is_outdated: + project_cache.update_data( + sync_server.get_active_site_type(project_name) + ) + site_name = project_cache.data + + site_cache = cls._root_overrides_cache[project_name][site_name] + if site_cache.is_outdated: + if site_name == "studio": + # Handle studio root overrides without sync server + # - studio root overrides can be done even without sync server + roots_overrides = cls._get_studio_roots_overrides( + project_name + ) + else: + # Ask sync server to get roots overrides + roots_overrides = sync_server.get_site_root_overrides( + project_name, site_name + ) + site_cache.update_data(roots_overrides) + return site_cache.data diff --git a/client/ayon_core/pipeline/anatomy/exceptions.py b/client/ayon_core/pipeline/anatomy/exceptions.py new file mode 100644 index 0000000000..39f116baf0 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/exceptions.py @@ -0,0 +1,39 @@ +from ayon_core.lib.path_templates import TemplateUnsolved + + +class ProjectNotSet(Exception): + """Exception raised when is created Anatomy without project name.""" + + +class RootCombinationError(Exception): + """This exception is raised when templates has combined root types.""" + + def __init__(self, roots): + joined_roots = ", ".join( + ["\"{}\"".format(_root) for _root in roots] + ) + # TODO better error message + msg = ( + "Combination of root with and" + " without root name in AnatomyTemplates. {}" + ).format(joined_roots) + + super(RootCombinationError, self).__init__(msg) + + +class TemplateMissingKey(Exception): + """Exception for cases when key does not exist in template.""" + + msg = "Template key '{}' was not found." + + def __init__(self, parents): + parent_join = "".join(["[\"{0}\"]".format(key) for key in parents]) + super(TemplateMissingKey, self).__init__( + self.msg.format(parent_join) + ) + + +class AnatomyTemplateUnsolved(TemplateUnsolved): + """Exception for unsolved template when strict is set to True.""" + + msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py new file mode 100644 index 0000000000..9290a2ca38 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -0,0 +1,534 @@ +import os +import numbers +import platform + +import six + +from ayon_core.lib import Logger +from ayon_core.lib.path_templates import FormatObject + +class RootItem(FormatObject): + """Represents one item or roots. + + Holds raw data of root item specification. Raw data contain value + for each platform, but current platform value is used when object + is used for formatting of template. + + Args: + root_raw_data (dict): Dictionary containing root values by platform + names. ["windows", "linux" and "darwin"] + name (str, optional): Root name which is representing. Used with + multi root setup otherwise None value is expected. + parent_keys (list, optional): All dictionary parent keys. Values of + `parent_keys` are used for get full key which RootItem is + representing. Used for replacing root value in path with + formattable key. e.g. parent_keys == ["work"] -> {root[work]} + parent (object, optional): It is expected to be `Roots` object. + Value of `parent` won't affect code logic much. + """ + + def __init__( + self, root_raw_data, name=None, parent_keys=None, parent=None + ): + super(RootItem, self).__init__() + self._log = None + lowered_platform_keys = {} + for key, value in root_raw_data.items(): + lowered_platform_keys[key.lower()] = value + self.raw_data = lowered_platform_keys + self.cleaned_data = self._clean_roots(lowered_platform_keys) + self.name = name + self.parent_keys = parent_keys or [] + self.parent = parent + + self.available_platforms = list(lowered_platform_keys.keys()) + self.value = lowered_platform_keys.get(platform.system().lower()) + self.clean_value = self.clean_root(self.value) + + def __format__(self, *args, **kwargs): + return self.value.__format__(*args, **kwargs) + + def __str__(self): + return str(self.value) + + def __repr__(self): + return self.__str__() + + def __getitem__(self, key): + if isinstance(key, numbers.Number): + return self.value[key] + + additional_info = "" + if self.parent and self.parent.project_name: + additional_info += " for project \"{}\"".format( + self.parent.project_name + ) + + raise AssertionError( + "Root key \"{}\" is missing{}.".format( + key, additional_info + ) + ) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def full_key(self): + """Full key value for dictionary formatting in template. + + Returns: + str: Return full replacement key for formatting. This helps when + multiple roots are set. In that case e.g. `"root[work]"` is + returned. + """ + if not self.name: + return "root" + + joined_parent_keys = "".join( + ["[{}]".format(key) for key in self.parent_keys] + ) + return "root{}".format(joined_parent_keys) + + def clean_path(self, path): + """Just replace backslashes with forward slashes.""" + return str(path).replace("\\", "/") + + def clean_root(self, root): + """Makes sure root value does not end with slash.""" + if root: + root = self.clean_path(root) + while root.endswith("/"): + root = root[:-1] + return root + + def _clean_roots(self, raw_data): + """Clean all values of raw root item values.""" + cleaned = {} + for key, value in raw_data.items(): + cleaned[key] = self.clean_root(value) + return cleaned + + def path_remapper(self, path, dst_platform=None, src_platform=None): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + cleaned_path = self.clean_path(path) + if dst_platform: + dst_root_clean = self.cleaned_data.get(dst_platform) + if not dst_root_clean: + key_part = "" + full_key = self.full_key() + if full_key != "root": + key_part += "\"{}\" ".format(full_key) + + self.log.warning( + "Root {}miss platform \"{}\" definition.".format( + key_part, dst_platform + ) + ) + return None + + if cleaned_path.startswith(dst_root_clean): + return cleaned_path + + if src_platform: + src_root_clean = self.cleaned_data.get(src_platform) + if src_root_clean is None: + self.log.warning( + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key(), src_platform + ) + ) + return None + + if not cleaned_path.startswith(src_root_clean): + return None + + subpath = cleaned_path[len(src_root_clean):] + if dst_platform: + # `dst_root_clean` is used from upper condition + return dst_root_clean + subpath + return self.clean_value + subpath + + result, template = self.find_root_template_from_path(path) + if not result: + return None + + def parent_dict(keys, value): + if not keys: + return value + + key = keys.pop(0) + return {key: parent_dict(keys, value)} + + if dst_platform: + format_value = parent_dict(list(self.parent_keys), dst_root_clean) + else: + format_value = parent_dict(list(self.parent_keys), self.value) + + return template.format(**{"root": format_value}) + + def find_root_template_from_path(self, path): + """Replaces known root value with formattable key in path. + + All platform values are checked for this replacement. + + Args: + path (str): Path where root value should be found. + + Returns: + tuple: Tuple contain 2 values: `success` (bool) and `path` (str). + When success it True then path should contain replaced root + value with formattable key. + + Example: + When input path is:: + "C:/windows/path/root/projects/my_project/file.ext" + + And raw data of item looks like:: + { + "windows": "C:/windows/path/root", + "linux": "/mount/root" + } + + Output will be:: + (True, "{root}/projects/my_project/file.ext") + + If any of raw data value wouldn't match path's root output is:: + (False, "C:/windows/path/root/projects/my_project/file.ext") + """ + result = False + output = str(path) + + mod_path = self.clean_path(path) + for root_os, root_path in self.cleaned_data.items(): + # Skip empty paths + if not root_path: + continue + + _mod_path = mod_path # reset to original cleaned value + if root_os == "windows": + root_path = root_path.lower() + _mod_path = _mod_path.lower() + + if _mod_path.startswith(root_path): + result = True + replacement = "{" + self.full_key() + "}" + output = replacement + mod_path[len(root_path):] + break + + return (result, output) + + +class Roots: + """Object which should be used for formatting "root" key in templates. + + Args: + anatomy Anatomy: Anatomy object created for a specific project. + """ + + env_prefix = "AYON_PROJECT_ROOT" + roots_filename = "roots.json" + + def __init__(self, anatomy): + self._log = None + self.anatomy = anatomy + self.loaded_project = None + self._roots = None + + def __format__(self, *args, **kwargs): + return self.roots.__format__(*args, **kwargs) + + def __getitem__(self, key): + return self.roots[key] + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def reset(self): + """Reset current roots value.""" + self._roots = None + + def path_remapper( + self, path, dst_platform=None, src_platform=None, roots=None + ): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + if roots is None: + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if "{root" in path: + path = path.format(**{"root": roots}) + # If `dst_platform` is not specified then return else continue. + if not dst_platform: + return path + + if isinstance(roots, RootItem): + return roots.path_remapper(path, dst_platform, src_platform) + + for _root in roots.values(): + result = self.path_remapper( + path, dst_platform, src_platform, _root + ) + if result is not None: + return result + + def find_root_template_from_path(self, path, roots=None): + """Find root value in entered path and replace it with formatting key. + + Args: + path (str): Source path where root will be searched. + roots (Roots/dict, optional): It is possible to use different + roots than instance where method was triggered has. + + Returns: + tuple: Output contains tuple with bool representing success as + first value and path with or without replaced root with + formatting key as second value. + + Raises: + ValueError: When roots are not entered and can't be loaded. + """ + if roots is None: + self.log.debug( + "Looking for matching root in path \"{}\".".format(path) + ) + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if isinstance(roots, RootItem): + return roots.find_root_template_from_path(path) + + for root_name, _root in roots.items(): + success, result = self.find_root_template_from_path(path, _root) + if success: + self.log.info("Found match in root \"{}\".".format(root_name)) + return success, result + + self.log.warning("No matching root was found in current setting.") + return (False, path) + + def set_root_environments(self): + """Set root environments for current project.""" + for key, value in self.root_environments().items(): + os.environ[key] = value + + def root_environments(self): + """Use root keys to create unique keys for environment variables. + + Concatenates prefix "AYON_PROJECT_ROOT_" with root keys to create + unique keys. + + Returns: + dict: Result is `{(str): (str)}` dicitonary where key represents + unique key concatenated by keys and value is root value of + current platform root. + + Example: + With raw root values:: + "work": { + "windows": "P:/projects/work", + "linux": "/mnt/share/projects/work", + "darwin": "/darwin/path/work" + }, + "publish": { + "windows": "P:/projects/publish", + "linux": "/mnt/share/projects/publish", + "darwin": "/darwin/path/publish" + } + + Result on windows platform:: + { + "AYON_PROJECT_ROOT_WORK": "P:/projects/work", + "AYON_PROJECT_ROOT_PUBLISH": "P:/projects/publish" + } + + """ + return self._root_environments() + + def all_root_paths(self, roots=None): + """Return all paths for all roots of all platforms.""" + if roots is None: + roots = self.roots + + output = [] + if isinstance(roots, RootItem): + for value in roots.raw_data.values(): + output.append(value) + return output + + for _roots in roots.values(): + output.extend(self.all_root_paths(_roots)) + return output + + def _root_environments(self, keys=None, roots=None): + if not keys: + keys = [] + if roots is None: + roots = self.roots + + if isinstance(roots, RootItem): + key_items = [self.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + + key = "_".join(key_items) + # Make sure key and value does not contain unicode + # - can happen in Python 2 hosts + return {str(key): str(roots.value)} + + output = {} + for _key, _value in roots.items(): + _keys = list(keys) + _keys.append(_key) + output.update(self._root_environments(_keys, _value)) + return output + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + if template is None: + template = "${}" + return self._root_environmets_fill_data(template) + + def _root_environmets_fill_data(self, template, keys=None, roots=None): + if keys is None and roots is None: + return { + "root": self._root_environmets_fill_data( + template, [], self.roots + ) + } + + if isinstance(roots, RootItem): + key_items = [Roots.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + key = "_".join(key_items) + return template.format(key) + + output = {} + for key, value in roots.items(): + _keys = list(keys) + _keys.append(key) + output[key] = self._root_environmets_fill_data( + template, _keys, value + ) + return output + + @property + def project_name(self): + """Return project name which will be used for loading root values.""" + return self.anatomy.project_name + + @property + def roots(self): + """Property for filling "root" key in templates. + + This property returns roots for current project or default root values. + Warning: + Default roots value may cause issues when project use different + roots settings. That may happen when project use multiroot + templates but default roots miss their keys. + """ + if self.project_name != self.loaded_project: + self._roots = None + + if self._roots is None: + self._roots = self._discover() + self.loaded_project = self.project_name + return self._roots + + def _discover(self): + """ Loads current project's roots or default. + + Default roots are loaded if project override's does not contain roots. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + + return self._parse_dict(self.anatomy["roots"], parent=self) + + @staticmethod + def _parse_dict(data, key=None, parent_keys=None, parent=None): + """Parse roots raw data into RootItem or dictionary with RootItems. + + Converting raw roots data to `RootItem` helps to handle platform keys. + This method is recursive to be able handle multiroot setup and + is static to be able to load default roots without creating new object. + + Args: + data (dict): Should contain raw roots data to be parsed. + key (str, optional): Current root key. Set by recursion. + parent_keys (list): Parent dictionary keys. Set by recursion. + parent (Roots, optional): Parent object set in `RootItem` + helps to keep RootItem instance updated with `Roots` object. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + if not parent_keys: + parent_keys = [] + is_last = False + for value in data.values(): + if isinstance(value, six.string_types): + is_last = True + break + + if is_last: + return RootItem(data, key, parent_keys, parent=parent) + + output = {} + for _key, value in data.items(): + _parent_keys = list(parent_keys) + _parent_keys.append(_key) + output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) + return output diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py new file mode 100644 index 0000000000..1eee5b0945 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -0,0 +1,523 @@ +import os +import copy +import re +import collections +import numbers + +import six + +from ayon_core.lib import Logger +from ayon_core.lib.path_templates import ( + TemplateResult, + StringTemplate, + TemplatesDict, +) + +from .exceptions import AnatomyTemplateUnsolved +from .roots import RootItem + + +class AnatomyTemplateResult(TemplateResult): + rootless = None + + def __new__(cls, result, rootless_path): + new_obj = super(AnatomyTemplateResult, cls).__new__( + cls, + str(result), + result.template, + result.solved, + result.used_values, + result.missing_keys, + result.invalid_types + ) + new_obj.rootless = rootless_path + return new_obj + + def validate(self): + if not self.solved: + raise AnatomyTemplateUnsolved( + self.template, + self.missing_keys, + self.invalid_types + ) + + def copy(self): + tmp = TemplateResult( + str(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + def normalized(self): + """Convert to normalized path.""" + + tmp = TemplateResult( + os.path.normpath(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + +class AnatomyStringTemplate(StringTemplate): + """String template which has access to anatomy.""" + + def __init__(self, anatomy_templates, template): + self.anatomy_templates = anatomy_templates + super(AnatomyStringTemplate, self).__init__(template) + + def format(self, data): + """Format template and add 'root' key to data if not available. + + Args: + data (dict[str, Any]): Formatting data for template. + + Returns: + AnatomyTemplateResult: Formatting result. + """ + + anatomy_templates = self.anatomy_templates + if not data.get("root"): + data = copy.deepcopy(data) + data["root"] = anatomy_templates.anatomy.roots + result = StringTemplate.format(self, data) + rootless_path = anatomy_templates.rootless_path_from_result(result) + return AnatomyTemplateResult(result, rootless_path) + + +class AnatomyTemplates(TemplatesDict): + inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") + inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") + + def __init__(self, anatomy): + self._log = Logger.get_logger(self.__class__.__name__) + super(AnatomyTemplates, self).__init__() + self.anatomy = anatomy + self.loaded_project = None + + def reset(self): + self._raw_templates = None + self._templates = None + self._objected_templates = None + + @property + def project_name(self): + return self.anatomy.project_name + + @property + def roots(self): + return self.anatomy.roots + + @property + def templates(self): + self._validate_discovery() + return self._templates + + @property + def objected_templates(self): + self._validate_discovery() + return self._objected_templates + + def _validate_discovery(self): + if self.project_name != self.loaded_project: + self.reset() + + if self._templates is None: + self._discover() + self.loaded_project = self.project_name + + def _format_value(self, value, data): + if isinstance(value, RootItem): + return self._solve_dict(value, data) + return super(AnatomyTemplates, self)._format_value(value, data) + + @staticmethod + def _ayon_template_conversion(templates): + def _convert_template_item(template_item): + # Change 'directory' to 'folder' + if "directory" in template_item: + template_item["folder"] = template_item["directory"] + + if ( + "path" not in template_item + and "file" in template_item + and "folder" in template_item + ): + template_item["path"] = "/".join( + (template_item["folder"], template_item["file"]) + ) + + def _get_default_template_name(templates): + default_template = None + for name, template in templates.items(): + if name == "default": + return "default" + + if default_template is None: + default_template = name + + return default_template + + def _fill_template_category(templates, cat_templates, cat_key): + default_template_name = _get_default_template_name(cat_templates) + for template_name, cat_template in cat_templates.items(): + _convert_template_item(cat_template) + if template_name == default_template_name: + templates[cat_key] = cat_template + else: + new_name = "{}_{}".format(cat_key, template_name) + templates["others"][new_name] = cat_template + + others_templates = templates.pop("others", None) or {} + new_others_templates = {} + templates["others"] = new_others_templates + for name, template in others_templates.items(): + _convert_template_item(template) + new_others_templates[name] = template + + for key in ( + "work", + "publish", + "hero", + ): + cat_templates = templates.pop(key) + _fill_template_category(templates, cat_templates, key) + + delivery_templates = templates.pop("delivery", None) or {} + new_delivery_templates = {} + for name, delivery_template in delivery_templates.items(): + new_delivery_templates[name] = "/".join( + (delivery_template["directory"], delivery_template["file"]) + ) + templates["delivery"] = new_delivery_templates + + def set_templates(self, templates): + if not templates: + self.reset() + return + + templates = copy.deepcopy(templates) + # TODO remove AYON convertion + self._ayon_template_conversion(templates) + + self._raw_templates = copy.deepcopy(templates) + v_queue = collections.deque() + v_queue.append(templates) + while v_queue: + item = v_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + v_queue.append(value) + + elif ( + isinstance(value, six.string_types) + and "{task}" in value + ): + item[key] = value.replace("{task}", "{task[name]}") + + solved_templates = self.solve_template_inner_links(templates) + self._templates = solved_templates + self._objected_templates = self.create_objected_templates( + solved_templates + ) + + def _create_template_object(self, template): + return AnatomyStringTemplate(self, template) + + def default_templates(self): + """Return default templates data with solved inner keys.""" + return self.solve_template_inner_links( + self.anatomy["templates"] + ) + + def _discover(self): + """ Loads anatomy templates from yaml. + Default templates are loaded if project is not set or project does + not have set it's own. + TODO: create templates if not exist. + + Returns: + TemplatesResultDict: Contain templates data for current project of + default templates. + """ + + if self.project_name is None: + # QUESTION create project specific if not found? + raise AssertionError(( + "Project \"{0}\" does not have his own templates." + " Trying to use default." + ).format(self.project_name)) + + self.set_templates(self.anatomy["templates"]) + + @classmethod + def replace_inner_keys(cls, matches, value, key_values, key): + """Replacement of inner keys in template values.""" + for match in matches: + anatomy_sub_keys = ( + cls.inner_key_name_pattern.findall(match) + ) + if key in anatomy_sub_keys: + raise ValueError(( + "Unsolvable recursion in inner keys, " + "key: \"{}\" is in his own value." + " Can't determine source, please check Anatomy templates." + ).format(key)) + + for anatomy_sub_key in anatomy_sub_keys: + replace_value = key_values.get(anatomy_sub_key) + if replace_value is None: + raise KeyError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`." + ).format(key, anatomy_sub_key)) + + if not ( + isinstance(replace_value, numbers.Number) + or isinstance(replace_value, six.string_types) + ): + raise ValueError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`" + " with value `{2}`." + ).format(key, anatomy_sub_key, str(replace_value))) + + value = value.replace(match, str(replace_value)) + + return value + + @classmethod + def prepare_inner_keys(cls, key_values): + """Check values of inner keys. + + Check if inner key exist in template group and has valid value. + It is also required to avoid infinite loop with unsolvable recursion + when first inner key's value refers to second inner key's value where + first is used. + """ + keys_to_solve = set(key_values.keys()) + while True: + found = False + for key in tuple(keys_to_solve): + value = key_values[key] + + if isinstance(value, six.string_types): + matches = cls.inner_key_pattern.findall(value) + if not matches: + keys_to_solve.remove(key) + continue + + found = True + key_values[key] = cls.replace_inner_keys( + matches, value, key_values, key + ) + continue + + elif not isinstance(value, dict): + keys_to_solve.remove(key) + continue + + subdict_found = False + for _key, _value in tuple(value.items()): + matches = cls.inner_key_pattern.findall(_value) + if not matches: + continue + + subdict_found = True + found = True + key_values[key][_key] = cls.replace_inner_keys( + matches, _value, key_values, + "{}.{}".format(key, _key) + ) + + if not subdict_found: + keys_to_solve.remove(key) + + if not found: + break + + return key_values + + @classmethod + def solve_template_inner_links(cls, templates): + """Solve templates inner keys identified by "{@*}". + + Process is split into 2 parts. + First is collecting all global keys (keys in top hierarchy where value + is not dictionary). All global keys are set for all group keys (keys + in top hierarchy where value is dictionary). Value of a key is not + overridden in group if already contain value for the key. + + In second part all keys with "at" symbol in value are replaced with + value of the key afterward "at" symbol from the group. + + Args: + templates (dict): Raw templates data. + + Example: + templates:: + key_1: "value_1", + key_2: "{@key_1}/{filling_key}" + + group_1: + key_3: "value_3/{@key_2}" + + group_2: + key_2": "value_2" + key_4": "value_4/{@key_2}" + + output:: + key_1: "value_1" + key_2: "value_1/{filling_key}" + + group_1: { + key_1: "value_1" + key_2: "value_1/{filling_key}" + key_3: "value_3/value_1/{filling_key}" + + group_2: { + key_1: "value_1" + key_2: "value_2" + key_4: "value_3/value_2" + """ + default_key_values = templates.pop("common", {}) + for key, value in tuple(templates.items()): + if isinstance(value, dict): + continue + default_key_values[key] = templates.pop(key) + + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + + keys_by_subkey = {} + for sub_key, sub_value in templates.items(): + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + for sub_key, sub_value in other_templates.items(): + if sub_key in keys_by_subkey: + self.log.warning(( + "Key \"{}\" is duplicated in others. Skipping." + ).format(sub_key)) + continue + + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) + + for key, value in default_keys_by_subkeys.items(): + keys_by_subkey[key] = value + + return keys_by_subkey + + @classmethod + def _dict_to_subkeys_list(cls, subdict, pre_keys=None): + if pre_keys is None: + pre_keys = [] + output = [] + for key in subdict: + value = subdict[key] + result = list(pre_keys) + result.append(key) + if isinstance(value, dict): + for item in cls._dict_to_subkeys_list(value, result): + output.append(item) + else: + output.append(result) + return output + + def _keys_to_dicts(self, key_list, value): + if not key_list: + return None + if len(key_list) == 1: + return {key_list[0]: value} + return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} + + @classmethod + def rootless_path_from_result(cls, result): + """Calculate rootless path from formatting result. + + Args: + result (TemplateResult): Result of StringTemplate formatting. + + Returns: + str: Rootless path if result contains one of anatomy roots. + """ + + used_values = result.used_values + missing_keys = result.missing_keys + template = result.template + invalid_types = result.invalid_types + if ( + "root" not in used_values + or "root" in missing_keys + or "{root" not in template + ): + return + + for invalid_type in invalid_types: + if "root" in invalid_type: + return + + root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) + if not root_keys: + return + + output = str(result) + for used_root_keys in root_keys: + if not used_root_keys: + continue + + used_value = used_values + root_key = None + for key in used_root_keys: + used_value = used_value[key] + if root_key is None: + root_key = key + else: + root_key += "[{}]".format(key) + + root_key = "{" + root_key + "}" + output = output.replace(str(used_value), root_key) + + return output + + def format(self, data, strict=True): + copy_data = copy.deepcopy(data) + roots = self.roots + if roots: + copy_data["root"] = roots + result = super(AnatomyTemplates, self).format(copy_data) + result.strict = strict + return result + + def format_all(self, in_data, only_keys=True): + """ Solves templates based on entered data. + + Args: + data (dict): Containing keys to be filled into template. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + """ + return self.format(in_data, strict=False) \ No newline at end of file From 2e24e1c4254650d310e462f0a263a3260835bd64 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:31:52 +0100 Subject: [PATCH 012/284] added some helper classes --- .../ayon_core/pipeline/anatomy/templates.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 1eee5b0945..565106a23e 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -92,6 +92,142 @@ class AnatomyStringTemplate(StringTemplate): return AnatomyTemplateResult(result, rootless_path) +class TemplateItem: + """Template item under template category. + + This item data usually contains 'file' and 'directory' by anatomy + definition, enhanced by common data ('frame_padding', + 'version_padding'). It adds 'path' key which is combination of + 'file' and 'directory' values. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + template_data (dict[str, Any]): Templates data. + + """ + def __init__(self, anatomy_templates, template_data): + template_data = copy.deepcopy(template_data) + + # Backwards compatibility for 'folder' + # TODO remove when deprecation not needed anymore + if ( + "folder" not in template_data + and "directory" in template_data + ): + template_data["folder"] = template_data["directory"] + + # Add 'path' key + if ( + "path" not in template_data + and "file" in template_data + and "directory" in template_data + ): + template_data["path"] = "/".join( + (template_data["directory"], template_data["file"]) + ) + + for key, value in template_data.items(): + if isinstance(value, str): + value = AnatomyStringTemplate(anatomy_templates, value) + template_data[key] = value + + self._template_data = template_data + self._anatomy_templates = anatomy_templates + + def __getitem__(self, key): + return self._template_data[key] + + def get(self, key, default=None): + return self._template_data.get(key, default) + + def format(self, data, strict=True): + output = {} + for key, value in self._template_data.items(): + if isinstance(value, AnatomyStringTemplate): + value = value.format(data) + output[key] = value + return output + + +class TemplateCategory: + """Template category. + + Template category groups template items for specific usage. Categories + available at the moment are 'work', 'publish', 'hero', 'delivery', + 'staging' and 'others'. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + category_name (str): Category name. + category_data (dict[str, Any]): Category data. + + """ + def __init__(self, anatomy_templates, category_name, category_data): + for key, value in category_data.items(): + if isinstance(value, dict): + value = TemplateItem(anatomy_templates, value) + elif isinstance(value, str): + value = AnatomyStringTemplate(anatomy_templates, value) + category_data[key] = value + self._name = category_name + self._name_prefix = "{}_".format(category_name) + self._category_data = category_data + + def __getitem__(self, key): + new_key = self._convert_getter_key(key) + return self._category_data[new_key] + + def get(self, key, default=None): + new_key = self._convert_getter_key(key) + return self._category_data.get(new_key, default) + + @property + def name(self): + """Category name. + + Returns: + str: Category name. + + """ + return self._name + + def format(self, data, strict=True): + output = {} + for key, value in self._category_data.items(): + if isinstance(value, TemplateItem): + value = value.format(data, strict) + elif isinstance(value, AnatomyStringTemplate): + value = value.format(data) + + output[key] = value + return output + + def _convert_getter_key(self, key): + """Convert key for backwards compatibility. + + OpenPype compatible settings did contain template keys prefixed by + category name e.g. 'publish_render' which should be just 'render'. + + This method keeps the backwards compatibility but only if the key + starts with the category name prefix and the key is available in + roots. + + Args: + key (str): Key to be converted. + + Returns: + str: Converted string. + + """ + if key in self._category_data: + return key + if key.startswith(self._name_prefix): + new_key = key[len(self._name_prefix):] + if new_key in self._category_data: + return new_key + return key + + class AnatomyTemplates(TemplatesDict): inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") From df23e27f91ce4c79845d6f1c68e05591aca90cf1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:40:56 +0100 Subject: [PATCH 013/284] added some helper methods --- .../ayon_core/pipeline/anatomy/templates.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 565106a23e..f8dd8179ae 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -388,6 +388,77 @@ class AnatomyTemplates(TemplatesDict): default templates. """ + @property + def frame_padding(self): + """Default frame padding. + + Returns: + int: Frame padding used by default in templates. + + """ + self._validate_discovery() + return self["frame_padding"] + + @property + def version_padding(self): + """Default version padding. + + Returns: + int: Version padding used by default in templates. + + """ + self._validate_discovery() + return self["version_padding"] + + @classmethod + def get_rootless_path_from_result(cls, result): + """Calculate rootless path from formatting result. + + Args: + result (TemplateResult): Result of StringTemplate formatting. + + Returns: + str: Rootless path if result contains one of anatomy roots. + """ + + used_values = result.used_values + missing_keys = result.missing_keys + template = result.template + invalid_types = result.invalid_types + if ( + "root" not in used_values + or "root" in missing_keys + or "{root" not in template + ): + return + + for invalid_type in invalid_types: + if "root" in invalid_type: + return + + root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) + if not root_keys: + return + + output = str(result) + for used_root_keys in root_keys: + if not used_root_keys: + continue + + used_value = used_values + root_key = None + for key in used_root_keys: + used_value = used_value[key] + if root_key is None: + root_key = key + else: + root_key += "[{}]".format(key) + + root_key = "{" + root_key + "}" + output = output.replace(str(used_value), root_key) + + return output + if self.project_name is None: # QUESTION create project specific if not found? raise AssertionError(( From f2f11e444efe2a8c54b85205ee7fa6f2c8298e5c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:42:51 +0100 Subject: [PATCH 014/284] modified templates --- .../ayon_core/pipeline/anatomy/templates.py | 687 +++++++++++------- 1 file changed, 408 insertions(+), 279 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index f8dd8179ae..63dfd9b1b0 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -1,19 +1,19 @@ import os -import copy import re +import copy import collections import numbers -import six - -from ayon_core.lib import Logger from ayon_core.lib.path_templates import ( TemplateResult, StringTemplate, - TemplatesDict, ) -from .exceptions import AnatomyTemplateUnsolved +from .exceptions import ( + ProjectNotSet, + TemplateMissingKey, + AnatomyTemplateUnsolved, +) from .roots import RootItem @@ -67,7 +67,12 @@ class AnatomyTemplateResult(TemplateResult): class AnatomyStringTemplate(StringTemplate): - """String template which has access to anatomy.""" + """String template which has access to anatomy. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + template (str): Template string. + """ def __init__(self, anatomy_templates, template): self.anatomy_templates = anatomy_templates @@ -88,10 +93,154 @@ class AnatomyStringTemplate(StringTemplate): data = copy.deepcopy(data) data["root"] = anatomy_templates.anatomy.roots result = StringTemplate.format(self, data) - rootless_path = anatomy_templates.rootless_path_from_result(result) + rootless_path = anatomy_templates.get_rootless_path_from_result( + result + ) return AnatomyTemplateResult(result, rootless_path) +def _merge_dict(main_dict, enhance_dict): + """Merges dictionaries by keys. + + Function call itself if value on key is again dictionary. + + Args: + main_dict (dict): First dict to merge second one into. + enhance_dict (dict): Second dict to be merged. + + Returns: + dict: Merged result. + + .. note:: does not override whole value on first found key + but only values differences from enhance_dict + + """ + + merge_queue = collections.deque() + merge_queue.append((main_dict, enhance_dict)) + while merge_queue: + queue_item = merge_queue.popleft() + l_dict, r_dict = queue_item + + for key, value in r_dict.items(): + if key not in l_dict: + l_dict[key] = value + elif isinstance(value, dict) and isinstance(l_dict[key], dict): + merge_queue.append((l_dict[key], value)) + else: + l_dict[key] = value + return main_dict + + +class TemplatesResultDict(dict): + """Holds and wrap 'AnatomyTemplateResult' for easy bug report. + + Dictionary like object which holds 'AnatomyTemplateResult' in the same + data structure as base dictionary of anatomy templates. It can raise + + """ + + def __init__(self, in_data, key=None, parent=None, strict=None): + super(TemplatesResultDict, self).__init__() + for _key, _value in in_data.items(): + if isinstance(_value, TemplatesResultDict): + _value.parent = self + elif isinstance(_value, dict): + _value = self.__class__(_value, _key, self) + self[_key] = _value + + if strict is None and parent is None: + strict = True + + self.key = key + self.parent = parent + self._is_strict = strict + + def __getitem__(self, key): + if key not in self.keys(): + hier = self.get_hierarchy() + hier.append(key) + raise TemplateMissingKey(hier) + + value = super(TemplatesResultDict, self).__getitem__(key) + if isinstance(value, self.__class__): + return value + + # Raise exception when expected solved templates and it is not. + if self.is_strict and hasattr(value, "validate"): + value.validate() + return value + + def get_is_strict(self): + return self._is_strict + + def set_is_strict(self, is_strict): + if is_strict is None and self.parent is None: + is_strict = True + self._is_strict = is_strict + for child in self.values(): + if isinstance(child, self.__class__): + child.set_is_strict(is_strict) + elif isinstance(child, AnatomyTemplateResult): + child.strict = is_strict + + strict = property(get_is_strict, set_is_strict) + is_strict = property(get_is_strict, set_is_strict) + + def get_hierarchy(self): + """Return dictionary keys one by one to root parent.""" + if self.key is None: + return [] + + if self.parent is None: + return [self.key] + + par_hier = list(self.parent.get_hierarchy()) + par_hier.append(self.key) + return par_hier + + @property + def missing_keys(self): + """Return missing keys of all children templates.""" + missing_keys = set() + for value in self.values(): + missing_keys |= value.missing_keys + return missing_keys + + @property + def invalid_types(self): + """Return invalid types of all children templates.""" + invalid_types = {} + for value in self.values(): + invalid_types = _merge_dict(invalid_types, value.invalid_types) + return invalid_types + + @property + def used_values(self): + """Return used values for all children templates.""" + used_values = {} + for value in self.values(): + used_values = _merge_dict(used_values, value.used_values) + return used_values + + def get_solved(self): + """Get only solved key from templates.""" + result = {} + for key, value in self.items(): + if isinstance(value, self.__class__): + value = value.get_solved() + if not value: + continue + result[key] = value + + elif ( + not hasattr(value, "solved") or + value.solved + ): + result[key] = value + return self.__class__(result, key=self.key, parent=self.parent) + + class TemplateItem: """Template item under template category. @@ -146,7 +295,7 @@ class TemplateItem: if isinstance(value, AnatomyStringTemplate): value = value.format(data) output[key] = value - return output + return TemplatesResultDict(output, strict=strict) class TemplateCategory: @@ -199,8 +348,10 @@ class TemplateCategory: elif isinstance(value, AnatomyStringTemplate): value = value.format(data) + if isinstance(value, TemplatesResultDict): + value.key = key output[key] = value - return output + return TemplatesResultDict(output, key=self.name, strict=strict) def _convert_getter_key(self, key): """Convert key for backwards compatibility. @@ -229,164 +380,76 @@ class TemplateCategory: class AnatomyTemplates(TemplatesDict): +class AnatomyTemplates: inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") def __init__(self, anatomy): - self._log = Logger.get_logger(self.__class__.__name__) - super(AnatomyTemplates, self).__init__() - self.anatomy = anatomy - self.loaded_project = None + self._anatomy = anatomy + + self._loaded_project = None + self._raw_templates = None + self._templates = None + self._objected_templates = None + + def __getitem__(self, key): + self._validate_discovery() + return self._objected_templates[key] + + def get(self, key, default=None): + self._validate_discovery() + return self._objected_templates.get(key, default) + + def keys(self): + return self._objected_templates.keys() def reset(self): self._raw_templates = None self._templates = None self._objected_templates = None + @property + def anatomy(self): + """Anatomy instance. + + Returns: + Anatomy: Anatomy instance. + + """ + return self._anatomy + @property def project_name(self): - return self.anatomy.project_name + """Project name. + + Returns: + Union[str, None]: Project name if set, otherwise None. + + """ + return self._anatomy.project_name @property def roots(self): - return self.anatomy.roots + """Anatomy roots object. + + Returns: + RootItem: Anatomy roots data. + + """ + return self._anatomy.roots @property def templates(self): - self._validate_discovery() - return self._templates + """Templates data. - @property - def objected_templates(self): - self._validate_discovery() - return self._objected_templates - - def _validate_discovery(self): - if self.project_name != self.loaded_project: - self.reset() - - if self._templates is None: - self._discover() - self.loaded_project = self.project_name - - def _format_value(self, value, data): - if isinstance(value, RootItem): - return self._solve_dict(value, data) - return super(AnatomyTemplates, self)._format_value(value, data) - - @staticmethod - def _ayon_template_conversion(templates): - def _convert_template_item(template_item): - # Change 'directory' to 'folder' - if "directory" in template_item: - template_item["folder"] = template_item["directory"] - - if ( - "path" not in template_item - and "file" in template_item - and "folder" in template_item - ): - template_item["path"] = "/".join( - (template_item["folder"], template_item["file"]) - ) - - def _get_default_template_name(templates): - default_template = None - for name, template in templates.items(): - if name == "default": - return "default" - - if default_template is None: - default_template = name - - return default_template - - def _fill_template_category(templates, cat_templates, cat_key): - default_template_name = _get_default_template_name(cat_templates) - for template_name, cat_template in cat_templates.items(): - _convert_template_item(cat_template) - if template_name == default_template_name: - templates[cat_key] = cat_template - else: - new_name = "{}_{}".format(cat_key, template_name) - templates["others"][new_name] = cat_template - - others_templates = templates.pop("others", None) or {} - new_others_templates = {} - templates["others"] = new_others_templates - for name, template in others_templates.items(): - _convert_template_item(template) - new_others_templates[name] = template - - for key in ( - "work", - "publish", - "hero", - ): - cat_templates = templates.pop(key) - _fill_template_category(templates, cat_templates, key) - - delivery_templates = templates.pop("delivery", None) or {} - new_delivery_templates = {} - for name, delivery_template in delivery_templates.items(): - new_delivery_templates[name] = "/".join( - (delivery_template["directory"], delivery_template["file"]) - ) - templates["delivery"] = new_delivery_templates - - def set_templates(self, templates): - if not templates: - self.reset() - return - - templates = copy.deepcopy(templates) - # TODO remove AYON convertion - self._ayon_template_conversion(templates) - - self._raw_templates = copy.deepcopy(templates) - v_queue = collections.deque() - v_queue.append(templates) - while v_queue: - item = v_queue.popleft() - if not isinstance(item, dict): - continue - - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, dict): - v_queue.append(value) - - elif ( - isinstance(value, six.string_types) - and "{task}" in value - ): - item[key] = value.replace("{task}", "{task[name]}") - - solved_templates = self.solve_template_inner_links(templates) - self._templates = solved_templates - self._objected_templates = self.create_objected_templates( - solved_templates - ) - - def _create_template_object(self, template): - return AnatomyStringTemplate(self, template) - - def default_templates(self): - """Return default templates data with solved inner keys.""" - return self.solve_template_inner_links( - self.anatomy["templates"] - ) - - def _discover(self): - """ Loads anatomy templates from yaml. - Default templates are loaded if project is not set or project does - not have set it's own. - TODO: create templates if not exist. + Templates data with replaced common data. Returns: - TemplatesResultDict: Contain templates data for current project of - default templates. + dict[str, Any]: Templates data. + """ + self._validate_discovery() + return self._templates @property def frame_padding(self): @@ -459,17 +522,152 @@ class AnatomyTemplates(TemplatesDict): return output - if self.project_name is None: - # QUESTION create project specific if not found? - raise AssertionError(( - "Project \"{0}\" does not have his own templates." - " Trying to use default." - ).format(self.project_name)) + def format(self, data, strict=True): + """Fill all templates based on entered data. - self.set_templates(self.anatomy["templates"]) + Args: + data (dict[str, Any]): Fill data used for template formatting. + strict (Optional[bool]): Raise exception is accessed value is + not fully filled. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + + """ + self._validate_discovery() + copy_data = copy.deepcopy(data) + roots = self._anatomy.roots + if roots: + copy_data["root"] = roots + + return self._solve_dict(copy_data, strict) + + def format_all(self, in_data): + """Fill all templates based on entered data. + + Deprecated: + Use `format` method with `strict=False` instead. + + Args: + in_data (dict): Containing keys to be filled into template. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + + """ + return self.format(in_data, strict=False) + + def get_template(self, category_name, template_name, subkey=None): + """Get template item from category. + + Args: + category_name (str): Category name. + template_name (str): Template name. + subkey (Optional[str]): Subkey name. + + Returns: + Any: Template item or subkey value. + + """ + self._validate_discovery() + category = self.get(category_name) + if category is None: + return None + + template_item = category.get(template_name) + if template_item is None: + return template_item + + if subkey is None: + return template_item + + return template_item.get(subkey) + + def _solve_dict(self, data, strict): + """ Solves templates with entered data. + + Args: + data (dict): Containing keys to be filled into template. + + Returns: + dict: With `TemplateResult` in values containing filled or + partially filled templates. + + """ + output = {} + for key, value in self._objected_templates.items(): + if isinstance(value, TemplateCategory): + value = value.format(data, strict) + elif isinstance(value, AnatomyStringTemplate): + value = value.format(data) + output[key] = value + return TemplatesResultDict(output, strict=strict) + + def _validate_discovery(self): + """Validate if templates are discovered and loaded for anatomy project. + + When project changes the cached data are reset and discovered again. + """ + if self.project_name != self._loaded_project: + self.reset() + + if self._templates is None: + self._discover() + self._loaded_project = self.project_name + + def _create_objected_templates(self, templates): + """Create objected templates from templates data. + + Args: + templates (dict[str, Any]): Templates data from project entity. + + Returns: + dict[str, Any]: Values are cnmverted to template objects. + + """ + objected_templates = {} + for category_name, category_value in copy.deepcopy(templates).items(): + if isinstance(category_value, dict): + category_value = TemplateCategory( + self, category_name, category_value + ) + elif isinstance(category_value, str): + category_value = AnatomyStringTemplate(self, category_value) + objected_templates[category_name] = category_value + return objected_templates + + def _discover(self): + """Load and cache templates from project entity.""" + if self.project_name is None: + raise ProjectNotSet("Anatomy project is not set.") + + templates = self.anatomy["templates"] + self._raw_templates = copy.deepcopy(templates) + + templates = copy.deepcopy(templates) + # Make sure all the keys are available + for key in ( + "publish", + "hero", + "work", + "delivery", + "staging", + "others", + ): + templates.setdefault(key, {}) + + solved_templates = self._solve_template_inner_links(templates) + self._templates = solved_templates + self._objected_templates = self._create_objected_templates( + solved_templates + ) @classmethod - def replace_inner_keys(cls, matches, value, key_values, key): + def _replace_inner_keys(cls, matches, value, key_values, key): """Replacement of inner keys in template values.""" for match in matches: anatomy_sub_keys = ( @@ -493,7 +691,7 @@ class AnatomyTemplates(TemplatesDict): if not ( isinstance(replace_value, numbers.Number) - or isinstance(replace_value, six.string_types) + or isinstance(replace_value, str) ): raise ValueError(( "Anatomy templates can't be filled." @@ -507,7 +705,7 @@ class AnatomyTemplates(TemplatesDict): return value @classmethod - def prepare_inner_keys(cls, key_values): + def _prepare_inner_keys(cls, key_values): """Check values of inner keys. Check if inner key exist in template group and has valid value. @@ -521,14 +719,14 @@ class AnatomyTemplates(TemplatesDict): for key in tuple(keys_to_solve): value = key_values[key] - if isinstance(value, six.string_types): + if isinstance(value, str): matches = cls.inner_key_pattern.findall(value) if not matches: keys_to_solve.remove(key) continue found = True - key_values[key] = cls.replace_inner_keys( + key_values[key] = cls._replace_inner_keys( matches, value, key_values, key ) continue @@ -545,7 +743,7 @@ class AnatomyTemplates(TemplatesDict): subdict_found = True found = True - key_values[key][_key] = cls.replace_inner_keys( + key_values[key][_key] = cls._replace_inner_keys( matches, _value, key_values, "{}.{}".format(key, _key) ) @@ -559,7 +757,7 @@ class AnatomyTemplates(TemplatesDict): return key_values @classmethod - def solve_template_inner_links(cls, templates): + def _solve_template_inner_links(cls, templates): """Solve templates inner keys identified by "{@*}". Process is split into 2 parts. @@ -599,132 +797,63 @@ class AnatomyTemplates(TemplatesDict): key_1: "value_1" key_2: "value_2" key_4: "value_3/value_2" + + Returns: + dict[str, Any]: Solved templates data. + """ default_key_values = templates.pop("common", {}) - for key, value in tuple(templates.items()): - if isinstance(value, dict): - continue - default_key_values[key] = templates.pop(key) - - # Pop "others" key before before expected keys are processed - other_templates = templates.pop("others") or {} - - keys_by_subkey = {} - for sub_key, sub_value in templates.items(): - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - for sub_key, sub_value in other_templates.items(): - if sub_key in keys_by_subkey: - self.log.warning(( - "Key \"{}\" is duplicated in others. Skipping." - ).format(sub_key)) - continue - - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) + output = {} + for category_name, category_value in templates.items(): + new_category_value = {} + for key, value in category_value.items(): + key_values = copy.deepcopy(default_key_values) + key_values.update(value) + new_category_value[key] = cls._prepare_inner_keys(key_values) + output[category_name] = new_category_value + default_keys_by_subkeys = cls._prepare_inner_keys(default_key_values) for key, value in default_keys_by_subkeys.items(): - keys_by_subkey[key] = value + output[key] = value - return keys_by_subkey + return output @classmethod - def _dict_to_subkeys_list(cls, subdict, pre_keys=None): - if pre_keys is None: - pre_keys = [] + def _dict_to_subkeys_list(cls, subdict): + """Convert dictionary to list of subkeys. + + Example:: + + _dict_to_subkeys_list({ + "root": { + "work": "path/to/work", + "publish": "path/to/publish" + } + }) + [ + ["root", "work"], + ["root", "publish"] + ] + + + Args: + dict[str, Any]: Dictionary to be converted. + + Returns: + list[list[str]]: List of subkeys. + + """ output = [] - for key in subdict: - value = subdict[key] - result = list(pre_keys) - result.append(key) - if isinstance(value, dict): - for item in cls._dict_to_subkeys_list(value, result): - output.append(item) - else: - output.append(result) - return output - - def _keys_to_dicts(self, key_list, value): - if not key_list: - return None - if len(key_list) == 1: - return {key_list[0]: value} - return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} - - @classmethod - def rootless_path_from_result(cls, result): - """Calculate rootless path from formatting result. - - Args: - result (TemplateResult): Result of StringTemplate formatting. - - Returns: - str: Rootless path if result contains one of anatomy roots. - """ - - used_values = result.used_values - missing_keys = result.missing_keys - template = result.template - invalid_types = result.invalid_types - if ( - "root" not in used_values - or "root" in missing_keys - or "{root" not in template - ): - return - - for invalid_type in invalid_types: - if "root" in invalid_type: - return - - root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) - if not root_keys: - return - - output = str(result) - for used_root_keys in root_keys: - if not used_root_keys: - continue - - used_value = used_values - root_key = None - for key in used_root_keys: - used_value = used_value[key] - if root_key is None: - root_key = key + subkey_queue = collections.deque() + subkey_queue.append((subdict, [])) + while subkey_queue: + queue_item = subkey_queue.popleft() + data, pre_keys = queue_item + for key, value in data.items(): + result = list(pre_keys) + result.append(key) + if isinstance(value, dict): + subkey_queue.append((value, result)) else: - root_key += "[{}]".format(key) - - root_key = "{" + root_key + "}" - output = output.replace(str(used_value), root_key) - + output.append(result) return output - - def format(self, data, strict=True): - copy_data = copy.deepcopy(data) - roots = self.roots - if roots: - copy_data["root"] = roots - result = super(AnatomyTemplates, self).format(copy_data) - result.strict = strict - return result - - def format_all(self, in_data, only_keys=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to False so accessing unfilled keys in templates - won't raise any exceptions. - """ - return self.format(in_data, strict=False) \ No newline at end of file From 1182c40140fcf71149ca62ea4f9be315aee73ec4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:44:01 +0100 Subject: [PATCH 015/284] modified roots --- client/ayon_core/pipeline/anatomy/roots.py | 192 ++++++++++----------- 1 file changed, 91 insertions(+), 101 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 9290a2ca38..2773559d49 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -1,12 +1,11 @@ import os -import numbers import platform - -import six +import numbers from ayon_core.lib import Logger from ayon_core.lib.path_templates import FormatObject + class RootItem(FormatObject): """Represents one item or roots. @@ -15,21 +14,13 @@ class RootItem(FormatObject): is used for formatting of template. Args: + parent (AnatomyRoots): Parent object. root_raw_data (dict): Dictionary containing root values by platform names. ["windows", "linux" and "darwin"] - name (str, optional): Root name which is representing. Used with + name (str): Root name which is representing. Used with multi root setup otherwise None value is expected. - parent_keys (list, optional): All dictionary parent keys. Values of - `parent_keys` are used for get full key which RootItem is - representing. Used for replacing root value in path with - formattable key. e.g. parent_keys == ["work"] -> {root[work]} - parent (object, optional): It is expected to be `Roots` object. - Value of `parent` won't affect code logic much. """ - - def __init__( - self, root_raw_data, name=None, parent_keys=None, parent=None - ): + def __init__(self, parent, root_raw_data, name): super(RootItem, self).__init__() self._log = None lowered_platform_keys = {} @@ -38,12 +29,11 @@ class RootItem(FormatObject): self.raw_data = lowered_platform_keys self.cleaned_data = self._clean_roots(lowered_platform_keys) self.name = name - self.parent_keys = parent_keys or [] self.parent = parent - self.available_platforms = list(lowered_platform_keys.keys()) + self.available_platforms = set(lowered_platform_keys.keys()) self.value = lowered_platform_keys.get(platform.system().lower()) - self.clean_value = self.clean_root(self.value) + self.clean_value = self._clean_root(self.value) def __format__(self, *args, **kwargs): return self.value.__format__(*args, **kwargs) @@ -64,7 +54,7 @@ class RootItem(FormatObject): self.parent.project_name ) - raise AssertionError( + raise KeyError( "Root key \"{}\" is missing{}.".format( key, additional_info ) @@ -76,6 +66,7 @@ class RootItem(FormatObject): self._log = Logger.get_logger(self.__class__.__name__) return self._log + @property def full_key(self): """Full key value for dictionary formatting in template. @@ -83,32 +74,40 @@ class RootItem(FormatObject): str: Return full replacement key for formatting. This helps when multiple roots are set. In that case e.g. `"root[work]"` is returned. + """ - if not self.name: - return "root" + return "root[{}]".format(self.name) - joined_parent_keys = "".join( - ["[{}]".format(key) for key in self.parent_keys] - ) - return "root{}".format(joined_parent_keys) + @staticmethod + def _clean_path(path): + """Just replace backslashes with forward slashes. - def clean_path(self, path): - """Just replace backslashes with forward slashes.""" + Args: + path (str): Path which should be cleaned. + + Returns: + str: Cleaned path with forward slashes. + + """ return str(path).replace("\\", "/") - def clean_root(self, root): - """Makes sure root value does not end with slash.""" - if root: - root = self.clean_path(root) - while root.endswith("/"): - root = root[:-1] - return root + def _clean_root(self, root): + """Clean root value. + + Args: + root (str): Root value which should be cleaned. + + Returns: + str: Cleaned root value. + + """ + return self._clean_path(root).rstrip("/") def _clean_roots(self, raw_data): """Clean all values of raw root item values.""" cleaned = {} for key, value in raw_data.items(): - cleaned[key] = self.clean_root(value) + cleaned[key] = self._clean_root(value) return cleaned def path_remapper(self, path, dst_platform=None, src_platform=None): @@ -121,27 +120,20 @@ class RootItem(FormatObject): src_platform (str, optional): Specify source platform. This is recommended to not use and keep unset until you really want to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". + Union[str, None]: When path does not contain known root then + None is returned else returns remapped path with + "{root[]}". + """ - cleaned_path = self.clean_path(path) + cleaned_path = self._clean_path(path) if dst_platform: dst_root_clean = self.cleaned_data.get(dst_platform) if not dst_root_clean: - key_part = "" - full_key = self.full_key() - if full_key != "root": - key_part += "\"{}\" ".format(full_key) - self.log.warning( - "Root {}miss platform \"{}\" definition.".format( - key_part, dst_platform + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key, dst_platform ) ) return None @@ -154,7 +146,7 @@ class RootItem(FormatObject): if src_root_clean is None: self.log.warning( "Root \"{}\" miss platform \"{}\" definition.".format( - self.full_key(), src_platform + self.full_key, src_platform ) ) return None @@ -172,19 +164,12 @@ class RootItem(FormatObject): if not result: return None - def parent_dict(keys, value): - if not keys: - return value - - key = keys.pop(0) - return {key: parent_dict(keys, value)} - if dst_platform: - format_value = parent_dict(list(self.parent_keys), dst_root_clean) + fill_data = {self.name: dst_root_clean} else: - format_value = parent_dict(list(self.parent_keys), self.value) + fill_data = {self.name: self.value} - return template.format(**{"root": format_value}) + return template.format(**{"root": fill_data}) def find_root_template_from_path(self, path): """Replaces known root value with formattable key in path. @@ -218,7 +203,7 @@ class RootItem(FormatObject): result = False output = str(path) - mod_path = self.clean_path(path) + mod_path = self._clean_path(path) for root_os, root_path in self.cleaned_data.items(): # Skip empty paths if not root_path: @@ -231,27 +216,26 @@ class RootItem(FormatObject): if _mod_path.startswith(root_path): result = True - replacement = "{" + self.full_key() + "}" + replacement = "{" + self.full_key + "}" output = replacement + mod_path[len(root_path):] break return (result, output) -class Roots: +class AnatomyRoots: """Object which should be used for formatting "root" key in templates. Args: - anatomy Anatomy: Anatomy object created for a specific project. + anatomy (Anatomy): Anatomy object created for a specific project. """ env_prefix = "AYON_PROJECT_ROOT" - roots_filename = "roots.json" def __init__(self, anatomy): self._log = None - self.anatomy = anatomy - self.loaded_project = None + self._anatomy = anatomy + self._loaded_project = None self._roots = None def __format__(self, *args, **kwargs): @@ -266,6 +250,16 @@ class Roots: self._log = Logger.get_logger(self.__class__.__name__) return self._log + @property + def anatomy(self): + """Parent Anatomy object. + + Returns: + Anatomy: Parent anatomy object. + + """ + return self._anatomy + def reset(self): """Reset current roots value.""" self._roots = None @@ -277,19 +271,20 @@ class Roots: Args: path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform + dst_platform (Optional[str]): Specify destination platform for which remapping should happen. - src_platform (str, optional): Specify source platform. This is + src_platform (Optional[str]): Specify source platform. This is recommended to not use and keep unset until you really want to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap + roots (Optional[Union[dict, RootItem])): It is possible to remap path with different roots then instance where method was called has. Returns: - str/None: When path does not contain known root then + Union[str, None]: When path does not contain known root then None is returned else returns remapped path with "{root}" or "{root[]}". + """ if roots is None: roots = self.roots @@ -318,8 +313,8 @@ class Roots: Args: path (str): Source path where root will be searched. - roots (Roots/dict, optional): It is possible to use different - roots than instance where method was triggered has. + roots (Optional[Union[AnatomyRoots, dict]): It is possible to use + different roots than instance where method was triggered has. Returns: tuple: Output contains tuple with bool representing success as @@ -344,7 +339,9 @@ class Roots: for root_name, _root in roots.items(): success, result = self.find_root_template_from_path(path, _root) if success: - self.log.info("Found match in root \"{}\".".format(root_name)) + self.log.debug( + "Found match in root \"{}\".".format(root_name) + ) return success, result self.log.warning("No matching root was found in current setting.") @@ -446,7 +443,7 @@ class Roots: } if isinstance(roots, RootItem): - key_items = [Roots.env_prefix] + key_items = [AnatomyRoots.env_prefix] for _key in keys: key_items.append(_key.upper()) key = "_".join(key_items) @@ -463,25 +460,31 @@ class Roots: @property def project_name(self): - """Return project name which will be used for loading root values.""" - return self.anatomy.project_name + """Current project name which will be used for loading root values. + + Returns: + str: Project name. + """ + return self._anatomy.project_name @property def roots(self): """Property for filling "root" key in templates. This property returns roots for current project or default root values. + Warning: Default roots value may cause issues when project use different roots settings. That may happen when project use multiroot templates but default roots miss their keys. + """ - if self.project_name != self.loaded_project: + if self.project_name != self._loaded_project: self._roots = None if self._roots is None: self._roots = self._discover() - self.loaded_project = self.project_name + self._loaded_project = self.project_name return self._roots def _discover(self): @@ -494,10 +497,10 @@ class Roots: setting is used. """ - return self._parse_dict(self.anatomy["roots"], parent=self) + return self._parse_dict(self._anatomy["roots"], self) @staticmethod - def _parse_dict(data, key=None, parent_keys=None, parent=None): + def _parse_dict(data, parent): """Parse roots raw data into RootItem or dictionary with RootItems. Converting raw roots data to `RootItem` helps to handle platform keys. @@ -506,29 +509,16 @@ class Roots: Args: data (dict): Should contain raw roots data to be parsed. - key (str, optional): Current root key. Set by recursion. - parent_keys (list): Parent dictionary keys. Set by recursion. - parent (Roots, optional): Parent object set in `RootItem` - helps to keep RootItem instance updated with `Roots` object. + parent (AnatomyRoots): Parent object set as parent + for ``RootItem``. Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. + dict[str, RootItem]: Root items by name. + """ - if not parent_keys: - parent_keys = [] - is_last = False - for value in data.values(): - if isinstance(value, six.string_types): - is_last = True - break - - if is_last: - return RootItem(data, key, parent_keys, parent=parent) - output = {} - for _key, value in data.items(): - _parent_keys = list(parent_keys) - _parent_keys.append(_key) - output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) + for root_name, root_values in data.items(): + output[root_name] = RootItem( + parent, root_values, root_name + ) return output From b40d734a3b82c77c8d5fd935f7f2850898184661 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:44:24 +0100 Subject: [PATCH 016/284] modified anatomy to use new options of templates and roots --- client/ayon_core/pipeline/anatomy/anatomy.py | 88 +++++++++++++++----- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py index 5eaf918663..77ba83234f 100644 --- a/client/ayon_core/pipeline/anatomy/anatomy.py +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -11,7 +11,7 @@ from ayon_core.lib import Logger, get_local_site_id from ayon_core.addon import AddonsManager from .exceptions import RootCombinationError, ProjectNotSet -from .roots import Roots +from .roots import AnatomyRoots from .templates import AnatomyTemplates log = Logger.get_logger(__name__) @@ -20,21 +20,20 @@ log = Logger.get_logger(__name__) class BaseAnatomy(object): """Anatomy module helps to keep project settings. - Wraps key project specifications, AnatomyTemplates and Roots. + Wraps key project specifications, AnatomyTemplates and AnatomyRoots. """ root_key_regex = re.compile(r"{(root?[^}]+)}") root_name_regex = re.compile(r"root\[([^]]+)\]") def __init__(self, project_entity, root_overrides=None): - project_name = project_entity["name"] - self.project_name = project_name - self.project_code = project_entity["code"] + self._project_name = project_entity["name"] + self._project_code = project_entity["code"] self._data = self._prepare_anatomy_data( project_entity, root_overrides ) self._templates_obj = AnatomyTemplates(self) - self._roots_obj = Roots(self) + self._roots_obj = AnatomyRoots(self) # Anatomy used as dictionary # - implemented only getters returning copy @@ -42,7 +41,9 @@ class BaseAnatomy(object): return copy.deepcopy(self._data[key]) def get(self, key, default=None): - return copy.deepcopy(self._data).get(key, default) + if key not in self._data: + return default + return copy.deepcopy(self._data[key]) def keys(self): return copy.deepcopy(self._data).keys() @@ -53,6 +54,26 @@ class BaseAnatomy(object): def items(self): return copy.deepcopy(self._data).items() + @property + def project_name(self): + """Project name for which is anatomy prepared. + + Returns: + str: Project name. + + """ + return self._project_name + + @property + def project_code(self): + """Project name for which is anatomy prepared. + + Returns: + str: Project code. + + """ + return self._project_code + def _prepare_anatomy_data(self, project_entity, root_overrides): """Prepare anatomy data for further processing. @@ -78,12 +99,33 @@ class BaseAnatomy(object): """Return `AnatomyTemplates` object of current Anatomy instance.""" return self._templates_obj + def get_template(self, category_name, template_name, subkey=None): + """Get template item from category. + + Args: + category_name (str): Category name. + template_name (str): Template name. + subkey (Optional[str]): Subkey name. + + Returns: + Any: Template item, subkey value as AnatomyStringTemplate or None. + + """ + return self._templates_obj.get_template( + category_name, template_name, subkey + ) + def format(self, *args, **kwargs): """Wrap `format` method of Anatomy's `templates_obj`.""" return self._templates_obj.format(*args, **kwargs) def format_all(self, *args, **kwargs): - """Wrap `format_all` method of Anatomy's `templates_obj`.""" + """Wrap `format_all` method of Anatomy's `templates_obj`. + + Deprecated: + Use ``format`` method with ``strict=False`` instead. + + """ return self._templates_obj.format_all(*args, **kwargs) @property @@ -93,7 +135,12 @@ class BaseAnatomy(object): @property def roots_obj(self): - """Return `Roots` object of current Anatomy instance.""" + """Roots wrapper object. + + Returns: + AnatomyRoots: Roots wrapper. + + """ return self._roots_obj def root_environments(self): @@ -110,15 +157,15 @@ class BaseAnatomy(object): return self.roots_obj.root_environmets_fill_data(template) def find_root_template_from_path(self, *args, **kwargs): - """Wrapper for Roots `find_root_template_from_path`.""" + """Wrapper for AnatomyRoots `find_root_template_from_path`.""" return self.roots_obj.find_root_template_from_path(*args, **kwargs) def path_remapper(self, *args, **kwargs): - """Wrapper for Roots `path_remapper`.""" + """Wrapper for AnatomyRoots `path_remapper`.""" return self.roots_obj.path_remapper(*args, **kwargs) def all_root_paths(self): - """Wrapper for Roots `all_root_paths`.""" + """Wrapper for AnatomyRoots `all_root_paths`.""" return self.roots_obj.all_root_paths() def set_root_environments(self): @@ -142,14 +189,17 @@ class BaseAnatomy(object): """ output = set() - if isinstance(data, dict): - for value in data.values(): - for root in self._root_keys_from_templates(value): - output.add(root) + keys_queue = collections.deque() + keys_queue.append(data) + while keys_queue: + queue_data = keys_queue.popleft() + if isinstance(queue_data, dict): + for value in queue_data.values(): + keys_queue.append(value) - elif isinstance(data, str): - for group in re.findall(self.root_key_regex, data): - output.add(group) + elif isinstance(queue_data, str): + for group in re.findall(self.root_key_regex, queue_data): + output.add(group) return output From 9bf5859f67a4b9a1df6273c636fe3a805a7dc720 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:46:40 +0100 Subject: [PATCH 017/284] places using anatomy templates are awared about categories --- .../plugins/publish/collect_render_path.py | 22 ++++----- client/ayon_core/hosts/fusion/api/plugin.py | 2 +- client/ayon_core/hosts/hiero/api/lib.py | 4 +- .../plugins/publish/extract_workfile_xgen.py | 6 +-- .../maya/plugins/publish/extract_xgen.py | 5 +- client/ayon_core/hosts/nuke/api/lib.py | 22 ++++----- .../hosts/nuke/startup/custom_write_node.py | 10 ++-- .../hosts/photoshop/api/launch_logic.py | 9 ++-- .../tvpaint/plugins/load/load_workfile.py | 11 ++--- .../unreal/hooks/pre_workfile_preparation.py | 4 +- client/ayon_core/lib/applications.py | 2 +- .../publish/submit_celaction_deadline.py | 8 ++-- .../plugins/publish/submit_fusion_deadline.py | 7 ++- .../plugins/publish/submit_nuke_deadline.py | 9 ++-- .../publish/submit_publish_cache_job.py | 21 ++------ .../plugins/publish/submit_publish_job.py | 21 ++------ client/ayon_core/pipeline/context_tools.py | 2 +- client/ayon_core/pipeline/delivery.py | 13 ++--- client/ayon_core/pipeline/farm/tools.py | 2 +- client/ayon_core/pipeline/publish/lib.py | 35 +++++--------- client/ayon_core/pipeline/usdlib.py | 4 +- .../pipeline/workfile/path_resolving.py | 15 +++--- .../plugins/actions/open_file_explorer.py | 8 +++- client/ayon_core/plugins/load/delivery.py | 6 ++- .../publish/collect_otio_subset_resources.py | 6 ++- .../plugins/publish/collect_resources_path.py | 23 +++------ client/ayon_core/plugins/publish/integrate.py | 12 ++--- .../plugins/publish/integrate_hero_version.py | 48 +++++-------------- .../tools/push_to_project/models/integrate.py | 13 +++-- client/ayon_core/tools/texture_copy/app.py | 4 +- .../tools/workfiles/models/workfiles.py | 11 ++--- 31 files changed, 148 insertions(+), 217 deletions(-) diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py index abe670b691..a7eef0fce4 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py @@ -18,7 +18,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): def process(self, instance): anatomy = instance.context.data["anatomy"] anatomy_data = copy.deepcopy(instance.data["anatomyData"]) - padding = anatomy.templates.get("frame_padding", 4) + padding = anatomy.templates_obj.frame_padding product_type = "render" anatomy_data.update({ "frame": f"%0{padding}d", @@ -28,15 +28,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin): }) anatomy_data["product"]["type"] = product_type - anatomy_filled = anatomy.format(anatomy_data) - # get anatomy rendering keys r_anatomy_key = self.anatomy_template_key_render_files m_anatomy_key = self.anatomy_template_key_metadata # get folder and path for rendering images from celaction - render_dir = anatomy_filled[r_anatomy_key]["folder"] - render_path = anatomy_filled[r_anatomy_key]["path"] + r_template_item = anatomy.get_template("publish", r_anatomy_key) + render_dir = r_template_item["directory"].format_strict(anatomy_data) + render_path = r_template_item["path"].format_strict(anatomy_data) self.log.debug("__ render_path: `{}`".format(render_path)) # create dir if it doesnt exists @@ -51,11 +50,12 @@ class CollectRenderPath(pyblish.api.InstancePlugin): instance.data["path"] = render_path # get anatomy for published renders folder path - if anatomy_filled.get(m_anatomy_key): - instance.data["publishRenderMetadataFolder"] = anatomy_filled[ - m_anatomy_key]["folder"] - self.log.info("Metadata render path: `{}`".format( - instance.data["publishRenderMetadataFolder"] - )) + m_template_item = anatomy.get_template("publish", m_anatomy_key) + if m_template_item is not None: + metadata_path = m_template_item["directory"].format_strict( + anatomy_data + ) + instance.data["publishRenderMetadataFolder"] = metadata_path + self.log.info("Metadata render path: `{}`".format(metadata_path)) self.log.info(f"Render output path set to: `{render_path}`") diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index f63b5eaec3..492841f967 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -133,7 +133,7 @@ class GenericCreateSaver(Creator): formatting_data = deepcopy(data) # get frame padding from anatomy templates - frame_padding = self.project_anatomy.templates["frame_padding"] + frame_padding = self.project_anatomy.templates_obj.frame_padding # get output format ext = data["creator_attributes"]["image_format"] diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index c46269b532..666119593a 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -632,7 +632,7 @@ def sync_avalon_data_to_workfile(): project_name = get_current_project_name() anatomy = Anatomy(project_name) - work_template = anatomy.templates["work"]["path"] + work_template = anatomy.get_template("work", "default", "path") work_root = anatomy.root_value_for_template(work_template) active_project_root = ( os.path.join(work_root, project_name) @@ -825,7 +825,7 @@ class PublishAction(QtWidgets.QAction): # root_node = hiero.core.nuke.RootNode() # # anatomy = Anatomy(get_current_project_name()) -# work_template = anatomy.templates["work"]["path"] +# work_template = anatomy.get_template("work", "default", "path") # root_path = anatomy.root_value_for_template(work_template) # # nuke_script.addNode(root_node) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py index 9aaba532b2..a0328725ca 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -128,9 +128,9 @@ class ExtractWorkfileXgen(publish.Extractor): alembic_files.append(alembic_file) template_data = copy.deepcopy(instance.data["anatomyData"]) - published_maya_path = StringTemplate( - instance.context.data["anatomy"].templates["publish"]["file"] - ).format(template_data) + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template("publish", "default", "file") + published_maya_path = publish_template.format(template_data) published_basename, _ = os.path.splitext(published_maya_path) for source in alembic_files: diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py index ee864bd89b..eff4dcc881 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py @@ -39,8 +39,9 @@ class ExtractXgen(publish.Extractor): # Get published xgen file name. template_data = copy.deepcopy(instance.data["anatomyData"]) template_data.update({"ext": "xgen"}) - templates = instance.context.data["anatomy"].templates["publish"] - xgen_filename = StringTemplate(templates["file"]).format(template_data) + anatomy = instance.context.data["anatomy"] + file_template = anatomy.get_template("publish", "default", "file") + xgen_filename = file_template.format(template_data) xgen_path = os.path.join( self.staging_dir(instance), xgen_filename diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index 8e39475f10..1bb0ff79e0 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -982,26 +982,18 @@ def format_anatomy(data): project_name = get_current_project_name() anatomy = Anatomy(project_name) - log.debug("__ anatomy.templates: {}".format(anatomy.templates)) - padding = None - if "frame_padding" in anatomy.templates.keys(): - padding = int(anatomy.templates["frame_padding"]) - elif "render" in anatomy.templates.keys(): - padding = int( - anatomy.templates["render"].get( - "frame_padding" - ) - ) + frame_padding = anatomy.templates_obj.frame_padding - version = data.get("version", None) - if not version: + version = data.get("version") + if version is None: file = script_name() data["version"] = get_version_from_path(file) folder_path = data["folderPath"] task_name = data["task"] host_name = get_current_host_name() + context_data = get_template_data_with_names( project_name, folder_path, task_name, host_name ) @@ -1013,7 +1005,7 @@ def format_anatomy(data): "name": data["productName"], "type": data["productType"], }, - "frame": "#" * padding, + "frame": "#" * frame_padding, }) return anatomy.format(data) @@ -1171,7 +1163,9 @@ def create_write_node( anatomy_filled = format_anatomy(data) # build file path to workfiles - fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") + fdir = str( + anatomy_filled["work"]["default"]["directory"] + ).replace("\\", "/") data["work"] = fdir fpath = StringTemplate(data["fpath_template"]).format_strict(data) diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/client/ayon_core/hosts/nuke/startup/custom_write_node.py index 075c8e7a17..098c3da3a8 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/client/ayon_core/hosts/nuke/startup/custom_write_node.py @@ -2,7 +2,7 @@ import os import nuke import nukescripts -from ayon_core.pipeline import Anatomy +from ayon_core.pipeline import Anatomy, get_current_project_name from ayon_core.hosts.nuke.api.lib import ( set_node_knobs_from_settings, get_nuke_imageio_settings @@ -102,13 +102,9 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for knob in ext_knob_list: ext = knob["value"] - anatomy = Anatomy() + anatomy = Anatomy(get_current_project_name()) - frame_padding = int( - anatomy.templates["render"].get( - "frame_padding" - ) - ) + frame_padding = anatomy.templates_obj.frame_padding for write_node in write_selected_nodes: # data for mapping the path # TODO add more fill data diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index 17fe7d5920..ccb7b0048b 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -392,17 +392,14 @@ class PhotoshopRoute(WebSocketRoute): ) data["root"] = anatomy.roots - file_template = anatomy.templates[template_key]["file"] + work_template = anatomy.get_template("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() - folder_template = anatomy.templates[template_key]["folder"] - work_root = StringTemplate.format_strict_template( - folder_template, data - ) + work_root = work_template["directory"].format_strict(data) last_workfile_path = get_last_workfile( - work_root, file_template, data, extensions, True + work_root, work_template["file"], data, extensions, True ) return last_workfile_path diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py index 4bb34089bd..16cb54f4a3 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py @@ -80,7 +80,7 @@ class LoadWorkfile(plugin.Loader): ) data["root"] = anatomy.roots - file_template = anatomy.templates[template_key]["file"] + work_template = anatomy.get_template("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() @@ -91,14 +91,11 @@ class LoadWorkfile(plugin.Loader): # Fall back to the first extension supported for this host. extension = extensions[0] - data["ext"] = extension + data["ext"] = extension.lstrip(".") - folder_template = anatomy.templates[template_key]["folder"] - work_root = StringTemplate.format_strict_template( - folder_template, data - ) + work_root = work_template["directory"].format_strict(data) version = get_last_workfile_with_version( - work_root, file_template, data, extensions + work_root, work_template["file"], data, extensions )[1] if version is None: diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py index 2f78bf026d..040beccb5e 100644 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py @@ -66,7 +66,9 @@ class UnrealPrelaunchHook(PreLaunchHook): self.host_name, ) # Fill templates - template_obj = anatomy.templates_obj[workfile_template_key]["file"] + template_obj = anatomy.get_template( + "work", workfile_template_key, "file" + ) # Return filename return template_obj.format_strict(workdir_data) diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 4bf0c31d93..c4f1d168b5 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1862,7 +1862,7 @@ def _prepare_last_workfile(data, workdir, addons_manager): project_settings=project_settings ) # Find last workfile - file_template = str(anatomy.templates[template_key]["file"]) + file_template = anatomy.get_template("work", template_key, "file") workdir_data.update({ "version": 1, diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py index bc3636da63..e11b74ae81 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -74,6 +74,8 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): render_path = os.path.normpath(render_path) script_name = os.path.basename(script_path) + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template("publish", "default", "path") for item in instance.context: if "workfile" in item.data["productType"]: msg = "Workfile (scene) must be published along" @@ -84,9 +86,9 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None - anatomy_filled = instance.context.data["anatomy"].format( - template_data) - template_filled = anatomy_filled["publish"]["path"] + template_filled = publish_template.format_strict( + template_data + ) script_path = os.path.normpath(template_filled) self.log.info( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index 837ed91c60..f99a7c6ba3 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -123,6 +123,8 @@ class FusionSubmitDeadline( script_path = context.data["currentFile"] + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template("publish", "default", "path") for item in context: if "workfile" in item.data["families"]: msg = "Workfile (scene) must be published along" @@ -133,8 +135,9 @@ class FusionSubmitDeadline( template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None - anatomy_filled = context.data["anatomy"].format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_filled = publish_template.format_strict( + template_data + ) script_path = os.path.normpath(template_filled) self.log.info( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index a3111454b3..193cad1d24 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -196,6 +196,9 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, def _get_published_workfile_path(self, context): """This method is temporary while the class is not inherited from AbstractSubmitDeadline""" + anatomy = context.data["anatomy"] + # WARNING Hardcoded template name 'default' > may not be used + publish_template = anatomy.get_template("publish", "default", "path") for instance in context: if ( instance.data["productType"] != "workfile" @@ -216,11 +219,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, template_data["ext"] = ext template_data["comment"] = None - anatomy = context.data["anatomy"] - # WARNING Hardcoded template name 'publish' > may not be used - template_obj = anatomy.templates_obj["publish"]["path"] - - template_filled = template_obj.format(template_data) + template_filled = publish_template.format(template_data) script_path = os.path.normpath(template_filled) self.log.info( "Using published scene for render {}".format( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 0561e0f65c..dbc5a12fa8 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -450,23 +450,10 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_templates = anatomy.templates_obj[template_name] - if "folder" in render_templates: - publish_folder = render_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = render_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - return publish_folder + render_dir_template = anatomy.get_template( + "publish", template_name, "directory" + ) + return render_dir_template.format_strict(template_data) @classmethod def get_attribute_defs(cls): diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 7a6abd5507..ce1c75e2ee 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -573,23 +573,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_templates = anatomy.templates_obj[template_name] - if "folder" in render_templates: - publish_folder = render_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = render_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - return publish_folder + render_dir_template = anatomy.get_template( + "publish", template_name, "directory" + ) + return render_dir_template.format_strict(template_data) @classmethod def get_attribute_defs(cls): diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 50c384bf88..bf21b43e0b 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -545,7 +545,7 @@ def get_workdir_from_session(session=None, template_key=None): ) anatomy = Anatomy(project_name) - template_obj = anatomy.templates_obj[template_key]["folder"] + template_obj = anatomy.get_template("work", template_key, "directory") path = template_obj.format_strict(template_data) if path: path = os.path.normpath(path) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index d2b78422e3..666909ef8d 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -77,8 +77,8 @@ def check_destination_path( """ anatomy_data.update(datetime_data) - anatomy_filled = anatomy.format_all(anatomy_data) - dest_path = anatomy_filled["delivery"][template_name] + path_template = anatomy.get_template("delivery", template_name, "path") + dest_path = path_template.format(anatomy_data) report_items = collections.defaultdict(list) if not dest_path.solved: @@ -150,7 +150,7 @@ def deliver_single_file( if format_dict: anatomy_data = copy.deepcopy(anatomy_data) anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.templates_obj["delivery"][template_name] + template_obj = anatomy.get_template("delivery", template_name, "path") delivery_path = template_obj.format_strict(anatomy_data) # Backwards compatibility when extension contained `.` @@ -220,8 +220,9 @@ def deliver_sequence( report_items["Source file was not found"].append(msg) return report_items, 0 - delivery_templates = anatomy.templates.get("delivery") or {} - delivery_template = delivery_templates.get(template_name) + delivery_template = anatomy.get_template( + "delivery", template_name, "path" + ) if delivery_template is None: msg = ( "Delivery template \"{}\" in anatomy of project \"{}\"" @@ -277,7 +278,7 @@ def deliver_sequence( anatomy_data["frame"] = frame_indicator if format_dict: anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.templates_obj["delivery"][template_name] + template_obj = anatomy.get_template("delivery", template_name, "path") delivery_path = template_obj.format_strict(anatomy_data) delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) diff --git a/client/ayon_core/pipeline/farm/tools.py b/client/ayon_core/pipeline/farm/tools.py index 8ab3b87ff6..1ed7e4b3f5 100644 --- a/client/ayon_core/pipeline/farm/tools.py +++ b/client/ayon_core/pipeline/farm/tools.py @@ -54,7 +54,7 @@ def from_published_scene(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] + template_obj = anatomy.get_template("publish", "default", "path") template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index b4ed69b5d7..8d2ae85694 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -742,29 +742,18 @@ def get_custom_staging_dir_info( anatomy = Anatomy(project_name) template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE - _validate_transient_template(project_name, template_name, anatomy) - custom_staging_dir = anatomy.templates[template_name]["folder"] + custom_staging_dir = anatomy.get_template( + "staging", template_name, "directory" + ) + if custom_staging_dir is None: + raise ValueError(( + "Anatomy of project \"{}\" does not have set" + " \"{}\" template key!" + ).format(project_name, template_name)) is_persistent = profile["custom_staging_dir_persistent"] - return custom_staging_dir, is_persistent - - -def _validate_transient_template(project_name, template_name, anatomy): - """Check that transient template is correctly configured. - - Raises: - ValueError - if misconfigured template - """ - if template_name not in anatomy.templates: - raise ValueError(("Anatomy of project \"{}\" does not have set" - " \"{}\" template key!" - ).format(project_name, template_name)) - - if "folder" not in anatomy.templates[template_name]: - raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa - " for project \"{}\"." - ).format(template_name, project_name)) + return str(custom_staging_dir), is_persistent def get_published_workfile_instance(context): @@ -815,9 +804,9 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_data["ext"] = rep.get("ext") template_data["comment"] = None - anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + anatomy = instance.context.data["anatomy"] + template = anatomy.get_template("publish", "default", "path") + template_filled = template.format_strict(template_data) file_path = os.path.normpath(template_filled) log.info("Using published scene for render {}".format(file_path)) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index dedcee6f99..9fabd1dce5 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -341,7 +341,9 @@ def get_usd_master_path(folder_entity, product_name, representation): "version": 0, # stub version zero }) - template_obj = anatomy.templates_obj["publish"]["path"] + template_obj = anatomy.get_template( + "publish", "default","path" + ) path = template_obj.format_strict(template_data) # Remove the version folder diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 5d36f432ad..239b8c1096 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -135,7 +135,7 @@ def get_workdir_with_workdir_data( project_settings ) - template_obj = anatomy.templates_obj[template_key]["folder"] + template_obj = anatomy.get_template("work", template_key, "directory") # Output is TemplateResult object which contain useful data output = template_obj.format_strict(workdir_data) if output: @@ -309,11 +309,12 @@ def get_last_workfile( Returns file with version 1 if there is not workfile yet. Args: - workdir(str): Path to dir where workfiles are stored. - file_template(str): Template of file name. - fill_data(Dict[str, Any]): Data for filling template. - extensions(Iterable[str]): All allowed file extensions of workfile. - full_path(bool): Full path to file is returned if set to True. + workdir (str): Path to dir where workfiles are stored. + file_template (str): Template of file name. + fill_data (Dict[str, Any]): Data for filling template. + extensions (Iterable[str]): All allowed file extensions of workfile. + full_path (Optional[bool]): Full path to file is returned if + set to True. Returns: str: Last or first workfile as filename of full path to filename. @@ -334,7 +335,7 @@ def get_last_workfile( data.pop("comment", None) if not data.get("ext"): data["ext"] = extensions[0] - data["ext"] = data["ext"].replace('.', '') + data["ext"] = data["ext"].lstrip(".") filename = StringTemplate.format_strict_template(file_template, data) if full_path: diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index c221752f11..9369a25eb0 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -70,7 +70,9 @@ class OpenTaskPath(LauncherAction): data = get_template_data(project_entity, folder_entity, task_entity) anatomy = Anatomy(project_name) - workdir = anatomy.templates_obj["work"]["folder"].format(data) + workdir = anatomy.get_template( + "work", "default", "folder" + ).format(data) # Remove any potential un-formatted parts of the path valid_workdir = self._find_first_filled_path(workdir) @@ -85,7 +87,9 @@ class OpenTaskPath(LauncherAction): return valid_workdir data.pop("task", None) - workdir = anatomy.templates_obj["work"]["folder"].format(data) + workdir = anatomy.get_template( + "work", "default", "folder" + ).format(data) valid_workdir = self._find_first_filled_path(workdir) if valid_workdir: # Normalize diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 453bdfb87a..5289e594f7 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -282,7 +282,11 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): """Adds list of delivery templates from Anatomy to dropdown.""" templates = {} for template_name, value in anatomy.templates["delivery"].items(): - if not isinstance(value, str) or not value.startswith('{root'): + path_template = value["path"] + if ( + not isinstance(path_template, str) + or not path_template.startswith('{root') + ): continue templates[template_name] = value diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 3f47e6e3bf..ad3a4d2c23 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -43,8 +43,10 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) anatomy = instance.context.data["anatomy"] - publish_template_category = anatomy.templates[template_name] - template = os.path.normpath(publish_template_category["path"]) + publish_path_template = anatomy.get_template( + "publish", template_name, "path" + ) + template = os.path.normpath(publish_path_template) self.log.debug( ">> template: {}".format(template)) diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 6a871124f1..05015909d5 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -79,23 +79,12 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "representation": "TEMP" }) - publish_templates = anatomy.templates_obj["publish"] - if "folder" in publish_templates: - publish_folder = publish_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(anatomy.project_name)) - - file_path = publish_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - publish_folder = os.path.normpath(publish_folder) + publish_templates = anatomy.get_template( + "publish", "default", "directory" + ) + publish_folder = os.path.normpath( + publish_templates.format_strict(template_data) + ) resources_folder = os.path.join(publish_folder, "resources") instance.data["publishDir"] = publish_folder diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index b7839338ae..16c8ebaaf5 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -665,8 +665,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data["anatomy"] - publish_template_category = anatomy.templates[template_name] - template = os.path.normpath(publish_template_category["path"]) + publish_template = anatomy.get_template("publish", template_name) + path_template_obj = publish_template["path"] + template = os.path.normpath(path_template_obj) is_udim = bool(repre.get("udim")) @@ -698,7 +699,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # - template_data (Dict[str, Any]): source data used to fill template # - to add required data to 'repre_context' not used for # formatting - path_template_obj = anatomy.templates_obj[template_name]["path"] # Treat template with 'orignalBasename' in special way if "{originalBasename}" in template: @@ -753,9 +753,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if not is_udim: # Change padding for frames if template has defined higher # padding. - template_padding = int( - publish_template_category["frame_padding"] - ) + template_padding = anatomy.templates_obj.frame_padding if template_padding > destination_padding: destination_padding = template_padding @@ -841,7 +839,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # todo: Are we sure the assumption each representation # ends up in the same folder is valid? if not instance.data.get("publishDir"): - template_obj = anatomy.templates_obj[template_name]["folder"] + template_obj = publish_template["directory"] template_filled = template_obj.format_strict(template_data) instance.data["publishDir"] = template_filled diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 6d690ba94b..e313c94393 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -103,22 +103,15 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): project_name = anatomy.project_name template_key = self._get_template_key(project_name, instance) + hero_template = anatomy.get_template("hero", template_key, "path") - if template_key not in anatomy.templates: + if hero_template is None: self.log.warning(( "!!! Anatomy of project \"{}\" does not have set" " \"{}\" template key!" ).format(project_name, template_key)) return - if "path" not in anatomy.templates[template_key]: - self.log.warning(( - "!!! There is not set \"path\" template in \"{}\" anatomy" - " for project \"{}\"." - ).format(template_key, project_name)) - return - - hero_template = anatomy.templates[template_key]["path"] self.log.debug("`hero` template check was successful. `{}`".format( hero_template )) @@ -327,7 +320,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): try: src_to_dst_file_paths = [] repre_integrate_data = [] - path_template_obj = anatomy.templates_obj[template_key]["path"] + path_template_obj = anatomy.get_template( + "hero", template_key, "path" + ) for repre_info in published_repres.values(): # Skip if new repre does not have published repre files @@ -383,9 +378,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data ) head, tail = _template_filled.split(frame_splitter) - padding = int( - anatomy.templates[template_key]["frame_padding"] - ) + padding = anatomy.templates_obj.frame_padding dst_col = clique.Collection( head=head, padding=padding, tail=tail @@ -545,29 +538,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): "originalBasename": instance.data.get("originalBasename") }) - if "folder" in anatomy.templates[template_key]: - template_obj = anatomy.templates_obj[template_key]["folder"] - publish_folder = template_obj.format_strict(template_data) - else: - # This is for cases of Deprecated anatomy without `folder` - # TODO remove when all clients have solved this issue - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(anatomy.project_name)) - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - template_obj = anatomy.templates_obj[template_key]["path"] - file_path = template_obj.format_strict(template_data) - - # Directory - publish_folder = os.path.dirname(file_path) - - publish_folder = os.path.normpath(publish_folder) + template_obj = anatomy.get_template( + "hero", template_key, "directory" + ) + publish_folder = os.path.normpath( + template_obj.format_strict(template_data) + ) self.log.debug("hero publish dir: \"{}\"".format(publish_folder)) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 8a29da2fe4..0987dc0c56 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -973,12 +973,9 @@ class ProjectPushItemProcess: "version": version_entity["version"] }) - path_template = anatomy.templates[template_name]["path"].replace( - "\\", "/" - ) - file_template = StringTemplate( - anatomy.templates[template_name]["file"] - ) + publish_template = anatomy.get_template("publish", template_name) + path_template = publish_template["path"].replace("\\", "/") + file_template = publish_template["file"] self._log_info("Preparing files to transfer") processed_repre_items = self._prepare_file_transactions( anatomy, template_name, formatting_data, file_template @@ -1014,7 +1011,9 @@ class ProjectPushItemProcess: if repre_output_name is not None: repre_format_data["output"] = repre_output_name - template_obj = anatomy.templates_obj[template_name]["folder"] + template_obj = anatomy.get_template( + "publish", template_name, "directory" + ) folder_path = template_obj.format_strict(formatting_data) repre_context = folder_path.used_values folder_path_rootless = folder_path.rootless diff --git a/client/ayon_core/tools/texture_copy/app.py b/client/ayon_core/tools/texture_copy/app.py index b3484fbcd0..9a94ba44f7 100644 --- a/client/ayon_core/tools/texture_copy/app.py +++ b/client/ayon_core/tools/texture_copy/app.py @@ -40,7 +40,9 @@ class TextureCopy: }, }) anatomy = Anatomy(project_name, project_entity=project_entity) - template_obj = anatomy.templates_obj["texture"]["path"] + template_obj = anatomy.get_template( + "publish", "texture", "path" + ) return template_obj.format_strict(template_data) def _get_version(self, path): diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 35a49a908f..f4ca08a45a 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -253,8 +253,9 @@ class WorkareaModel: return list(comment_hints), current_comment def _get_workdir(self, anatomy, template_key, fill_data): - template_info = anatomy.templates_obj[template_key] - directory_template = template_info["folder"] + directory_template = anatomy.get_template( + "work", template_key, "directory" + ) return directory_template.format_strict(fill_data).normalized() def get_workarea_save_as_data(self, folder_id, task_id): @@ -299,8 +300,7 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - template_info = anatomy.templates_obj[template_key] - file_template = template_info["file"] + file_template = anatomy.get_template("work", template_key, "file") comment_hints, comment = self._get_comments_from_root( file_template, @@ -342,8 +342,7 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - template_info = anatomy.templates_obj[template_key] - file_template = template_info["file"] + file_template = anatomy.get_template("work", template_key, "file") if use_last_version: version = self._get_last_workfile_version( From 4d26f516b72172993218c5ecc62e7e97c86647c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:46:49 +0100 Subject: [PATCH 018/284] fix default template names --- client/ayon_core/pipeline/publish/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/constants.py b/client/ayon_core/pipeline/publish/constants.py index 92e3fb089f..38f5ffef3f 100644 --- a/client/ayon_core/pipeline/publish/constants.py +++ b/client/ayon_core/pipeline/publish/constants.py @@ -6,6 +6,6 @@ ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 -DEFAULT_PUBLISH_TEMPLATE = "publish" -DEFAULT_HERO_PUBLISH_TEMPLATE = "hero" -TRANSIENT_DIR_TEMPLATE = "transient" +DEFAULT_PUBLISH_TEMPLATE = "default" +DEFAULT_HERO_PUBLISH_TEMPLATE = "default" +TRANSIENT_DIR_TEMPLATE = "default" From 3e00d850ccec6f0f43d7a63a3471c4b5ec0d8b0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2024 18:47:22 +0100 Subject: [PATCH 019/284] removed unused 'TemplatesDict' and 'TemplatesResultDict' from path templates --- client/ayon_core/lib/__init__.py | 6 - client/ayon_core/lib/path_templates.py | 268 +------------------------ 2 files changed, 2 insertions(+), 272 deletions(-) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 6f76506dd8..918d14c8a7 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -81,11 +81,8 @@ from .log import ( ) from .path_templates import ( - merge_dict, - TemplateMissingKey, TemplateUnsolved, StringTemplate, - TemplatesDict, FormatObject, ) @@ -265,11 +262,8 @@ __all__ = [ "get_version_from_path", "get_last_version_from_path", - "merge_dict", - "TemplateMissingKey", "TemplateUnsolved", "StringTemplate", - "TemplatesDict", "FormatObject", "terminal", diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 9be1736abf..09d11ea1de 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -1,8 +1,6 @@ import os import re -import copy import numbers -import collections import six @@ -12,44 +10,6 @@ SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") -def merge_dict(main_dict, enhance_dict): - """Merges dictionaries by keys. - - Function call itself if value on key is again dictionary. - - Args: - main_dict (dict): First dict to merge second one into. - enhance_dict (dict): Second dict to be merged. - - Returns: - dict: Merged result. - - .. note:: does not overrides whole value on first found key - but only values differences from enhance_dict - - """ - for key, value in enhance_dict.items(): - if key not in main_dict: - main_dict[key] = value - elif isinstance(value, dict) and isinstance(main_dict[key], dict): - main_dict[key] = merge_dict(main_dict[key], value) - else: - main_dict[key] = value - return main_dict - - -class TemplateMissingKey(Exception): - """Exception for cases when key does not exist in template.""" - - msg = "Template key does not exist: `{}`." - - def __init__(self, parents): - parent_join = "".join(["[\"{0}\"]".format(key) for key in parents]) - super(TemplateMissingKey, self).__init__( - self.msg.format(parent_join) - ) - - class TemplateUnsolved(Exception): """Exception for unsolved template when strict is set to True.""" @@ -240,137 +200,6 @@ class StringTemplate(object): new_parts.extend(tmp_parts[idx]) return new_parts - -class TemplatesDict(object): - def __init__(self, templates=None): - self._raw_templates = None - self._templates = None - self._objected_templates = None - self.set_templates(templates) - - def set_templates(self, templates): - if templates is None: - self._raw_templates = None - self._templates = None - self._objected_templates = None - elif isinstance(templates, dict): - self._raw_templates = copy.deepcopy(templates) - self._templates = templates - self._objected_templates = self.create_objected_templates( - templates) - else: - raise TypeError("<{}> argument must be a dict, not {}.".format( - self.__class__.__name__, str(type(templates)) - )) - - def __getitem__(self, key): - return self.objected_templates[key] - - def get(self, key, *args, **kwargs): - return self.objected_templates.get(key, *args, **kwargs) - - @property - def raw_templates(self): - return self._raw_templates - - @property - def templates(self): - return self._templates - - @property - def objected_templates(self): - return self._objected_templates - - def _create_template_object(self, template): - """Create template object from a template string. - - Separated into method to give option change class of templates. - - Args: - template (str): Template string. - - Returns: - StringTemplate: Object of template. - """ - - return StringTemplate(template) - - def create_objected_templates(self, templates): - if not isinstance(templates, dict): - raise TypeError("Expected dict object, got {}".format( - str(type(templates)) - )) - - objected_templates = copy.deepcopy(templates) - inner_queue = collections.deque() - inner_queue.append(objected_templates) - while inner_queue: - item = inner_queue.popleft() - if not isinstance(item, dict): - continue - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, six.string_types): - item[key] = self._create_template_object(value) - elif isinstance(value, dict): - inner_queue.append(value) - return objected_templates - - def _format_value(self, value, data): - if isinstance(value, StringTemplate): - return value.format(data) - - if isinstance(value, dict): - return self._solve_dict(value, data) - return value - - def _solve_dict(self, templates, data): - """ Solves templates with entered data. - - Args: - templates (dict): All templates which will be formatted. - data (dict): Containing keys to be filled into template. - - Returns: - dict: With `TemplateResult` in values containing filled or - partially filled templates. - """ - output = collections.defaultdict(dict) - for key, value in templates.items(): - output[key] = self._format_value(value, data) - - return output - - def format(self, in_data, only_keys=True, strict=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - only_keys (bool, optional): Decides if environ will be used to - fill templates or only keys in data. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to True so accessing unfilled keys in templates - will raise exceptions with explaned error. - """ - # Create a copy of inserted data - data = copy.deepcopy(in_data) - - # Add environment variable to data - if only_keys is False: - for key, val in os.environ.items(): - env_key = "$" + key - if env_key not in data: - data[env_key] = val - - solved = self._solve_dict(self.objected_templates, data) - - output = TemplatesResultDict(solved) - output.strict = strict - return output - - class TemplateResult(str): """Result of template format with most of information in. @@ -379,8 +208,8 @@ class TemplateResult(str): only used keys. solved (bool): For check if all required keys were filled. template (str): Original template. - missing_keys (list): Missing keys that were not in the data. Include - missing optional keys. + missing_keys (Iterable[str]): Missing keys that were not in the data. + Include missing optional keys. invalid_types (dict): When key was found in data, but value had not allowed DataType. Allowed data types are `numbers`, `str`(`basestring`) and `dict`. Dictionary may cause invalid type @@ -445,99 +274,6 @@ class TemplateResult(str): ) -class TemplatesResultDict(dict): - """Holds and wrap TemplateResults for easy bug report.""" - - def __init__(self, in_data, key=None, parent=None, strict=None): - super(TemplatesResultDict, self).__init__() - for _key, _value in in_data.items(): - if isinstance(_value, dict): - _value = self.__class__(_value, _key, self) - self[_key] = _value - - self.key = key - self.parent = parent - self.strict = strict - if self.parent is None and strict is None: - self.strict = True - - def __getitem__(self, key): - if key not in self.keys(): - hier = self.hierarchy() - hier.append(key) - raise TemplateMissingKey(hier) - - value = super(TemplatesResultDict, self).__getitem__(key) - if isinstance(value, self.__class__): - return value - - # Raise exception when expected solved templates and it is not. - if self.raise_on_unsolved and hasattr(value, "validate"): - value.validate() - return value - - @property - def raise_on_unsolved(self): - """To affect this change `strict` attribute.""" - if self.strict is not None: - return self.strict - return self.parent.raise_on_unsolved - - def hierarchy(self): - """Return dictionary keys one by one to root parent.""" - if self.parent is None: - return [] - - hier_keys = [] - par_hier = self.parent.hierarchy() - if par_hier: - hier_keys.extend(par_hier) - hier_keys.append(self.key) - - return hier_keys - - @property - def missing_keys(self): - """Return missing keys of all children templates.""" - missing_keys = set() - for value in self.values(): - missing_keys |= value.missing_keys - return missing_keys - - @property - def invalid_types(self): - """Return invalid types of all children templates.""" - invalid_types = {} - for value in self.values(): - invalid_types = merge_dict(invalid_types, value.invalid_types) - return invalid_types - - @property - def used_values(self): - """Return used values for all children templates.""" - used_values = {} - for value in self.values(): - used_values = merge_dict(used_values, value.used_values) - return used_values - - def get_solved(self): - """Get only solved key from templates.""" - result = {} - for key, value in self.items(): - if isinstance(value, self.__class__): - value = value.get_solved() - if not value: - continue - result[key] = value - - elif ( - not hasattr(value, "solved") or - value.solved - ): - result[key] = value - return self.__class__(result, key=self.key, parent=self.parent) - - class TemplatePartResult: """Result to store result of template parts.""" def __init__(self, optional=False): From 81073b4cafab3dc337ed597b05a696d1975a893b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 15 Mar 2024 09:55:45 +0000 Subject: [PATCH 020/284] If knob name exists only change value. --- client/ayon_core/hosts/nuke/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index e304b33dc7..32e9ace377 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -397,7 +397,13 @@ def imprint(node, data, tab=None): """ for knob in create_knobs(data, tab): - node.addKnob(knob) + # If knob name exists we set the value. Technically there could be + # multiple knobs with the same name, but the intent is not to have + # duplicated knobs so we do not account for that. + if knob.name() in node.knobs().keys(): + node[knob.name()].setValue(knob.value()) + else: + node.addKnob(knob) @deprecated From f6e8465dca6081b3deecda9d3d28fceabad2e169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 14:57:05 +0100 Subject: [PATCH 021/284] :alembic: add ruff workflow and code-spell pre-commit --- .github/workflows/pr_linting.yml | 24 ++++++ .pre-commit-config.yaml | 33 +++++--- poetry.toml | 0 pyproject.toml | 105 ++++++++++++++++++++++++++ scripts/setup_env.ps1 | 67 +++++++++++++++++ scripts/setup_env.sh | 124 +++++++++++++++++++++++++++++++ 6 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/pr_linting.yml create mode 100644 poetry.toml create mode 100644 pyproject.toml create mode 100644 scripts/setup_env.ps1 create mode 100644 scripts/setup_env.sh diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml new file mode 100644 index 0000000000..3d2431b69a --- /dev/null +++ b/.github/workflows/pr_linting.yml @@ -0,0 +1,24 @@ +name: 📇 Code Linting + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number}} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eec388924e..8aa3e1b81b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,27 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: no-commit-to-branch - args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: no-commit-to-branch + args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli + + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.3.3 + hooks: + # Run the linter. + - id: ruff + # Run the formatter. + # - id: ruff-format diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..c50c0bad5b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,105 @@ +[tool.poetry] +name = "ayon-core" +version = "0.3.0" +description = "" +authors = ["Ynput Team "] +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.9.1,<3.10" +aiohttp_json_rpc = "*" # TVPaint server +aiohttp-middlewares = "^2.0.0" +wsrpc_aiohttp = "^3.1.1" # websocket server +Click = "^8" +clique = "1.6.*" +jsonschema = "^4" +pyblish-base = "^1.8.11" +pynput = "^1.7.2" # Timers manager - TODO remove +speedcopy = "^2.1" +six = "^1.15" +qtawesome = "0.7.3" + + +[tool.poetry.dev-dependencies] +pytest = "^8.0" +pytest-print = "^1.0" +ayon-python-api = "^1.0" +arrow = "^1.3.0" +ruff = "^0.2.2" + + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "vendor", + "generated", +] + +# Same as Black. +line-length = 79 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py39" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.codespell] +# Ignore words that are not in the dictionary. +ignore-words-list = "ayon,ynput" +skip = "./.*,./package/*" +count = true +quiet-level = 3 + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/scripts/setup_env.ps1 b/scripts/setup_env.ps1 new file mode 100644 index 0000000000..82ff515bc6 --- /dev/null +++ b/scripts/setup_env.ps1 @@ -0,0 +1,67 @@ +$current_dir = Get-Location +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$repo_root = (Get-Item $script_dir).parent.FullName +& git submodule update --init --recursive + + +function Exit-WithCode($exitcode) { + # Only exit this host process if it's a child of another PowerShell parent process... + $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId + $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name + if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) } + + exit $exitcode + } + + +function Install-Poetry() { + Write-Host ">>> Installing Poetry ... " + $python = "python" + if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + if (-not (Test-Path -PathType Leaf -Path "$($repo_root)\.python-version")) { + $result = & pyenv global + if ($result -eq "no global version configured") { + Write-Host "!!! Using pyenv but having no local or global version of Python set." -Color Red, Yellow + Exit-WithCode 1 + } + } + $python = & pyenv which python + + } + + $env:POETRY_HOME="$repo_root\.poetry" + (Invoke-WebRequest -Uri https://install.python-poetry.org/ -UseBasicParsing).Content | & $($python) - +} + +Write-Host ">>> Reading Poetry ... " -NoNewline +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { + Write-Host "NOT FOUND" + Install-Poetry + Write-Host "INSTALLED" +} else { + Write-Host "OK" +} + +if (-not (Test-Path -PathType Leaf -Path "$($repo_root)\poetry.lock")) { + Write-Host ">>> Installing virtual environment and creating lock." +} else { + Write-Host ">>> Installing virtual environment from lock." +} +$startTime = [int][double]::Parse((Get-Date -UFormat %s)) +& "$env:POETRY_HOME\bin\poetry" install --no-root $poetry_verbosity --ansi +if ($LASTEXITCODE -ne 0) { + Write-Host "!!! ", "Poetry command failed." + Set-Location -Path $current_dir + Exit-WithCode 1 +} +Write-Host ">>> Installing pre-commit hooks ..." +& "$env:POETRY_HOME\bin\poetry" run pre-commit install +if ($LASTEXITCODE -ne 0) { + Write-Host "!!! Installation of pre-commit hooks failed." + Set-Location -Path $current_dir + Exit-WithCode 1 +} + +$endTime = [int][double]::Parse((Get-Date -UFormat %s)) +Set-Location -Path $current_dir +Write-Host ">>> Done in $( $endTime - $startTime ) secs." diff --git a/scripts/setup_env.sh b/scripts/setup_env.sh new file mode 100644 index 0000000000..16298cb8bc --- /dev/null +++ b/scripts/setup_env.sh @@ -0,0 +1,124 @@ +# Colors for terminal + +RST='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + + +############################################################################## +# Detect required version of python +# Globals: +# colors +# PYTHON +# Arguments: +# None +# Returns: +# None +############################################################################### +detect_python () { + echo -e "${BIGreen}>>>${RST} Using python \c" + command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.9 installed to continue.${RST}"; return 1; } + local version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))" + local python_version="$(python <<< ${version_command})" + oIFS="$IFS" + IFS=. + set -- $python_version + IFS="$oIFS" + if [ "$1" -ge "3" ] && [ "$2" -ge "9" ] ; then + if [ "$2" -gt "9" ] ; then + echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}"; return 1; + else + echo -e "${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}" + fi + else + command -v python >/dev/null 2>&1 || { echo -e "${BIRed}$1.$2$ - ${BIRed}FAILED${RST} ${BIYellow}Version is old and unsupported${RST}"; return 1; } + fi +} + +install_poetry () { + echo -e "${BIGreen}>>>${RST} Installing Poetry ..." + export POETRY_HOME="$repo_root/.poetry" + command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; } + curl -sSL https://install.python-poetry.org/ | python - +} + +############################################################################## +# Return absolute path +# Globals: +# None +# Arguments: +# Path to resolve +# Returns: +# None +############################################################################### +realpath () { + echo $(cd $(dirname "$1"); pwd)/$(basename "$1") +} + +main () { + detect_python || return 1 + + # Directories + repo_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + if [[ -z $POETRY_HOME ]]; then + export POETRY_HOME="$repo_root/.poetry" + fi + + pushd "$repo_root" > /dev/null || return > /dev/null + + echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" + if [ -f "$POETRY_HOME/bin/poetry" ]; then + echo -e "${BIGreen}OK${RST}" + else + echo -e "${BIYellow}NOT FOUND${RST}" + install_poetry || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return 1; } + fi + + if [ -f "$repo_root/poetry.lock" ]; then + echo -e "${BIGreen}>>>${RST} Updating dependencies ..." + else + echo -e "${BIGreen}>>>${RST} Installing dependencies ..." + fi + + "$POETRY_HOME/bin/poetry" install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return 1; } + if [ $? -ne 0 ] ; then + echo -e "${BIRed}!!!${RST} Virtual environment creation failed." + return 1 + fi + + echo -e "${BIGreen}>>>${RST} Installing pre-commit hooks ..." + "$POETRY_HOME/bin/poetry" run pre-commit install +} + +return_code=0 +main || return_code=$? +exit $return_code From 55e8ab0c4d155487474c3bb5791e19001ef5416f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 14:59:02 +0100 Subject: [PATCH 022/284] :recycle: update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 502cf85b9f..acbc3e2572 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ dump.sql # Poetry ######## +.poetry/ .python-version .editorconfig .pre-commit-config.yaml From 84c42d060ed9ff97ec4f73f0858844b33052c7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 15:00:08 +0100 Subject: [PATCH 023/284] :heavy_plus_sign: add pre-commit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c50c0bad5b..14fe5fd1e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pynput = "^1.7.2" # Timers manager - TODO remove speedcopy = "^2.1" six = "^1.15" qtawesome = "0.7.3" +pre-commit = "^3.6.2" [tool.poetry.dev-dependencies] From 775d53f4d29edb0d13f86c588aa36f3da111b837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 15:06:36 +0100 Subject: [PATCH 024/284] :heavy_plus_sign: add codespell and config --- poetry.toml | 2 ++ pyproject.toml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/poetry.toml b/poetry.toml index e69de29bb2..ab1033bd37 100644 --- a/poetry.toml +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml index 14fe5fd1e3..f9c0f4602c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ speedcopy = "^2.1" six = "^1.15" qtawesome = "0.7.3" pre-commit = "^3.6.2" +codespell = "^2.2.6" [tool.poetry.dev-dependencies] @@ -97,7 +98,7 @@ line-ending = "auto" [tool.codespell] # Ignore words that are not in the dictionary. ignore-words-list = "ayon,ynput" -skip = "./.*,./package/*" +skip = "./.*,./package/*,*/vendor/*,*/unreal/integration/*,*/aftereffects/api/extension/js/libs/*" count = true quiet-level = 3 From 70f89e234bd03423863a7d7ebaa25dda4ba4ff28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:06:38 +0100 Subject: [PATCH 025/284] renamed key 'custom_attributes' to 'attributes' --- .../traypublisher/plugins/publish/collect_shot_instances.py | 2 +- client/ayon_core/plugins/publish/collect_hierarchy.py | 2 +- client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py index edcbb27cb3..f1e5e83f0a 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -155,7 +155,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): in_info = { "entity_type": "Shot", - "custom_attributes": { + "attributes": { "handleStart": handle_start, "handleEnd": handle_end, "frameStart": instance.data["frameStart"], diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 7387b1865b..d7f0777ba4 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -46,7 +46,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): shot_data['tasks'] = instance.data.get("tasks") or {} shot_data["comments"] = instance.data.get("comments", []) - shot_data['custom_attributes'] = { + shot_data['attributes'] = { "handleStart": instance.data["handleStart"], "handleEnd": instance.data["handleEnd"], "frameStart": instance.data["frameStart"], diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 59a15af299..50b2bf7c0b 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -241,7 +241,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): project_item["name"] = key project_item["tasks"] = [] project_item["attributes"] = project_item.pop( - "custom_attributes", {} + "attributes", {} ) project_item["children"] = [] @@ -280,7 +280,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): task_info["name"] = task_name task_items.append(task_info) new_item["tasks"] = task_items - new_item["attributes"] = new_item.pop("custom_attributes", {}) + new_item["attributes"] = new_item.pop("attributes", {}) items_by_id[item_id] = new_item parent_id_by_item_id[item_id] = parent_id From 92fc2f230860477ae5d81d9467a9078b979416de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 15:07:04 +0100 Subject: [PATCH 026/284] :heavy_plus_sign: add `poetry.lock` --- poetry.lock | 1468 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1468 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..adfa083e82 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1468 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, + {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, + {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, + {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, + {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, + {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, + {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, + {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, + {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, + {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, + {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, + {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiohttp-json-rpc" +version = "0.13.3" +description = "Implementation JSON-RPC 2.0 server and client using aiohttp on top of websockets transport" +optional = false +python-versions = ">=3.5" +files = [ + {file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"}, + {file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"}, +] + +[package.dependencies] +aiohttp = ">=3,<4" + +[[package]] +name = "aiohttp-middlewares" +version = "2.3.0" +description = "Collection of useful middlewares for aiohttp applications." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "aiohttp_middlewares-2.3.0-py3-none-any.whl", hash = "sha256:4424b136a351b67b4c93da7d7505b56a342addaa324be30793f6cba463d18ac8"}, + {file = "aiohttp_middlewares-2.3.0.tar.gz", hash = "sha256:b2564c1dfa8dbcf7d2e101a6a03dcaad45464744531c269e8e582cb2dc551d08"}, +] + +[package.dependencies] +aiohttp = ">=3.8.1,<4.0.0" +async-timeout = ">=4.0.2,<5.0.0" +yarl = ">=1.5.1,<2.0.0" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "ayon-python-api" +version = "1.0.1" +description = "AYON Python API" +optional = false +python-versions = "*" +files = [ + {file = "ayon-python-api-1.0.1.tar.gz", hash = "sha256:6a53af84903317e2097f3c6bba0094e90d905d6670fb9c7d3ad3aa9de6552bc1"}, + {file = "ayon_python_api-1.0.1-py3-none-any.whl", hash = "sha256:d4b649ac39c9003cdbd60f172c0d35f05d310fba3a0649b6d16300fe67f967d6"}, +] + +[package.dependencies] +appdirs = ">=1,<2" +requests = ">=2.27.1" +six = ">=1.15" +Unidecode = ">=1.2.0" + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "clique" +version = "1.6.1" +description = "Manage collections with common numerical component" +optional = false +python-versions = ">=2.7, <4.0" +files = [ + {file = "clique-1.6.1-py2.py3-none-any.whl", hash = "sha256:8619774fa035661928dd8c93cd805acf2d42533ccea1b536c09815ed426c9858"}, + {file = "clique-1.6.1.tar.gz", hash = "sha256:90165c1cf162d4dd1baef83ceaa1afc886b453e379094fa5b60ea470d1733e66"}, +] + +[package.extras] +dev = ["lowdown (>=0.2.0,<1)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] +doc = ["lowdown (>=0.2.0,<1)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] +test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)"] + +[[package]] +name = "codespell" +version = "2.2.6" +description = "Codespell" +optional = false +python-versions = ">=3.8" +files = [ + {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"}, + {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"}, +] + +[package.extras] +dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] +types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "evdev" +version = "1.7.0" +description = "Bindings to the Linux input handling subsystem" +optional = false +python-versions = ">=3.6" +files = [ + {file = "evdev-1.7.0.tar.gz", hash = "sha256:95bd2a1e0c6ce2cd7a2ecc6e6cd9736ff794b3ad5cb54d81d8cbc2e414d0b870"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyblish-base" +version = "1.8.11" +description = "Plug-in driven automation framework for content" +optional = false +python-versions = "*" +files = [ + {file = "pyblish-base-1.8.11.tar.gz", hash = "sha256:86dfeec0567430eb7eb25f89a18312054147a729ec66f6ac8c7e421fd15b66e1"}, + {file = "pyblish_base-1.8.11-py2.py3-none-any.whl", hash = "sha256:c321be7020c946fe9dfa11941241bd985a572c5009198b4f9810e5afad1f0b4b"}, +] + +[[package]] +name = "pynput" +version = "1.7.6" +description = "Monitor and control user input devices" +optional = false +python-versions = "*" +files = [ + {file = "pynput-1.7.6-py2.py3-none-any.whl", hash = "sha256:19861b2a0c430d646489852f89500e0c9332e295f2c020e7c2775e7046aa2e2f"}, + {file = "pynput-1.7.6.tar.gz", hash = "sha256:3a5726546da54116b687785d38b1db56997ce1d28e53e8d22fc656d8b92e533c"}, +] + +[package.dependencies] +evdev = {version = ">=1.3", markers = "sys_platform in \"linux\""} +pyobjc-framework-ApplicationServices = {version = ">=8.0", markers = "sys_platform == \"darwin\""} +pyobjc-framework-Quartz = {version = ">=8.0", markers = "sys_platform == \"darwin\""} +python-xlib = {version = ">=0.17", markers = "sys_platform in \"linux\""} +six = "*" + +[[package]] +name = "pyobjc-core" +version = "10.2" +description = "Python<->ObjC Interoperability Module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-core-10.2.tar.gz", hash = "sha256:0153206e15d0e0d7abd53ee8a7fbaf5606602a032e177a028fc8589516a8771c"}, + {file = "pyobjc_core-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8eab50ce7f17017a0f1d68c3b7e88bb1bb033415fdff62b8e0a9ee4ab72f242"}, + {file = "pyobjc_core-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f2115971463073426ab926416e17e5c16de5b90d1a1f2a2d8724637eb1c21308"}, + {file = "pyobjc_core-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a70546246177c23acb323c9324330e37638f1a0a3d13664abcba3bb75e43012c"}, + {file = "pyobjc_core-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a9b5a215080d13bd7526031d21d5eb27a410780878d863f486053a0eba7ca9a5"}, + {file = "pyobjc_core-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:eb1ab700a44bcc4ceb125091dfaae0b998b767b49990df5fdc83eb58158d8e3f"}, + {file = "pyobjc_core-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a7163aff9c47d654f835f80361c1b112886ec754800d34e75d1e02ff52c3d7"}, +] + +[[package]] +name = "pyobjc-framework-applicationservices" +version = "10.2" +description = "Wrappers for the framework ApplicationServices on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-ApplicationServices-10.2.tar.gz", hash = "sha256:f83d6ed3320afb6648be6defafe0f05bac00d0281fc84ee4766ff977309b659f"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2aebfed888f9bcb4f11d93f9ef9a76d561e92848dcb6011da5d5e9d3593371be"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edfd3153e64ee9573bcff7ccaa1fbbbd6964658f187464c461ad34f24552bc85"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d2c89b246c19a041221ff36e9121c92e86a4422016f809a40f5ce3d647882d9"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee1e69947f31aad5fdec44921ce37f7f921faf50a0ceb27ed40b6d54f4b15d0e"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:101f5b09d71e55bd39e6e91f0787433805d422622336b72fde969a7c54528045"}, + {file = "pyobjc_framework_ApplicationServices-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a3ef00c9aea09c5ef5840b8749d0753249869bc30e124145b763cd0b4b81155"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" +pyobjc-framework-CoreText = ">=10.2" +pyobjc-framework-Quartz = ">=10.2" + +[[package]] +name = "pyobjc-framework-cocoa" +version = "10.2" +description = "Wrappers for the Cocoa frameworks on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-Cocoa-10.2.tar.gz", hash = "sha256:6383141379636b13855dca1b39c032752862b829f93a49d7ddb35046abfdc035"}, + {file = "pyobjc_framework_Cocoa-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9227b4f271fda2250f5a88cbc686ff30ae02c0f923bb7854bb47972397496b2"}, + {file = "pyobjc_framework_Cocoa-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a6042b7703bdc33b7491959c715c1e810a3f8c7a560c94b36e00ef321480797"}, + {file = "pyobjc_framework_Cocoa-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:18886d5013cd7dc7ecd6e0df5134c767569b5247fc10a5e293c72ee3937b217b"}, + {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ecf01400ee698d2e0ff4c907bcf9608d9d710e97203fbb97b37d208507a9362"}, + {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0def036a7b24e3ae37a244c77bec96b7c9c8384bf6bb4d33369f0a0c8807a70d"}, + {file = "pyobjc_framework_Cocoa-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f47ecc393bc1019c4b47e8653207188df784ac006ad54d8c2eb528906ff7013"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" + +[[package]] +name = "pyobjc-framework-coretext" +version = "10.2" +description = "Wrappers for the framework CoreText on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-CoreText-10.2.tar.gz", hash = "sha256:59ef8ca8d88bb53ce9980dda0b8094daa3e2dabe355847365ba965ff0b49f961"}, + {file = "pyobjc_framework_CoreText-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:44052f752f42b62d342fa8aced5d1b8928831e70830eccddc594726d40500d5c"}, + {file = "pyobjc_framework_CoreText-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0bc278f509a3fd3eea89124d81e77de11af10167c0df0d0cc15a369f060465a0"}, + {file = "pyobjc_framework_CoreText-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7b819119dc859e49c0ce9040ae09d6a3bd66658003793f486ef5a21e46a2d34f"}, + {file = "pyobjc_framework_CoreText-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2719c57ff08af6e4fdcddd0fa5eda56113808a1690c3325f1c6926740817f9a1"}, + {file = "pyobjc_framework_CoreText-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:8239ce92f9496587a60fc1bfd4994136832bad99405bb45572f92d960cbe746e"}, + {file = "pyobjc_framework_CoreText-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:80a1d207fcdb2999841daa430c83d760ac1a3f2f65c605949fc5ff789425b1f6"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" +pyobjc-framework-Quartz = ">=10.2" + +[[package]] +name = "pyobjc-framework-quartz" +version = "10.2" +description = "Wrappers for the Quartz frameworks on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-Quartz-10.2.tar.gz", hash = "sha256:9b947e081f5bd6cd01c99ab5d62c36500d2d6e8d3b87421c1cbb7f9c885555eb"}, + {file = "pyobjc_framework_Quartz-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bc0ab739259a717d9d13a739434991b54eb8963ad7c27f9f6d04d68531fb479b"}, + {file = "pyobjc_framework_Quartz-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a74d00e933c1e1a1820839323dc5cf252bee8bb98e2a298d961f7ae7905ce71"}, + {file = "pyobjc_framework_Quartz-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3e8e33246d966c2bd7f5ee2cf3b431582fa434a6ec2b6dbe580045ebf1f55be5"}, + {file = "pyobjc_framework_Quartz-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c6ca490eff1be0dd8dc7726edde79c97e21ec1afcf55f75962a79e27b4eb2961"}, + {file = "pyobjc_framework_Quartz-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d3d54d9fa50de09ee8994248151def58f30b4738eb20755b0bdd5ee1e1f5883d"}, + {file = "pyobjc_framework_Quartz-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:520c8031b2389110f80070b078dde1968caaecb10921f8070046c26132ac9286"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" + +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-print" +version = "1.0.0" +description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_print-1.0.0-py3-none-any.whl", hash = "sha256:23484f42b906b87e31abd564761efffeb0348a6f83109fb857ee6e8e5df42b69"}, + {file = "pytest_print-1.0.0.tar.gz", hash = "sha256:1fcde9945fba462227a8959271369b10bb7a193be8452162707e63cd60875ca0"}, +] + +[package.dependencies] +pytest = ">=7.4" + +[package.extras] +test = ["covdefaults (>=2.3)", "coverage (>=7.3)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-xlib" +version = "0.33" +description = "Python X Library" +optional = false +python-versions = "*" +files = [ + {file = "python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32"}, + {file = "python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398"}, +] + +[package.dependencies] +six = ">=1.10.0" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "qtawesome" +version = "0.7.3" +description = "FontAwesome icons in PyQt and PySide applications" +optional = false +python-versions = "*" +files = [ + {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, + {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, +] + +[package.dependencies] +qtpy = "*" +six = "*" + +[[package]] +name = "qtpy" +version = "2.4.1" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "QtPy-2.4.1-py3-none-any.whl", hash = "sha256:1c1d8c4fa2c884ae742b069151b0abe15b3f70491f3972698c683b8e38de839b"}, + {file = "QtPy-2.4.1.tar.gz", hash = "sha256:a5a15ffd519550a1361bdc56ffc07fda56a6af7292f17c7b395d4083af632987"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] + +[[package]] +name = "referencing" +version = "0.34.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rpds-py" +version = "0.18.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, + {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, + {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, + {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, + {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, + {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, + {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, + {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, + {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, + {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, + {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, + {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, + {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, +] + +[[package]] +name = "ruff" +version = "0.2.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, +] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "speedcopy" +version = "2.1.5" +description = "Replacement or alternative for python copyfile() utilizing server side copy on network shares for faster copying." +optional = false +python-versions = "*" +files = [ + {file = "speedcopy-2.1.5-py3-none-any.whl", hash = "sha256:903d0b466c2bef7c07dfac17493cdfbc09aadd70e947199c81caa6c6da2c095f"}, + {file = "speedcopy-2.1.5.tar.gz", hash = "sha256:9d6c482300791f02462ad451730ae247901978eae9e25290ca352de964698c82"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20240316" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, +] + +[[package]] +name = "unidecode" +version = "1.3.8" +description = "ASCII transliterations of Unicode text" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, + {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wsrpc-aiohttp" +version = "3.2.0" +description = "WSRPC is the RPC over WebSocket for aiohttp" +optional = false +python-versions = ">3.5.*, <4" +files = [ + {file = "wsrpc-aiohttp-3.2.0.tar.gz", hash = "sha256:f467abc51bcdc760fc5aeb7041abdeef46eeca3928dc43dd6e7fa7a533563818"}, + {file = "wsrpc_aiohttp-3.2.0-py3-none-any.whl", hash = "sha256:fa9b0bf5cb056898cb5c9f64cbc5eacb8a5dd18ab1b7f0cd4a2208b4a7fde282"}, +] + +[package.dependencies] +aiohttp = "<4" +yarl = "*" + +[package.extras] +develop = ["Sphinx", "async-timeout", "coverage (!=4.3)", "coveralls", "pytest", "pytest-aiohttp", "pytest-cov", "sphinxcontrib-plantuml", "tox (>=2.4)"] +testing = ["async-timeout", "coverage (!=4.3)", "coveralls", "pytest", "pytest-aiohttp", "pytest-cov"] +ujson = ["ujson"] + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9.1,<3.10" +content-hash = "9904c684f7885d871c560a189ff074527231d44f7bf4b221290ae646102f00f1" From e22eff05b7884f10af133115952c13bf5923e519 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:09:01 +0100 Subject: [PATCH 027/284] renamed 'childs' key to 'children' --- .../plugins/publish/collect_shot_instances.py | 2 +- .../plugins/publish/collect_anatomy_instance_data.py | 8 ++++++-- client/ayon_core/plugins/publish/collect_hierarchy.py | 7 +++---- .../plugins/publish/extract_hierarchy_to_ayon.py | 6 +++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py index f1e5e83f0a..e3f8bfebba 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -177,7 +177,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): next_dict = { parent_name: { "entity_type": parent["entity_type"], - "childs": actual + "children": actual } } actual = next_dict diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index e3b27a0db5..f8cc81e718 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -465,7 +465,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): current_data = hierarchy_context.get(project_name, {}) for key in folder_path.split("/"): if key: - current_data = current_data.get("childs", {}).get(key, {}) + current_data = ( + current_data + .get("children", {}) + .get(key, {}) + ) tasks_info = current_data.get("tasks", {}) task_info = tasks_info.get(task_name, {}) @@ -529,5 +533,5 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): return item[folder_name].get("tasks") or {} for subitem in item.values(): - hierarchy_queue.extend(subitem.get("childs") or []) + hierarchy_queue.extend(subitem.get("children") or []) return {} diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index d7f0777ba4..c74378bbd3 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -68,7 +68,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): next_dict[parent_name] = {} next_dict[parent_name]["entity_type"] = parent[ "entity_type"].capitalize() - next_dict[parent_name]["childs"] = actual + next_dict[parent_name]["children"] = actual actual = next_dict temp_context = self._update_dict(temp_context, actual) @@ -77,7 +77,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not temp_context: return - final_context[project_name]['childs'] = temp_context + final_context[project_name]['children'] = temp_context # adding hierarchy context to context context.data["hierarchyContext"] = final_context @@ -85,8 +85,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): context.data["hierarchyContext"])) def _update_dict(self, parent_dict, child_dict): - """ - Nesting each children into its parent. + """Nesting each child into its parent. Args: parent_dict (dict): parent dict wich should be nested with children diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 50b2bf7c0b..d43dcab28c 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -237,7 +237,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): hierarchy_context = copy.deepcopy(context.data["hierarchyContext"]) for key, value in hierarchy_context.items(): project_item = copy.deepcopy(value) - project_children_context = project_item.pop("childs", None) + project_children_context = project_item.pop("children", None) project_item["name"] = key project_item["tasks"] = [] project_item["attributes"] = project_item.pop( @@ -265,7 +265,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): folder_path = "{}/{}".format(parent_path, folder_name) if ( folder_path not in active_folder_paths - and not folder_info.get("childs") + and not folder_info.get("children") ): continue @@ -273,7 +273,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): new_item = copy.deepcopy(folder_info) new_item["name"] = folder_name new_item["children"] = [] - new_children_context = new_item.pop("childs", None) + new_children_context = new_item.pop("children", None) tasks = new_item.pop("tasks", {}) task_items = [] for task_name, task_info in tasks.items(): From a29809a8138e91d0a7640854a8b4fac9b2941b96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:11:31 +0100 Subject: [PATCH 028/284] add 'folder_type' to data next to 'entity_type' --- .../plugins/publish/collect_shot_instances.py | 15 ++++++++------- .../plugins/publish/collect_hierarchy.py | 6 ++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py index e3f8bfebba..5a2f5cbc20 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -154,7 +154,8 @@ class CollectShotInstance(pyblish.api.InstancePlugin): handle_end = int(instance.data["handleEnd"]) in_info = { - "entity_type": "Shot", + "entity_type": "folder", + "folder_type": "Shot", "attributes": { "handleStart": handle_start, "handleEnd": handle_end, @@ -174,13 +175,13 @@ class CollectShotInstance(pyblish.api.InstancePlugin): for parent in reversed(parents): parent_name = parent["entity_name"] - next_dict = { - parent_name: { - "entity_type": parent["entity_type"], - "children": actual - } + parent_info = { + "entity_type": parent["entity_type"], + "children": actual, } - actual = next_dict + if parent_info["entity_type"] == "folder": + parent_info["folder_type"] = parent["folder_type"] + actual = {parent_name: parent_info} final_context = self._update_dict(final_context, actual) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index c74378bbd3..a16ca27e5c 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -41,8 +41,9 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not instance.data.get("heroTrack"): continue + shot_data["entity_type"] = "folder" # suppose that all instances are Shots - shot_data['entity_type'] = 'Shot' + shot_data["folder_type"] = "Shot" shot_data['tasks'] = instance.data.get("tasks") or {} shot_data["comments"] = instance.data.get("comments", []) @@ -66,7 +67,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): next_dict = {} parent_name = parent["entity_name"] next_dict[parent_name] = {} - next_dict[parent_name]["entity_type"] = parent[ + next_dict[parent_name]["entity_type"] = "folder" + next_dict[parent_name]["folder_type"] = parent[ "entity_type"].capitalize() next_dict[parent_name]["children"] = actual actual = next_dict From a0dc2d7bffdab00925dde7dbbdaedb244731cb8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:11:58 +0100 Subject: [PATCH 029/284] refactored how data structure in collect hierarchy is created --- .../plugins/publish/collect_hierarchy.py | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index a16ca27e5c..0751bf305b 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -17,17 +17,19 @@ class CollectHierarchy(pyblish.api.ContextPlugin): hosts = ["resolve", "hiero", "flame"] def process(self, context): - temp_context = {} project_name = context.data["projectName"] - final_context = {} - final_context[project_name] = {} - final_context[project_name]["entity_type"] = "project" + temp_context = {} + final_context = { + project_name: { + "entity_type": "project", + "children": temp_context + }, + } for instance in context: self.log.debug("Processing instance: `{}` ...".format(instance)) # shot data dict - shot_data = {} product_type = instance.data["productType"] families = instance.data["families"] @@ -41,23 +43,25 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not instance.data.get("heroTrack"): continue - shot_data["entity_type"] = "folder" - # suppose that all instances are Shots - shot_data["folder_type"] = "Shot" - shot_data['tasks'] = instance.data.get("tasks") or {} - shot_data["comments"] = instance.data.get("comments", []) - - shot_data['attributes'] = { - "handleStart": instance.data["handleStart"], - "handleEnd": instance.data["handleEnd"], - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "clipIn": instance.data["clipIn"], - "clipOut": instance.data["clipOut"], - "fps": instance.data["fps"], - "resolutionWidth": instance.data["resolutionWidth"], - "resolutionHeight": instance.data["resolutionHeight"], - "pixelAspect": instance.data["pixelAspect"] + shot_data = { + "entity_type": "folder", + # WARNING Default folder type is hardcoded + # suppose that all instances are Shots + "folder_type": "Shot", + "tasks": instance.data.get("tasks") or {}, + "comments": instance.data.get("comments", []), + "attributes": { + "handleStart": instance.data["handleStart"], + "handleEnd": instance.data["handleEnd"], + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "clipIn": instance.data["clipIn"], + "clipOut": instance.data["clipOut"], + "fps": instance.data["fps"], + "resolutionWidth": instance.data["resolutionWidth"], + "resolutionHeight": instance.data["resolutionHeight"], + "pixelAspect": instance.data["pixelAspect"], + }, } # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] @@ -71,6 +75,16 @@ class CollectHierarchy(pyblish.api.ContextPlugin): next_dict[parent_name]["folder_type"] = parent[ "entity_type"].capitalize() next_dict[parent_name]["children"] = actual + + for parent in reversed(instance.data["parents"]): + parent_name = parent["entity_name"] + next_dict = { + parent_name: { + "entity_type": "folder", + "folder_type": parent["entity_type"].capitalize(), + "children": actual + } + } actual = next_dict temp_context = self._update_dict(temp_context, actual) @@ -79,8 +93,6 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not temp_context: return - final_context[project_name]['children'] = temp_context - # adding hierarchy context to context context.data["hierarchyContext"] = final_context self.log.debug("context.data[hierarchyContext] is: {}".format( From 0e6c050618322244f37fea0ccb0cc688f9dd003b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 18 Mar 2024 15:28:33 +0100 Subject: [PATCH 030/284] :recycle: update ruff and fix python version --- poetry.lock | 38 +++++++++++++++++++------------------- pyproject.toml | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index adfa083e82..d2476400f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1207,28 +1207,28 @@ files = [ [[package]] name = "ruff" -version = "0.2.2" +version = "0.3.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] @@ -1465,4 +1465,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "9904c684f7885d871c560a189ff074527231d44f7bf4b221290ae646102f00f1" +content-hash = "8a62cc31c960aff7e5df7bfdc5b65790e57bf0e7a87fedd16a68ababa49268c8" diff --git a/pyproject.toml b/pyproject.toml index f9c0f4602c..29213281af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ pytest = "^8.0" pytest-print = "^1.0" ayon-python-api = "^1.0" arrow = "^1.3.0" -ruff = "^0.2.2" +ruff = "^0.3.3" [tool.ruff] @@ -67,7 +67,7 @@ exclude = [ line-length = 79 indent-width = 4 -# Assume Python 3.8 +# Assume Python 3.9 target-version = "py39" [tool.ruff.lint] From 65af945b7d1baf9d8f2fad2aa46023d2e417b322 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:29:01 +0100 Subject: [PATCH 031/284] remove forgotten line --- client/ayon_core/pipeline/anatomy/templates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 63dfd9b1b0..7ea2c8e004 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -379,7 +379,6 @@ class TemplateCategory: return key -class AnatomyTemplates(TemplatesDict): class AnatomyTemplates: inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") From bf60b264def3f45162016b255715aec64152a31b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:42:14 +0100 Subject: [PATCH 032/284] 'get_template' raises 'KeyError' but have option to pass in 'default' --- .../ayon_core/pipeline/anatomy/templates.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 7ea2c8e004..e9401c1501 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -560,31 +560,56 @@ class AnatomyTemplates: """ return self.format(in_data, strict=False) - def get_template(self, category_name, template_name, subkey=None): + def get_template( + self, category_name, template_name, subkey=None, default=_PLACEHOLDER + ): """Get template item from category. Args: category_name (str): Category name. template_name (str): Template name. subkey (Optional[str]): Subkey name. + default (Any): Default value if template is not found. Returns: Any: Template item or subkey value. + Raises: + KeyError: When any passed key is not available. Raise of error + does not happen if 'default' is filled. + """ self._validate_discovery() category = self.get(category_name) if category is None: - return None + if default is not _PLACEHOLDER: + return default + raise KeyError("Category '{}' not found.".format(category_name)) template_item = category.get(template_name) if template_item is None: - return template_item + if default is not _PLACEHOLDER: + return default + raise KeyError( + "Template '{}' not found in category '{}'.".format( + template_name, category_name + ) + ) if subkey is None: return template_item - return template_item.get(subkey) + item = template_item.get(subkey) + if item is not None: + return item + + if default is not _PLACEHOLDER: + return default + raise KeyError( + "Subkey '{}' not found in '{}/{}'.".format( + subkey, category_name, template_name + ) + ) def _solve_dict(self, data, strict): """ Solves templates with entered data. From eee55221a2ec5d2da55477cc786bad155fd4770e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:43:33 +0100 Subject: [PATCH 033/284] fix cases when template key is name of the category --- client/ayon_core/pipeline/anatomy/templates.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index e9401c1501..7becb5026e 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -372,6 +372,12 @@ class TemplateCategory: """ if key in self._category_data: return key + + # Use default when the key is the category name + if key == self._name: + return "default" + + # Remove prefix if is key prefixed if key.startswith(self._name_prefix): new_key = key[len(self._name_prefix):] if new_key in self._category_data: From e70786ce15c27f78e1b629133d65c769c4959a52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:45:26 +0100 Subject: [PATCH 034/284] modify default settings --- server/settings/tools.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index b45f9b49d4..488d27e8f1 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -410,14 +410,14 @@ DEFAULT_TOOLS_VALUES = { { "task_types": [], "hosts": [], - "workfile_template": "work" + "workfile_template": "default" }, { "task_types": [], "hosts": [ "unreal" ], - "workfile_template": "work_unreal" + "workfile_template": "unreal" } ], "last_workfile_on_startup": [ @@ -457,7 +457,7 @@ DEFAULT_TOOLS_VALUES = { "hosts": [], "task_types": [], "task_names": [], - "template_name": "publish" + "template_name": "default" }, { "product_types": [ @@ -468,7 +468,7 @@ DEFAULT_TOOLS_VALUES = { "hosts": [], "task_types": [], "task_names": [], - "template_name": "publish_render" + "template_name": "render" }, { "product_types": [ @@ -479,7 +479,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_simpleUnrealTexture" + "template_name": "simpleUnrealTexture" }, { "product_types": [ @@ -491,7 +491,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_maya2unreal" + "template_name": "maya2unreal" }, { "product_types": [ @@ -502,7 +502,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_online" + "template_name": "online" }, { "product_types": [ @@ -513,7 +513,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_tycache" + "template_name": "tycache" } ], "hero_template_name_profiles": [ @@ -526,7 +526,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "hero_simpleUnrealTextureHero" + "template_name": "simpleUnrealTextureHero" } ] } From 77b3cd1c534e2fdf7fa26e16bfc123cb537791a7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:47:54 +0100 Subject: [PATCH 035/284] add missing placeholder object --- client/ayon_core/pipeline/anatomy/templates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 7becb5026e..7d9a1255a3 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -16,6 +16,8 @@ from .exceptions import ( ) from .roots import RootItem +_PLACEHOLDER = object() + class AnatomyTemplateResult(TemplateResult): rootless = None From b66dded41125ea5fe8eed4629afd6fa329680ae9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 15:55:37 +0100 Subject: [PATCH 036/284] fix file template conversion --- client/ayon_core/hosts/photoshop/api/launch_logic.py | 5 +++-- .../ayon_core/hosts/tvpaint/plugins/load/load_workfile.py | 4 ++-- client/ayon_core/lib/applications.py | 4 +++- client/ayon_core/tools/workfiles/models/workfiles.py | 6 ++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index ccb7b0048b..9c6a1e8a01 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -397,9 +397,10 @@ class PhotoshopRoute(WebSocketRoute): # Define saving file extension extensions = host.get_workfile_extensions() - work_root = work_template["directory"].format_strict(data) + work_root = str(work_template["directory"].format_strict(data)) + file_template = str(work_template["file"]) last_workfile_path = get_last_workfile( - work_root, work_template["file"], data, extensions, True + work_root, file_template, data, extensions, True ) return last_workfile_path diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py index 16cb54f4a3..0b6d67a840 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py @@ -93,9 +93,9 @@ class LoadWorkfile(plugin.Loader): data["ext"] = extension.lstrip(".") - work_root = work_template["directory"].format_strict(data) + work_root = str(work_template["directory"].format_strict(data)) version = get_last_workfile_with_version( - work_root, work_template["file"], data, extensions + work_root, str(work_template["file"]), data, extensions )[1] if version is None: diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index c4f1d168b5..123396f5f6 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1862,7 +1862,9 @@ def _prepare_last_workfile(data, workdir, addons_manager): project_settings=project_settings ) # Find last workfile - file_template = anatomy.get_template("work", template_key, "file") + file_template = str( + anatomy.get_template("work", template_key, "file") + ) workdir_data.update({ "version": 1, diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index f4ca08a45a..ef502e3d6f 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -200,7 +200,7 @@ class WorkareaModel: self, workdir, file_template, fill_data, extensions ): version = get_last_workfile_with_version( - workdir, str(file_template), fill_data, extensions + workdir, file_template, fill_data, extensions )[1] if version is None: @@ -300,7 +300,9 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - file_template = anatomy.get_template("work", template_key, "file") + file_template = str( + anatomy.get_template("work", template_key, "file") + ) comment_hints, comment = self._get_comments_from_root( file_template, From 52e9993b4a58b77b1b5951095910ab30ef56b0ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Mar 2024 16:03:37 +0100 Subject: [PATCH 037/284] change create and publish logic in hosts to set 'folder_type' correctly --- client/ayon_core/hosts/flame/api/plugin.py | 10 +++++----- .../plugins/publish/collect_timeline_instances.py | 6 ++++++ client/ayon_core/hosts/hiero/api/plugin.py | 10 +++++----- .../hiero/plugins/publish/precollect_instances.py | 5 +++++ client/ayon_core/hosts/resolve/api/plugin.py | 6 +++--- .../resolve/plugins/publish/precollect_instances.py | 5 +++++ client/ayon_core/hosts/traypublisher/api/editorial.py | 6 ++++-- 7 files changed, 33 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/client/ayon_core/hosts/flame/api/plugin.py index c5667eb75a..20edba2d6b 100644 --- a/client/ayon_core/hosts/flame/api/plugin.py +++ b/client/ayon_core/hosts/flame/api/plugin.py @@ -644,13 +644,13 @@ class PublishableClip: "families": [self.base_product_type, self.product_type] } - def _convert_to_entity(self, type, template): + def _convert_to_entity(self, src_type, template): """ Converting input key to key with type. """ # convert to entity type - entity_type = self.types.get(type, None) + folder_type = self.types.get(src_type, None) - assert entity_type, "Missing entity type for `{}`".format( - type + assert folder_type, "Missing folder type for `{}`".format( + src_type ) # first collect formatting data to use for formatting template @@ -661,7 +661,7 @@ class PublishableClip: formatting_data[_k] = value return { - "entity_type": entity_type, + "folder_type": folder_type, "entity_name": template.format( **formatting_data ) diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py index 9d6560023c..47632ecf3d 100644 --- a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -100,6 +100,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): marker_data["handleEnd"] = min( marker_data["handleEnd"], tail) + # Backward compatibility fix of 'entity_type' > 'folder_type' + if "parents" in marker_data: + for parent in marker_data["parents"]: + if "entity_type" in parent: + parent["folder_type"] = parent.pop("entity_type") + workfile_start = self._set_workfile_start(marker_data) with_audio = bool(marker_data.pop("audio")) diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 6a665dc9c5..721ade09b2 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -909,13 +909,13 @@ class PublishClip: "families": [self.product_type, self.data["family"]] } - def _convert_to_entity(self, type, template): + def _convert_to_entity(self, src_type, template): """ Converting input key to key with type. """ # convert to entity type - entity_type = self.types.get(type, None) + folder_type = self.types.get(src_type, None) - assert entity_type, "Missing entity type for `{}`".format( - type + assert folder_type, "Missing folder type for `{}`".format( + src_type ) # first collect formatting data to use for formatting template @@ -926,7 +926,7 @@ class PublishClip: formatting_data[_k] = value return { - "entity_type": entity_type, + "folder_type": folder_type, "entity_name": template.format( **formatting_data ) diff --git a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py index d921f37934..2d3689f001 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py @@ -84,6 +84,11 @@ class PrecollectInstances(pyblish.api.ContextPlugin): k: v for k, v in tag_data.items() if k not in ("id", "applieswhole", "label") }) + # Backward compatibility fix of 'entity_type' > 'folder_type' + if "parents" in data: + for parent in data["parents"]: + if "entity_type" in parent: + parent["folder_type"] = parent.pop("entity_type") asset, asset_name = self._get_folder_data(tag_data) diff --git a/client/ayon_core/hosts/resolve/api/plugin.py b/client/ayon_core/hosts/resolve/api/plugin.py index 8c97df98b8..e3e2715bad 100644 --- a/client/ayon_core/hosts/resolve/api/plugin.py +++ b/client/ayon_core/hosts/resolve/api/plugin.py @@ -873,14 +873,14 @@ class PublishClip: def _convert_to_entity(self, key): """ Converting input key to key with type. """ # convert to entity type - entity_type = self.types.get(key) + folder_type = self.types.get(key) - assert entity_type, "Missing entity type for `{}`".format( + assert folder_type, "Missing folder type for `{}`".format( key ) return { - "entity_type": entity_type, + "folder_type": folder_type, "entity_name": self.hierarchy_data[key]["value"].format( **self.timeline_item_default_data ) diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py index 72ecd3669d..caa79c85c0 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py @@ -64,6 +64,11 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) folder_path = tag_data["folder_path"] + # Backward compatibility fix of 'entity_type' > 'folder_type' + if "parents" in data: + for parent in data["parents"]: + if "entity_type" in parent: + parent["folder_type"] = parent.pop("entity_type") # TODO: remove backward compatibility product_name = tag_data.get("productName") diff --git a/client/ayon_core/hosts/traypublisher/api/editorial.py b/client/ayon_core/hosts/traypublisher/api/editorial.py index 8dedec7398..92a7b315f8 100644 --- a/client/ayon_core/hosts/traypublisher/api/editorial.py +++ b/client/ayon_core/hosts/traypublisher/api/editorial.py @@ -193,7 +193,8 @@ class ShotMetadataSolver: continue parents.append({ - "entity_type": parent_token_type, + "entity_type": "folder", + "folder_type": parent_token_type, "entity_name": parent_name }) @@ -264,7 +265,8 @@ class ShotMetadataSolver: }] for entity in folders_hierarchy: output.append({ - "entity_type": entity["folderType"], + "entity_type": "folder", + "folder_type": entity["folderType"], "entity_name": entity["name"] }) return output From ab71411d122220ecd2edf4d5bde4ebe04f1a889e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 19 Mar 2024 11:37:15 +0100 Subject: [PATCH 038/284] :recycle: remove unneeded dependencies --- poetry.lock | 906 +------------------------------------------------ pyproject.toml | 18 +- 2 files changed, 5 insertions(+), 919 deletions(-) diff --git a/poetry.lock b/poetry.lock index d2476400f7..be5a3b2c2c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,145 +1,5 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. -[[package]] -name = "aiohttp" -version = "3.9.3" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, -] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] - -[[package]] -name = "aiohttp-json-rpc" -version = "0.13.3" -description = "Implementation JSON-RPC 2.0 server and client using aiohttp on top of websockets transport" -optional = false -python-versions = ">=3.5" -files = [ - {file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"}, - {file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"}, -] - -[package.dependencies] -aiohttp = ">=3,<4" - -[[package]] -name = "aiohttp-middlewares" -version = "2.3.0" -description = "Collection of useful middlewares for aiohttp applications." -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "aiohttp_middlewares-2.3.0-py3-none-any.whl", hash = "sha256:4424b136a351b67b4c93da7d7505b56a342addaa324be30793f6cba463d18ac8"}, - {file = "aiohttp_middlewares-2.3.0.tar.gz", hash = "sha256:b2564c1dfa8dbcf7d2e101a6a03dcaad45464744531c269e8e582cb2dc551d08"}, -] - -[package.dependencies] -aiohttp = ">=3.8.1,<4.0.0" -async-timeout = ">=4.0.2,<5.0.0" -yarl = ">=1.5.1,<2.0.0" - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - [[package]] name = "appdirs" version = "1.4.4" @@ -151,55 +11,6 @@ files = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] name = "ayon-python-api" version = "1.0.1" @@ -338,36 +149,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "clique" -version = "1.6.1" -description = "Manage collections with common numerical component" -optional = false -python-versions = ">=2.7, <4.0" -files = [ - {file = "clique-1.6.1-py2.py3-none-any.whl", hash = "sha256:8619774fa035661928dd8c93cd805acf2d42533ccea1b536c09815ed426c9858"}, - {file = "clique-1.6.1.tar.gz", hash = "sha256:90165c1cf162d4dd1baef83ceaa1afc886b453e379094fa5b60ea470d1733e66"}, -] - -[package.extras] -dev = ["lowdown (>=0.2.0,<1)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] -doc = ["lowdown (>=0.2.0,<1)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] -test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)"] - [[package]] name = "codespell" version = "2.2.6" @@ -407,16 +188,6 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] -[[package]] -name = "evdev" -version = "1.7.0" -description = "Bindings to the Linux input handling subsystem" -optional = false -python-versions = ">=3.6" -files = [ - {file = "evdev-1.7.0.tar.gz", hash = "sha256:95bd2a1e0c6ce2cd7a2ecc6e6cd9736ff794b3ad5cb54d81d8cbc2e414d0b870"}, -] - [[package]] name = "exceptiongroup" version = "1.2.0" @@ -447,92 +218,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - [[package]] name = "identify" version = "2.5.35" @@ -569,140 +254,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "jsonschema" -version = "4.21.1" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, - {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-specifications" -version = "2023.12.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - [[package]] name = "nodeenv" version = "1.8.0" @@ -776,133 +327,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pyblish-base" -version = "1.8.11" -description = "Plug-in driven automation framework for content" -optional = false -python-versions = "*" -files = [ - {file = "pyblish-base-1.8.11.tar.gz", hash = "sha256:86dfeec0567430eb7eb25f89a18312054147a729ec66f6ac8c7e421fd15b66e1"}, - {file = "pyblish_base-1.8.11-py2.py3-none-any.whl", hash = "sha256:c321be7020c946fe9dfa11941241bd985a572c5009198b4f9810e5afad1f0b4b"}, -] - -[[package]] -name = "pynput" -version = "1.7.6" -description = "Monitor and control user input devices" -optional = false -python-versions = "*" -files = [ - {file = "pynput-1.7.6-py2.py3-none-any.whl", hash = "sha256:19861b2a0c430d646489852f89500e0c9332e295f2c020e7c2775e7046aa2e2f"}, - {file = "pynput-1.7.6.tar.gz", hash = "sha256:3a5726546da54116b687785d38b1db56997ce1d28e53e8d22fc656d8b92e533c"}, -] - -[package.dependencies] -evdev = {version = ">=1.3", markers = "sys_platform in \"linux\""} -pyobjc-framework-ApplicationServices = {version = ">=8.0", markers = "sys_platform == \"darwin\""} -pyobjc-framework-Quartz = {version = ">=8.0", markers = "sys_platform == \"darwin\""} -python-xlib = {version = ">=0.17", markers = "sys_platform in \"linux\""} -six = "*" - -[[package]] -name = "pyobjc-core" -version = "10.2" -description = "Python<->ObjC Interoperability Module" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyobjc-core-10.2.tar.gz", hash = "sha256:0153206e15d0e0d7abd53ee8a7fbaf5606602a032e177a028fc8589516a8771c"}, - {file = "pyobjc_core-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8eab50ce7f17017a0f1d68c3b7e88bb1bb033415fdff62b8e0a9ee4ab72f242"}, - {file = "pyobjc_core-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f2115971463073426ab926416e17e5c16de5b90d1a1f2a2d8724637eb1c21308"}, - {file = "pyobjc_core-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a70546246177c23acb323c9324330e37638f1a0a3d13664abcba3bb75e43012c"}, - {file = "pyobjc_core-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a9b5a215080d13bd7526031d21d5eb27a410780878d863f486053a0eba7ca9a5"}, - {file = "pyobjc_core-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:eb1ab700a44bcc4ceb125091dfaae0b998b767b49990df5fdc83eb58158d8e3f"}, - {file = "pyobjc_core-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a7163aff9c47d654f835f80361c1b112886ec754800d34e75d1e02ff52c3d7"}, -] - -[[package]] -name = "pyobjc-framework-applicationservices" -version = "10.2" -description = "Wrappers for the framework ApplicationServices on macOS" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyobjc-framework-ApplicationServices-10.2.tar.gz", hash = "sha256:f83d6ed3320afb6648be6defafe0f05bac00d0281fc84ee4766ff977309b659f"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2aebfed888f9bcb4f11d93f9ef9a76d561e92848dcb6011da5d5e9d3593371be"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edfd3153e64ee9573bcff7ccaa1fbbbd6964658f187464c461ad34f24552bc85"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d2c89b246c19a041221ff36e9121c92e86a4422016f809a40f5ce3d647882d9"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee1e69947f31aad5fdec44921ce37f7f921faf50a0ceb27ed40b6d54f4b15d0e"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:101f5b09d71e55bd39e6e91f0787433805d422622336b72fde969a7c54528045"}, - {file = "pyobjc_framework_ApplicationServices-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a3ef00c9aea09c5ef5840b8749d0753249869bc30e124145b763cd0b4b81155"}, -] - -[package.dependencies] -pyobjc-core = ">=10.2" -pyobjc-framework-Cocoa = ">=10.2" -pyobjc-framework-CoreText = ">=10.2" -pyobjc-framework-Quartz = ">=10.2" - -[[package]] -name = "pyobjc-framework-cocoa" -version = "10.2" -description = "Wrappers for the Cocoa frameworks on macOS" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyobjc-framework-Cocoa-10.2.tar.gz", hash = "sha256:6383141379636b13855dca1b39c032752862b829f93a49d7ddb35046abfdc035"}, - {file = "pyobjc_framework_Cocoa-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9227b4f271fda2250f5a88cbc686ff30ae02c0f923bb7854bb47972397496b2"}, - {file = "pyobjc_framework_Cocoa-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a6042b7703bdc33b7491959c715c1e810a3f8c7a560c94b36e00ef321480797"}, - {file = "pyobjc_framework_Cocoa-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:18886d5013cd7dc7ecd6e0df5134c767569b5247fc10a5e293c72ee3937b217b"}, - {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ecf01400ee698d2e0ff4c907bcf9608d9d710e97203fbb97b37d208507a9362"}, - {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0def036a7b24e3ae37a244c77bec96b7c9c8384bf6bb4d33369f0a0c8807a70d"}, - {file = "pyobjc_framework_Cocoa-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f47ecc393bc1019c4b47e8653207188df784ac006ad54d8c2eb528906ff7013"}, -] - -[package.dependencies] -pyobjc-core = ">=10.2" - -[[package]] -name = "pyobjc-framework-coretext" -version = "10.2" -description = "Wrappers for the framework CoreText on macOS" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyobjc-framework-CoreText-10.2.tar.gz", hash = "sha256:59ef8ca8d88bb53ce9980dda0b8094daa3e2dabe355847365ba965ff0b49f961"}, - {file = "pyobjc_framework_CoreText-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:44052f752f42b62d342fa8aced5d1b8928831e70830eccddc594726d40500d5c"}, - {file = "pyobjc_framework_CoreText-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0bc278f509a3fd3eea89124d81e77de11af10167c0df0d0cc15a369f060465a0"}, - {file = "pyobjc_framework_CoreText-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7b819119dc859e49c0ce9040ae09d6a3bd66658003793f486ef5a21e46a2d34f"}, - {file = "pyobjc_framework_CoreText-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2719c57ff08af6e4fdcddd0fa5eda56113808a1690c3325f1c6926740817f9a1"}, - {file = "pyobjc_framework_CoreText-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:8239ce92f9496587a60fc1bfd4994136832bad99405bb45572f92d960cbe746e"}, - {file = "pyobjc_framework_CoreText-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:80a1d207fcdb2999841daa430c83d760ac1a3f2f65c605949fc5ff789425b1f6"}, -] - -[package.dependencies] -pyobjc-core = ">=10.2" -pyobjc-framework-Cocoa = ">=10.2" -pyobjc-framework-Quartz = ">=10.2" - -[[package]] -name = "pyobjc-framework-quartz" -version = "10.2" -description = "Wrappers for the Quartz frameworks on macOS" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyobjc-framework-Quartz-10.2.tar.gz", hash = "sha256:9b947e081f5bd6cd01c99ab5d62c36500d2d6e8d3b87421c1cbb7f9c885555eb"}, - {file = "pyobjc_framework_Quartz-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bc0ab739259a717d9d13a739434991b54eb8963ad7c27f9f6d04d68531fb479b"}, - {file = "pyobjc_framework_Quartz-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a74d00e933c1e1a1820839323dc5cf252bee8bb98e2a298d961f7ae7905ce71"}, - {file = "pyobjc_framework_Quartz-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3e8e33246d966c2bd7f5ee2cf3b431582fa434a6ec2b6dbe580045ebf1f55be5"}, - {file = "pyobjc_framework_Quartz-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c6ca490eff1be0dd8dc7726edde79c97e21ec1afcf55f75962a79e27b4eb2961"}, - {file = "pyobjc_framework_Quartz-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d3d54d9fa50de09ee8994248151def58f30b4738eb20755b0bdd5ee1e1f5883d"}, - {file = "pyobjc_framework_Quartz-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:520c8031b2389110f80070b078dde1968caaecb10921f8070046c26132ac9286"}, -] - -[package.dependencies] -pyobjc-core = ">=10.2" -pyobjc-framework-Cocoa = ">=10.2" - [[package]] name = "pytest" version = "8.1.1" @@ -942,34 +366,6 @@ pytest = ">=7.4" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3)", "pytest-mock (>=3.11.1)"] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-xlib" -version = "0.33" -description = "Python X Library" -optional = false -python-versions = "*" -files = [ - {file = "python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32"}, - {file = "python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398"}, -] - -[package.dependencies] -six = ">=1.10.0" - [[package]] name = "pyyaml" version = "6.0.1" @@ -1029,53 +425,6 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -[[package]] -name = "qtawesome" -version = "0.7.3" -description = "FontAwesome icons in PyQt and PySide applications" -optional = false -python-versions = "*" -files = [ - {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, - {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, -] - -[package.dependencies] -qtpy = "*" -six = "*" - -[[package]] -name = "qtpy" -version = "2.4.1" -description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." -optional = false -python-versions = ">=3.7" -files = [ - {file = "QtPy-2.4.1-py3-none-any.whl", hash = "sha256:1c1d8c4fa2c884ae742b069151b0abe15b3f70491f3972698c683b8e38de839b"}, - {file = "QtPy-2.4.1.tar.gz", hash = "sha256:a5a15ffd519550a1361bdc56ffc07fda56a6af7292f17c7b395d4083af632987"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] - -[[package]] -name = "referencing" -version = "0.34.0" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, - {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - [[package]] name = "requests" version = "2.31.0" @@ -1097,114 +446,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rpds-py" -version = "0.18.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, - {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, - {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, - {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, - {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, - {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, - {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, - {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, - {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, - {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, - {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, - {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, - {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, -] - [[package]] name = "ruff" version = "0.3.3" @@ -1258,17 +499,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "speedcopy" -version = "2.1.5" -description = "Replacement or alternative for python copyfile() utilizing server side copy on network shares for faster copying." -optional = false -python-versions = "*" -files = [ - {file = "speedcopy-2.1.5-py3-none-any.whl", hash = "sha256:903d0b466c2bef7c07dfac17493cdfbc09aadd70e947199c81caa6c6da2c095f"}, - {file = "speedcopy-2.1.5.tar.gz", hash = "sha256:9d6c482300791f02462ad451730ae247901978eae9e25290ca352de964698c82"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1280,17 +510,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20240316" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, -] - [[package]] name = "unidecode" version = "1.3.8" @@ -1339,130 +558,7 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] -[[package]] -name = "wsrpc-aiohttp" -version = "3.2.0" -description = "WSRPC is the RPC over WebSocket for aiohttp" -optional = false -python-versions = ">3.5.*, <4" -files = [ - {file = "wsrpc-aiohttp-3.2.0.tar.gz", hash = "sha256:f467abc51bcdc760fc5aeb7041abdeef46eeca3928dc43dd6e7fa7a533563818"}, - {file = "wsrpc_aiohttp-3.2.0-py3-none-any.whl", hash = "sha256:fa9b0bf5cb056898cb5c9f64cbc5eacb8a5dd18ab1b7f0cd4a2208b4a7fde282"}, -] - -[package.dependencies] -aiohttp = "<4" -yarl = "*" - -[package.extras] -develop = ["Sphinx", "async-timeout", "coverage (!=4.3)", "coveralls", "pytest", "pytest-aiohttp", "pytest-cov", "sphinxcontrib-plantuml", "tox (>=2.4)"] -testing = ["async-timeout", "coverage (!=4.3)", "coveralls", "pytest", "pytest-aiohttp", "pytest-cov"] -ujson = ["ujson"] - -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "8a62cc31c960aff7e5df7bfdc5b65790e57bf0e7a87fedd16a68ababa49268c8" +content-hash = "1bb724694792fbc2b3c05e3355e6c25305d9f4034eb7b1b4b1791ee95427f8d2" diff --git a/pyproject.toml b/pyproject.toml index 29213281af..2740e9307e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,27 +7,17 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.9.1,<3.10" -aiohttp_json_rpc = "*" # TVPaint server -aiohttp-middlewares = "^2.0.0" -wsrpc_aiohttp = "^3.1.1" # websocket server -Click = "^8" -clique = "1.6.*" -jsonschema = "^4" -pyblish-base = "^1.8.11" -pynput = "^1.7.2" # Timers manager - TODO remove -speedcopy = "^2.1" -six = "^1.15" -qtawesome = "0.7.3" -pre-commit = "^3.6.2" -codespell = "^2.2.6" [tool.poetry.dev-dependencies] +# test dependencies pytest = "^8.0" pytest-print = "^1.0" ayon-python-api = "^1.0" -arrow = "^1.3.0" +# linting dependencies ruff = "^0.3.3" +pre-commit = "^3.6.2" +codespell = "^2.2.6" [tool.ruff] From 945f395a89cdb4e64496f8719592d5a88e1154a7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Mar 2024 14:24:01 +0100 Subject: [PATCH 039/284] rename 'get_template' to 'get_template_item' --- client/ayon_core/pipeline/anatomy/anatomy.py | 7 +++---- client/ayon_core/pipeline/anatomy/templates.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py index 77ba83234f..4c16f20a44 100644 --- a/client/ayon_core/pipeline/anatomy/anatomy.py +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -99,21 +99,20 @@ class BaseAnatomy(object): """Return `AnatomyTemplates` object of current Anatomy instance.""" return self._templates_obj - def get_template(self, category_name, template_name, subkey=None): + def get_template_item(self, *args, **kwargs): """Get template item from category. Args: category_name (str): Category name. template_name (str): Template name. subkey (Optional[str]): Subkey name. + default (Any): Default value. Returns: Any: Template item, subkey value as AnatomyStringTemplate or None. """ - return self._templates_obj.get_template( - category_name, template_name, subkey - ) + return self._templates_obj.get_template_item(*args, **kwargs) def format(self, *args, **kwargs): """Wrap `format` method of Anatomy's `templates_obj`.""" diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py index 7d9a1255a3..46cad385f0 100644 --- a/client/ayon_core/pipeline/anatomy/templates.py +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -568,7 +568,7 @@ class AnatomyTemplates: """ return self.format(in_data, strict=False) - def get_template( + def get_template_item( self, category_name, template_name, subkey=None, default=_PLACEHOLDER ): """Get template item from category. From eaf580bf3b6d533c1f330dcc8cf56d95c20ef6ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Mar 2024 14:24:24 +0100 Subject: [PATCH 040/284] 'root_value_for_template' can handle 'StringTemplate' object --- client/ayon_core/pipeline/anatomy/anatomy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py index 4c16f20a44..658f5fe3fc 100644 --- a/client/ayon_core/pipeline/anatomy/anatomy.py +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -7,7 +7,7 @@ import time import ayon_api -from ayon_core.lib import Logger, get_local_site_id +from ayon_core.lib import Logger, get_local_site_id, StringTemplate from ayon_core.addon import AddonsManager from .exceptions import RootCombinationError, ProjectNotSet @@ -204,6 +204,8 @@ class BaseAnatomy(object): def root_value_for_template(self, template): """Returns value of root key from template.""" + if isinstance(template, StringTemplate): + template = template.template root_templates = [] for group in re.findall(self.root_key_regex, template): root_templates.append("{" + group + "}") From e5fe964178c7fee39f8a62eda98fd5ad583a8213 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Mar 2024 14:26:05 +0100 Subject: [PATCH 041/284] use new method name and fix issues with string conversions --- .../plugins/publish/collect_render_path.py | 6 ++++-- client/ayon_core/hosts/hiero/api/lib.py | 6 ++++-- .../plugins/publish/extract_workfile_xgen.py | 4 +++- .../hosts/maya/plugins/publish/extract_xgen.py | 2 +- .../hosts/photoshop/api/launch_logic.py | 6 +++--- .../tvpaint/plugins/load/load_workfile.py | 6 +++--- .../unreal/hooks/pre_workfile_preparation.py | 2 +- client/ayon_core/lib/applications.py | 6 +++--- .../publish/submit_celaction_deadline.py | 4 +++- .../plugins/publish/submit_fusion_deadline.py | 4 +++- .../plugins/publish/submit_nuke_deadline.py | 4 +++- .../publish/submit_publish_cache_job.py | 2 +- .../plugins/publish/submit_publish_job.py | 2 +- client/ayon_core/pipeline/context_tools.py | 2 +- client/ayon_core/pipeline/delivery.py | 17 ++++++++++------- client/ayon_core/pipeline/farm/tools.py | 2 +- client/ayon_core/pipeline/publish/lib.py | 8 ++++---- client/ayon_core/pipeline/usdlib.py | 4 ++-- .../pipeline/workfile/path_resolving.py | 4 +++- .../plugins/actions/open_file_explorer.py | 4 ++-- .../publish/collect_otio_subset_resources.py | 4 ++-- .../plugins/publish/collect_resources_path.py | 2 +- client/ayon_core/plugins/publish/integrate.py | 4 ++-- .../plugins/publish/integrate_hero_version.py | 12 ++++++++---- .../tools/push_to_project/models/integrate.py | 6 +++--- client/ayon_core/tools/texture_copy/app.py | 2 +- .../tools/workfiles/models/workfiles.py | 18 ++++++++++-------- 27 files changed, 83 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py index a7eef0fce4..52bb183663 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py @@ -33,7 +33,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): m_anatomy_key = self.anatomy_template_key_metadata # get folder and path for rendering images from celaction - r_template_item = anatomy.get_template("publish", r_anatomy_key) + r_template_item = anatomy.get_template_item("publish", r_anatomy_key) render_dir = r_template_item["directory"].format_strict(anatomy_data) render_path = r_template_item["path"].format_strict(anatomy_data) self.log.debug("__ render_path: `{}`".format(render_path)) @@ -50,7 +50,9 @@ class CollectRenderPath(pyblish.api.InstancePlugin): instance.data["path"] = render_path # get anatomy for published renders folder path - m_template_item = anatomy.get_template("publish", m_anatomy_key) + m_template_item = anatomy.get_template_item( + "publish", m_anatomy_key, default=None + ) if m_template_item is not None: metadata_path = m_template_item["directory"].format_strict( anatomy_data diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index 666119593a..8e08e8cbf3 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -632,7 +632,9 @@ def sync_avalon_data_to_workfile(): project_name = get_current_project_name() anatomy = Anatomy(project_name) - work_template = anatomy.get_template("work", "default", "path") + work_template = anatomy.get_template_item( + "work", "default", "path" + ) work_root = anatomy.root_value_for_template(work_template) active_project_root = ( os.path.join(work_root, project_name) @@ -825,7 +827,7 @@ class PublishAction(QtWidgets.QAction): # root_node = hiero.core.nuke.RootNode() # # anatomy = Anatomy(get_current_project_name()) -# work_template = anatomy.get_template("work", "default", "path") +# work_template = anatomy.get_template_item("work", "default", "path") # root_path = anatomy.root_value_for_template(work_template) # # nuke_script.addNode(root_node) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py index a0328725ca..d305b8dc6c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -129,7 +129,9 @@ class ExtractWorkfileXgen(publish.Extractor): template_data = copy.deepcopy(instance.data["anatomyData"]) anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template("publish", "default", "file") + publish_template = anatomy.get_template_item( + "publish", "default", "file" + ) published_maya_path = publish_template.format(template_data) published_basename, _ = os.path.splitext(published_maya_path) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py index eff4dcc881..73668da28d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py @@ -40,7 +40,7 @@ class ExtractXgen(publish.Extractor): template_data = copy.deepcopy(instance.data["anatomyData"]) template_data.update({"ext": "xgen"}) anatomy = instance.context.data["anatomy"] - file_template = anatomy.get_template("publish", "default", "file") + file_template = anatomy.get_template_item("publish", "default", "file") xgen_filename = file_template.format(template_data) xgen_path = os.path.join( diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index 9c6a1e8a01..d0823646d7 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -392,13 +392,13 @@ class PhotoshopRoute(WebSocketRoute): ) data["root"] = anatomy.roots - work_template = anatomy.get_template("work", template_key) + work_template = anatomy.get_template_item("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() - work_root = str(work_template["directory"].format_strict(data)) - file_template = str(work_template["file"]) + work_root = work_template["directory"].format_strict(data) + file_template = work_template["file"].template last_workfile_path = get_last_workfile( work_root, file_template, data, extensions, True ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py index 0b6d67a840..203610288f 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py @@ -80,7 +80,7 @@ class LoadWorkfile(plugin.Loader): ) data["root"] = anatomy.roots - work_template = anatomy.get_template("work", template_key) + work_template = anatomy.get_template_item("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() @@ -93,9 +93,9 @@ class LoadWorkfile(plugin.Loader): data["ext"] = extension.lstrip(".") - work_root = str(work_template["directory"].format_strict(data)) + work_root = work_template["directory"].format_strict(data) version = get_last_workfile_with_version( - work_root, str(work_template["file"]), data, extensions + work_root, work_template["file"].template, data, extensions )[1] if version is None: diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py index 040beccb5e..54ffba3a63 100644 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py @@ -66,7 +66,7 @@ class UnrealPrelaunchHook(PreLaunchHook): self.host_name, ) # Fill templates - template_obj = anatomy.get_template( + template_obj = anatomy.get_template_item( "work", workfile_template_key, "file" ) diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 123396f5f6..58f910ce31 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1862,9 +1862,9 @@ def _prepare_last_workfile(data, workdir, addons_manager): project_settings=project_settings ) # Find last workfile - file_template = str( - anatomy.get_template("work", template_key, "file") - ) + file_template = anatomy.get_template_item( + "work", template_key, "file" + ).template workdir_data.update({ "version": 1, diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py index e11b74ae81..1fae23c9b2 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -75,7 +75,9 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): script_name = os.path.basename(script_path) anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template("publish", "default", "path") + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for item in instance.context: if "workfile" in item.data["productType"]: msg = "Workfile (scene) must be published along" diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index f99a7c6ba3..cf124c0bcc 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -124,7 +124,9 @@ class FusionSubmitDeadline( script_path = context.data["currentFile"] anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template("publish", "default", "path") + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for item in context: if "workfile" in item.data["families"]: msg = "Workfile (scene) must be published along" diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index 193cad1d24..ac01af901c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -198,7 +198,9 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, AbstractSubmitDeadline""" anatomy = context.data["anatomy"] # WARNING Hardcoded template name 'default' > may not be used - publish_template = anatomy.get_template("publish", "default", "path") + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for instance in context: if ( instance.data["productType"] != "workfile" diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index dbc5a12fa8..50bd414587 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -450,7 +450,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_dir_template = anatomy.get_template( + render_dir_template = anatomy.get_template_item( "publish", template_name, "directory" ) return render_dir_template.format_strict(template_data) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index ce1c75e2ee..84bac6d017 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -573,7 +573,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_dir_template = anatomy.get_template( + render_dir_template = anatomy.get_template_item( "publish", template_name, "directory" ) return render_dir_template.format_strict(template_data) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index bf21b43e0b..84a17be8f2 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -545,7 +545,7 @@ def get_workdir_from_session(session=None, template_key=None): ) anatomy = Anatomy(project_name) - template_obj = anatomy.get_template("work", template_key, "directory") + template_obj = anatomy.get_template_item("work", template_key, "directory") path = template_obj.format_strict(template_data) if path: path = os.path.normpath(path) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 666909ef8d..029775e1db 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -77,7 +77,9 @@ def check_destination_path( """ anatomy_data.update(datetime_data) - path_template = anatomy.get_template("delivery", template_name, "path") + path_template = anatomy.get_template_item( + "delivery", template_name, "path" + ) dest_path = path_template.format(anatomy_data) report_items = collections.defaultdict(list) @@ -150,7 +152,9 @@ def deliver_single_file( if format_dict: anatomy_data = copy.deepcopy(anatomy_data) anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.get_template("delivery", template_name, "path") + template_obj = anatomy.get_template_item( + "delivery", template_name, "path" + ) delivery_path = template_obj.format_strict(anatomy_data) # Backwards compatibility when extension contained `.` @@ -220,8 +224,8 @@ def deliver_sequence( report_items["Source file was not found"].append(msg) return report_items, 0 - delivery_template = anatomy.get_template( - "delivery", template_name, "path" + delivery_template = anatomy.get_template_item( + "delivery", template_name, "path", default=None ) if delivery_template is None: msg = ( @@ -233,7 +237,7 @@ def deliver_sequence( # Check if 'frame' key is available in template which is required # for sequence delivery - if "{frame" not in delivery_template: + if "{frame" not in delivery_template.template: msg = ( "Delivery template \"{}\" in anatomy of project \"{}\"" "does not contain '{{frame}}' key to fill. Delivery of sequence" @@ -278,8 +282,7 @@ def deliver_sequence( anatomy_data["frame"] = frame_indicator if format_dict: anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.get_template("delivery", template_name, "path") - delivery_path = template_obj.format_strict(anatomy_data) + delivery_path = delivery_template.format_strict(anatomy_data) delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) diff --git a/client/ayon_core/pipeline/farm/tools.py b/client/ayon_core/pipeline/farm/tools.py index 1ed7e4b3f5..0b647340f3 100644 --- a/client/ayon_core/pipeline/farm/tools.py +++ b/client/ayon_core/pipeline/farm/tools.py @@ -54,7 +54,7 @@ def from_published_scene(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - template_obj = anatomy.get_template("publish", "default", "path") + template_obj = anatomy.get_template_item("publish", "default", "path") template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8d2ae85694..8d3644637b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -743,8 +743,8 @@ def get_custom_staging_dir_info( template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE - custom_staging_dir = anatomy.get_template( - "staging", template_name, "directory" + custom_staging_dir = anatomy.get_template_item( + "staging", template_name, "directory", default=None ) if custom_staging_dir is None: raise ValueError(( @@ -753,7 +753,7 @@ def get_custom_staging_dir_info( ).format(project_name, template_name)) is_persistent = profile["custom_staging_dir_persistent"] - return str(custom_staging_dir), is_persistent + return custom_staging_dir.template, is_persistent def get_published_workfile_instance(context): @@ -805,7 +805,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data["anatomy"] - template = anatomy.get_template("publish", "default", "path") + template = anatomy.get_template_item("publish", "default", "path") template_filled = template.format_strict(template_data) file_path = os.path.normpath(template_filled) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index 9fabd1dce5..1c7943441e 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -341,8 +341,8 @@ def get_usd_master_path(folder_entity, product_name, representation): "version": 0, # stub version zero }) - template_obj = anatomy.get_template( - "publish", "default","path" + template_obj = anatomy.get_template_item( + "publish", "default", "path" ) path = template_obj.format_strict(template_data) diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 239b8c1096..47d6f4ddfa 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -135,7 +135,9 @@ def get_workdir_with_workdir_data( project_settings ) - template_obj = anatomy.get_template("work", template_key, "directory") + template_obj = anatomy.get_template_item( + "work", template_key, "directory" + ) # Output is TemplateResult object which contain useful data output = template_obj.format_strict(workdir_data) if output: diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 9369a25eb0..69375a7859 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -70,7 +70,7 @@ class OpenTaskPath(LauncherAction): data = get_template_data(project_entity, folder_entity, task_entity) anatomy = Anatomy(project_name) - workdir = anatomy.get_template( + workdir = anatomy.get_template_item( "work", "default", "folder" ).format(data) @@ -87,7 +87,7 @@ class OpenTaskPath(LauncherAction): return valid_workdir data.pop("task", None) - workdir = anatomy.get_template( + workdir = anatomy.get_template_item( "work", "default", "folder" ).format(data) valid_workdir = self._find_first_filled_path(workdir) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index ad3a4d2c23..37a5e87a7a 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -43,9 +43,9 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) anatomy = instance.context.data["anatomy"] - publish_path_template = anatomy.get_template( + publish_path_template = anatomy.get_template_item( "publish", template_name, "path" - ) + ).template template = os.path.normpath(publish_path_template) self.log.debug( ">> template: {}".format(template)) diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 05015909d5..959523918e 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -79,7 +79,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "representation": "TEMP" }) - publish_templates = anatomy.get_template( + publish_templates = anatomy.get_template_item( "publish", "default", "directory" ) publish_folder = os.path.normpath( diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 16c8ebaaf5..ce34f2e88b 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -665,9 +665,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template("publish", template_name) + publish_template = anatomy.get_template_item("publish", template_name) path_template_obj = publish_template["path"] - template = os.path.normpath(path_template_obj) + template = path_template_obj.template.replace("\\", "/") is_udim = bool(repre.get("udim")) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index e313c94393..c352e67f89 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -103,7 +103,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): project_name = anatomy.project_name template_key = self._get_template_key(project_name, instance) - hero_template = anatomy.get_template("hero", template_key, "path") + hero_template = anatomy.get_template_item( + "hero", template_key, "path", default=None + ) if hero_template is None: self.log.warning(( @@ -320,7 +322,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): try: src_to_dst_file_paths = [] repre_integrate_data = [] - path_template_obj = anatomy.get_template( + path_template_obj = anatomy.get_template_item( "hero", template_key, "path" ) for repre_info in published_repres.values(): @@ -335,7 +337,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data.pop("version", None) # Get filled path to repre context - template_filled = path_template_obj.format_strict(anatomy_data) + template_filled = path_template_obj.format_strict( + anatomy_data + ) repre_context = template_filled.used_values for key in self.db_representation_context_keys: value = anatomy_data.get(key) @@ -538,7 +542,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): "originalBasename": instance.data.get("originalBasename") }) - template_obj = anatomy.get_template( + template_obj = anatomy.get_template_item( "hero", template_key, "directory" ) publish_folder = os.path.normpath( diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 0987dc0c56..6e43050c05 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -973,8 +973,8 @@ class ProjectPushItemProcess: "version": version_entity["version"] }) - publish_template = anatomy.get_template("publish", template_name) - path_template = publish_template["path"].replace("\\", "/") + publish_template = anatomy.get_template_item("publish", template_name) + path_template = publish_template["path"].template.replace("\\", "/") file_template = publish_template["file"] self._log_info("Preparing files to transfer") processed_repre_items = self._prepare_file_transactions( @@ -1011,7 +1011,7 @@ class ProjectPushItemProcess: if repre_output_name is not None: repre_format_data["output"] = repre_output_name - template_obj = anatomy.get_template( + template_obj = anatomy.get_template_item( "publish", template_name, "directory" ) folder_path = template_obj.format_strict(formatting_data) diff --git a/client/ayon_core/tools/texture_copy/app.py b/client/ayon_core/tools/texture_copy/app.py index 9a94ba44f7..c288187aac 100644 --- a/client/ayon_core/tools/texture_copy/app.py +++ b/client/ayon_core/tools/texture_copy/app.py @@ -40,7 +40,7 @@ class TextureCopy: }, }) anatomy = Anatomy(project_name, project_entity=project_entity) - template_obj = anatomy.get_template( + template_obj = anatomy.get_template_item( "publish", "texture", "path" ) return template_obj.format_strict(template_data) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index ef502e3d6f..479c8ea849 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -253,7 +253,7 @@ class WorkareaModel: return list(comment_hints), current_comment def _get_workdir(self, anatomy, template_key, fill_data): - directory_template = anatomy.get_template( + directory_template = anatomy.get_template_item( "work", template_key, "directory" ) return directory_template.format_strict(fill_data).normalized() @@ -300,9 +300,12 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - file_template = str( - anatomy.get_template("work", template_key, "file") - ) + file_template = anatomy.get_template_item( + "work", template_key, "file" + ).template + + template_has_version = "{version" in file_template + template_has_comment = "{comment" in file_template comment_hints, comment = self._get_comments_from_root( file_template, @@ -313,9 +316,6 @@ class WorkareaModel: ) last_version = self._get_last_workfile_version( workdir, file_template, fill_data, extensions) - str_file_template = str(file_template) - template_has_version = "{version" in str_file_template - template_has_comment = "{comment" in str_file_template return { "template_key": template_key, @@ -344,7 +344,9 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - file_template = anatomy.get_template("work", template_key, "file") + file_template = anatomy.get_template_item( + "work", template_key, "file" + ).template if use_last_version: version = self._get_last_workfile_version( From 2f0d87d084a9dc70eecb11e0aa4100c33d2dc30d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Mar 2024 14:39:31 +0100 Subject: [PATCH 042/284] handle 'StingTemplate' in '_root_keys_from_templates' --- client/ayon_core/pipeline/anatomy/anatomy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py index 658f5fe3fc..0d250116bd 100644 --- a/client/ayon_core/pipeline/anatomy/anatomy.py +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -192,6 +192,9 @@ class BaseAnatomy(object): keys_queue.append(data) while keys_queue: queue_data = keys_queue.popleft() + if isinstance(queue_data, StringTemplate): + queue_data = queue_data.template + if isinstance(queue_data, dict): for value in queue_data.values(): keys_queue.append(value) From 38c8cf2e3bdf8a5210eb64ceed369d00aa0f2cb8 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 19 Mar 2024 14:48:46 +0100 Subject: [PATCH 043/284] add zbrush being part of application.json --- .../applications/server/applications.json | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index b72d117225..85bf6f1dda 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -1225,6 +1225,32 @@ } ] }, + "zbrush": { + "enabled": true, + "label": "Zbrush", + "icon": "{}/app_icons/zbrush.png", + "host_name": "zbrush", + "environment": "{\n \"ZBRUSH_PLUGIN_PATH\": [\n \"{ZBRUSH_PLUGIN_PATH}\",\n \"{OPENPYPE_STUDIO_PLUGINS}/zbrush/api/zscripts\"\n ]\n}", + "variants": [ + { + "name": "2024", + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Maxon ZBrush 2024\\ZBrush.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{}" + } + ] + }, "additional_apps": [] } } From 0477e1775c043c199a244e2c15e00ac06e47b473 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 19 Mar 2024 14:55:10 +0100 Subject: [PATCH 044/284] add icon --- .../ayon_core/resources/app_icons/zbrush.png | Bin 0 -> 287255 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/ayon_core/resources/app_icons/zbrush.png diff --git a/client/ayon_core/resources/app_icons/zbrush.png b/client/ayon_core/resources/app_icons/zbrush.png new file mode 100644 index 0000000000000000000000000000000000000000..4b0662580c907e797d68ed61041c23f8134fea53 GIT binary patch literal 287255 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 literal 0 HcmV?d00001 From 42219e11184df511c48d2f0d812498c33e9b03a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 19 Mar 2024 15:09:34 +0100 Subject: [PATCH 045/284] :memo: update readme --- README.md | 24 ++++++++++++++++++------ pyproject.toml | 5 +++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e2aa98bb16..11a660e643 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -AYON Core addon -======== +AYON Core Addon +=============== -AYON core provides the base building blocks for all other AYON addons and integrations and is responsible for discovery and initialization of other addons. +AYON core provides the base building blocks for all other AYON addons and integrations and is responsible for discovery and initialization of other addons. - Some of its key functions include: - It is used as the main command line handler in [ayon-launcher](https://github.com/ynput/ayon-launcher) application. @@ -13,8 +13,20 @@ AYON core provides the base building blocks for all other AYON addons and integr - Defines pipeline API used by other integrations - Provides all graphical tools for artists - Defines AYON QT styling -- A bunch more things +- A bunch more things -Together with [ayon-launcher](https://github.com/ynput/ayon-launcher) , they form the base of AYON pipeline and is one of few compulsory addons for AYON pipeline to be useful in a meaningful way. +Together with [ayon-launcher](https://github.com/ynput/ayon-launcher) , they form the base of AYON pipeline and is one of few compulsory addons for AYON pipeline to be useful in a meaningful way. -AYON-core is a successor to OpenPype repository (minus all the addons) and still in the process of cleaning up of all references. Please bear with us during this transitional phase. +AYON-core is a successor to [OpenPype repository](https://github.com/ynput/OpenPype) (minus all the addons) and still in the process of cleaning up of all references. Please bear with us during this transitional phase. + +Development and testing notes +----------------------------- +There is `pyproject.toml` file in the root of the repository. This file is used to define the development environment and is used by `poetry` to create a virtual environment. +This virtual environment is used to run tests and to develop the code, to help with +linting and formatting. Dependencies defined here are not used in actual addon +deployment - for that you need to edit `./client/pyproject.toml` file. That file +will be then processed [ayon-dependencies-tool](https://github.com/ynput/ayon-dependencies-tool) +to create dependency package. + +Right now, this file needs to by synced with dependencies manually, but in the future +we plan to automate process of development environment creation. diff --git a/pyproject.toml b/pyproject.toml index 2740e9307e..ee124ddc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ +# WARNING: This file is used only for development done on this addon. +# Be aware that dependencies used here might not match the ones used by +# the specific addon bundle set up on the AYON server. This file should +# be used only for local development and CI/CD purposes. + [tool.poetry] name = "ayon-core" version = "0.3.0" From 17176fd41330b51681a4cfbf6f74b4dd88c15fe8 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 19 Mar 2024 15:43:50 +0100 Subject: [PATCH 046/284] add missing code for adding application for zbrush --- server_addon/applications/server/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server_addon/applications/server/settings.py b/server_addon/applications/server/settings.py index a49175d488..5743e9f471 100644 --- a/server_addon/applications/server/settings.py +++ b/server_addon/applications/server/settings.py @@ -188,6 +188,8 @@ class ApplicationsSettings(BaseSettingsModel): default_factory=AppGroupWithPython, title="Wrap") openrv: AppGroup = SettingsField( default_factory=AppGroupWithPython, title="OpenRV") + zbrush: AppGroup = SettingsField( + default_factory=AppGroupWithPython, title="Zbrush") additional_apps: list[AdditionalAppGroup] = SettingsField( default_factory=list, title="Additional Applications") From 58d63af8848e4a45868d17168cbb2c7fde68c00b Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 19 Mar 2024 15:44:16 +0100 Subject: [PATCH 047/284] upversioning --- server_addon/applications/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py index f1380eede2..9cb17e7976 100644 --- a/server_addon/applications/server/version.py +++ b/server_addon/applications/server/version.py @@ -1 +1 @@ -__version__ = "0.1.7" +__version__ = "0.1.8" From 5ee980041a13ae2097a9067e2f128b88967e04f9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 19 Mar 2024 23:44:44 +0100 Subject: [PATCH 048/284] Fix outdated highlighting and filtering state for non-hero and hero version - Also optimize the query of highest version by doing one query per product id instead of one per representation id --- .../ayon_core/tools/sceneinventory/model.py | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index df0dea7a3d..92591206fb 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -68,13 +68,7 @@ class InventoryModel(TreeModel): } def outdated(self, item): - value = item.get("version") - if isinstance(value, HeroVersionType): - return False - - if item.get("version") == item.get("highest_version"): - return False - return True + return item.get("isOutdated", True) def data(self, index, role): if not index.isValid(): @@ -297,6 +291,23 @@ class InventoryModel(TreeModel): ) sites_info = self._controller.get_sites_information() + # Query the highest available version so the model can know + # whether current version is currently up-to-date. + highest_versions = ayon_api.get_versions( + project_name, + product_ids={ + group["version"]["productId"] for group in grouped.values() + }, + latest=True, + standard=True, + hero=False, + fields=["productId", "version"] + ) + highest_version_by_product_id = { + version["productId"]: version["version"] + for version in highest_versions + } + for repre_id, group_dict in sorted(grouped.items()): group_containers = group_dict["containers"] repre_entity = group_dict["representation"] @@ -306,12 +317,6 @@ class InventoryModel(TreeModel): product_type = product_entity["productType"] - # Store the highest available version so the model can know - # whether current version is currently up-to-date. - highest_version = ayon_api.get_last_version_by_product_id( - project_name, version_entity["productId"] - ) - # create the group header group_node = Item() group_node["Name"] = "{}_{}: ({})".format( @@ -321,7 +326,17 @@ class InventoryModel(TreeModel): ) group_node["representation"] = repre_id group_node["version"] = version_entity["version"] - group_node["highest_version"] = highest_version["version"] + + # We check against `abs(version)` because we allow a hero version + # which is represented by a negative number to also count as + # latest version + # If a hero version for whatever reason does not match the latest + # positive version number, we also consider it outdated + group_node["isOutdated"] = ( + abs(version_entity["version"]) != + highest_version_by_product_id.get(version_entity["productId"]) + ) + group_node["productType"] = product_type or "" group_node["productTypeIcon"] = product_type_icon group_node["count"] = len(group_containers) @@ -490,17 +505,15 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): def _is_outdated(self, row, parent): """Return whether row is outdated. - A row is considered outdated if it has "version" and "highest_version" - data and in the internal data structure, and they are not of an - equal value. + A row is considered outdated if it has no "version" or the "isOutdated" + value is True. """ def outdated(node): version = node.get("version", None) - highest = node.get("highest_version", None) # Always allow indices that have no version data at all - if version is None and highest is None: + if version is None: return True # If either a version or highest is present but not the other @@ -508,9 +521,10 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if not self._hierarchy_view: # Skip this check if in hierarchy view, or the child item # node will be hidden even it's actually outdated. - if version is None or highest is None: + if version is None: return False - return version != highest + + return node.get("isOutdated", True) index = self.sourceModel().index(row, self.filterKeyColumn(), parent) From 7b5b2a980cf68afd3fa5e8051b0e15d9f88d913f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 19 Mar 2024 23:56:54 +0100 Subject: [PATCH 049/284] Fix update to latest - it now updates to latest instead of hero version --- client/ayon_core/pipeline/load/utils.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index f3d39800cd..66ead70c14 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -479,10 +479,15 @@ def update_container(container, version=-1): project_name, current_version["productId"] ) elif version == -1: - new_version = ayon_api.get_last_version_by_product_id( - project_name, current_version["productId"] - ) - + # TODO: Use `ayon_api.get_last_version_by_product_id` when fixed + # to not return hero versions instead of the last version + new_version = next(ayon_api.get_versions( + project_name, + product_ids=[current_version["productId"]], + standard=True, + hero=False, + latest=True, + ), None) else: new_version = ayon_api.get_version_by_name( project_name, version, current_version["productId"] From b1f8a1c9b4649507b211715b44cad27cd1e7937f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 00:19:41 +0100 Subject: [PATCH 050/284] Fix argument name --- .../ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py index eaa7ff1ae3..004b85f420 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -64,7 +64,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): path = self.filepath_from_context(context) self._set_path(grid_node, path=path, - representation=context["representation"]) + repre_entity=context["representation"]) # Lock the shape node so the user can't delete the transform/shape # as if it was referenced @@ -94,7 +94,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid - self._set_path(grid_nodes[0], path=path, representation=repre_entity) + self._set_path(grid_nodes[0], path=path, repre_entity=repre_entity) # Update container representation cmds.setAttr(container["objectName"] + ".representation", From 87a0c7555c823c29cc54af9cd8b747dc9b92e6a1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 00:35:41 +0100 Subject: [PATCH 051/284] Fix indentations --- .../plugins/publish/collect_karma_rop.py | 28 +++--- .../plugins/publish/collect_mantra_rop.py | 92 +++++++++---------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py index 85100bc2c6..78651b0c69 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -41,23 +41,23 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) - default_prefix = evalParmNoFrame(rop, "picture") - render_products = [] + default_prefix = evalParmNoFrame(rop, "picture") + render_products = [] - # Default beauty AOV - beauty_product = self.get_render_product_name( - prefix=default_prefix, suffix=None - ) - render_products.append(beauty_product) + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) - files_by_aov = { - "beauty": self.generate_expected_files(instance, - beauty_product) - } + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } - filenames = list(render_products) - instance.data["files"] = filenames - instance.data["renderProducts"] = colorspace.ARenderProduct() + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() for product in render_products: self.log.debug("Found render product: %s" % product) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py index d46476c2ce..df9acc4b61 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -41,57 +41,57 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) - default_prefix = evalParmNoFrame(rop, "vm_picture") - render_products = [] + default_prefix = evalParmNoFrame(rop, "vm_picture") + render_products = [] - # Store whether we are splitting the render job (export + render) - split_render = bool(rop.parm("soho_outputmode").eval()) - instance.data["splitRender"] = split_render - export_prefix = None - export_products = [] - if split_render: - export_prefix = evalParmNoFrame( - rop, "soho_diskfile", pad_character="0" - ) - beauty_export_product = self.get_render_product_name( - prefix=export_prefix, - suffix=None) - export_products.append(beauty_export_product) - self.log.debug( - "Found export product: {}".format(beauty_export_product) - ) - instance.data["ifdFile"] = beauty_export_product - instance.data["exportFiles"] = list(export_products) - - # Default beauty AOV - beauty_product = self.get_render_product_name( - prefix=default_prefix, suffix=None + # Store whether we are splitting the render job (export + render) + split_render = bool(rop.parm("soho_outputmode").eval()) + instance.data["splitRender"] = split_render + export_prefix = None + export_products = [] + if split_render: + export_prefix = evalParmNoFrame( + rop, "soho_diskfile", pad_character="0" ) - render_products.append(beauty_product) + beauty_export_product = self.get_render_product_name( + prefix=export_prefix, + suffix=None) + export_products.append(beauty_export_product) + self.log.debug( + "Found export product: {}".format(beauty_export_product) + ) + instance.data["ifdFile"] = beauty_export_product + instance.data["exportFiles"] = list(export_products) - files_by_aov = { - "beauty": self.generate_expected_files(instance, - beauty_product) - } + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) - aov_numbers = rop.evalParm("vm_numaux") - if aov_numbers > 0: - # get the filenames of the AOVs - for i in range(1, aov_numbers + 1): - var = rop.evalParm("vm_variable_plane%d" % i) - if var: - aov_name = "vm_filename_plane%d" % i - aov_boolean = "vm_usefile_plane%d" % i - aov_enabled = rop.evalParm(aov_boolean) - has_aov_path = rop.evalParm(aov_name) - if has_aov_path and aov_enabled == 1: - aov_prefix = evalParmNoFrame(rop, aov_name) - aov_product = self.get_render_product_name( - prefix=aov_prefix, suffix=None - ) - render_products.append(aov_product) + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } - files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa + aov_numbers = rop.evalParm("vm_numaux") + if aov_numbers > 0: + # get the filenames of the AOVs + for i in range(1, aov_numbers + 1): + var = rop.evalParm("vm_variable_plane%d" % i) + if var: + aov_name = "vm_filename_plane%d" % i + aov_boolean = "vm_usefile_plane%d" % i + aov_enabled = rop.evalParm(aov_boolean) + has_aov_path = rop.evalParm(aov_name) + if has_aov_path and aov_enabled == 1: + aov_prefix = evalParmNoFrame(rop, aov_name) + aov_product = self.get_render_product_name( + prefix=aov_prefix, suffix=None + ) + render_products.append(aov_product) + + files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From a1b32846f0506e2385c2e3b2a9bb441481c07397 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 00:47:03 +0100 Subject: [PATCH 052/284] Return the invalid nodes --- .../hosts/maya/plugins/publish/validate_rig_contents.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py index be495a8fb9..7e483ca37a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py @@ -162,6 +162,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): if cmds.nodeType(shape) not in cls.accepted_output: invalid.append(shape) + return invalid + @classmethod def validate_controls(cls, set_members): """ From 714c798420cd57e1c2ebd1867d5a8e59685c61b5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 00:52:18 +0100 Subject: [PATCH 053/284] Fix docstrings --- .../plugins/publish/validate_rig_contents.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py index 7e483ca37a..9d4ec0a41a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py @@ -87,9 +87,9 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """Validate missing objectsets in rig sets Args: - instance (str): instance - required_objsets (list): list of objectset names - rig_sets (list): list of rig sets + instance (pyblish.api.Instance): instance + required_objsets (list[str]): list of objectset names + rig_sets (list[str]): list of rig sets Raises: PublishValidationError: When the error is raised, it will show @@ -109,15 +109,15 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Check if all rig set members are within the hierarchy of the rig root Args: - instance (str): instance - content (list): list of content from rig sets + instance (pyblish.api.Instance): instance + content (list[str]): list of content from rig sets Raises: PublishValidationError: It means no dag nodes in the rig instance Returns: - list: invalid hierarchy + List[str]: invalid hierarchy """ # Ensure there are at least some transforms or dag nodes # in the rig instance @@ -140,15 +140,13 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def validate_geometry(cls, set_members): - """ - Checks if the node types of the set members valid + """Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_set - hierarchy: list of nodes which reside under the root node + set_members (list[str]): nodes of the out_set Returns: - errors (list) + list[str]: Nodes of invalid types. """ # Validate all shape types @@ -166,16 +164,13 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def validate_controls(cls, set_members): - """ - Checks if the control set members are allowed node types. - Checks if the node types of the set members valid + """Checks if the node types of the set members are valid for controls. Args: - set_members: list of nodes of the controls_set - hierarchy: list of nodes which reside under the root node + set_members (list[str]): list of nodes of the controls_set Returns: - errors (list) + list: Controls of disallowed node types. """ # Validate control types @@ -191,7 +186,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """Get the target objectsets and rig sets nodes Args: - instance (str): instance + instance (pyblish.api.Instance): instance Returns: tuple: 2-tuple of list of objectsets, @@ -249,11 +244,10 @@ class ValidateSkeletonRigContents(ValidateRigContents): """Get the target objectsets and rig sets nodes Args: - instance (str): instance + instance (pyblish.api.Instance): instance Returns: - tuple: 2-tuple of list of objectsets, - list of rig sets nodes + tuple: 2-tuple of list of objectsets, list of rig sets nodes """ objectsets = ["skeletonMesh_SET"] skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) From d223be1a88e0cbe50f5df290df63836a812e7783 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:01:39 +0100 Subject: [PATCH 054/284] Remove plugins for `instancer` family - there is no `instancer` family? --- .../publish/validate_instancer_content.py | 75 -------- .../validate_instancer_frame_ranges.py | 167 ------------------ 2 files changed, 242 deletions(-) delete mode 100644 client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py delete mode 100644 client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py deleted file mode 100644 index 5f57b31868..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py +++ /dev/null @@ -1,75 +0,0 @@ -import maya.cmds as cmds -import pyblish.api - -from ayon_core.hosts.maya.api import lib -from ayon_core.pipeline.publish import PublishValidationError - - -class ValidateInstancerContent(pyblish.api.InstancePlugin): - """Validates that all meshes in the instance have object IDs. - - This skips a check on intermediate objects because we consider them - not important. - """ - order = pyblish.api.ValidatorOrder - label = 'Instancer Content' - families = ['instancer'] - - def process(self, instance): - - error = False - members = instance.data['setMembers'] - export_members = instance.data['exactExportMembers'] - - self.log.debug("Contents {0}".format(members)) - - if not len(members) == len(cmds.ls(members, type="instancer")): - self.log.error("Instancer can only contain instancers") - error = True - - # TODO: Implement better check for particles are cached - if not cmds.ls(export_members, type="nucleus"): - self.log.error("Instancer must have a connected nucleus") - error = True - - if not cmds.ls(export_members, type="cacheFile"): - self.log.error("Instancer must be cached") - error = True - - hidden = self.check_geometry_hidden(export_members) - if not hidden: - error = True - self.log.error("Instancer input geometry must be hidden " - "the scene. Invalid: {0}".format(hidden)) - - # Ensure all in one group - parents = cmds.listRelatives(members, - allParents=True, - fullPath=True) or [] - roots = list(set(cmds.ls(parents, assemblies=True, long=True))) - if len(roots) > 1: - self.log.error("Instancer should all be contained in a single " - "group. Current roots: {0}".format(roots)) - error = True - - if error: - raise PublishValidationError( - "Instancer Content is invalid. See log.") - - def check_geometry_hidden(self, export_members): - - # Ensure all instanced geometry is hidden - shapes = cmds.ls(export_members, - dag=True, - shapes=True, - noIntermediate=True) - meshes = cmds.ls(shapes, type="mesh") - - visible = [node for node in meshes - if lib.is_visible(node, - displayLayer=False, - intermediateObject=False)] - if visible: - return False - - return True diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py deleted file mode 100644 index be6724d7e9..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -import re - -import pyblish.api - -from ayon_core.pipeline.publish import PublishValidationError - - -def is_cache_resource(resource): - """Return whether resource is a cacheFile resource""" - required = set(["maya", "node", "cacheFile"]) - tags = resource.get("tags", []) - return required.issubset(tags) - - -def valdidate_files(files): - for f in files: - assert os.path.exists(f) - assert f.endswith(".mcx") or f.endswith(".mcc") - - return True - - -def filter_ticks(files): - tick_files = set() - ticks = set() - for path in files: - match = re.match(".+Tick([0-9]+).mcx$", os.path.basename(path)) - if match: - tick_files.add(path) - num = match.group(1) - ticks.add(int(num)) - - return tick_files, ticks - - -class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): - """Validates all instancer particle systems are cached correctly. - - This means they should have the files/frames as required by the start-end - frame (including handles). - - This also checks the files exist and checks the "ticks" (substeps) files. - - """ - order = pyblish.api.ValidatorOrder - label = 'Instancer Cache Frame Ranges' - families = ['instancer'] - - @classmethod - def get_invalid(cls, instance): - - import pyseq - - start_frame = instance.data.get("frameStart", 0) - end_frame = instance.data.get("frameEnd", 0) - required = range(int(start_frame), int(end_frame) + 1) - - invalid = list() - resources = instance.data.get("resources", []) - - for resource in resources: - if not is_cache_resource(resource): - continue - - node = resource['node'] - all_files = resource['files'][:] - all_lookup = set(all_files) - - # The first file is usually the .xml description file. - xml = all_files.pop(0) - assert xml.endswith(".xml") - - # Ensure all files exist (including ticks) - # The remainder file paths should be the .mcx or .mcc files - valdidate_files(all_files) - - # Maya particle caches support substeps by saving out additional - # files that end with a Tick60.mcx, Tick120.mcx, etc. suffix. - # To avoid `pyseq` getting confused we filter those out and then - # for each file (except the last frame) check that at least all - # ticks exist. - - tick_files, ticks = filter_ticks(all_files) - if tick_files: - files = [f for f in all_files if f not in tick_files] - else: - files = all_files - - sequences = pyseq.get_sequences(files) - if len(sequences) != 1: - invalid.append(node) - cls.log.warning("More than one sequence found? " - "{0} {1}".format(node, files)) - cls.log.warning("Found caches: {0}".format(sequences)) - continue - - sequence = sequences[0] - cls.log.debug("Found sequence: {0}".format(sequence)) - - start = sequence.start() - end = sequence.end() - - if start > start_frame or end < end_frame: - invalid.append(node) - cls.log.warning("Sequence does not have enough " - "frames: {0}-{1} (requires: {2}-{3})" - "".format(start, end, - start_frame, - end_frame)) - continue - - # Ensure all frames are present - missing = set(sequence.missing()) - if missing: - required_missing = [x for x in required if x in missing] - if required_missing: - invalid.append(node) - cls.log.warning("Sequence is missing required frames: " - "{0}".format(required_missing)) - continue - - # Ensure all tick files (substep) exist for the files in the folder - # for the frames required by the time range. - if ticks: - ticks = list(sorted(ticks)) - cls.log.debug("Found ticks: {0} " - "(substeps: {1})".format(ticks, len(ticks))) - - # Check all frames except the last since we don't - # require subframes after our time range. - tick_check_frames = set(required[:-1]) - - # Check all frames - for item in sequence: - frame = item.frame - if not frame: - invalid.append(node) - cls.log.error("Path is not a frame in sequence: " - "{0}".format(item)) - continue - - # Not required for our time range - if frame not in tick_check_frames: - continue - - path = item.path - for num in ticks: - base, ext = os.path.splitext(path) - tick_file = base + "Tick{0}".format(num) + ext - if tick_file not in all_lookup: - invalid.append(node) - cls.log.warning("Tick file found that is not " - "in cache query filenames: " - "{0}".format(tick_file)) - - return invalid - - def process(self, instance): - - invalid = self.get_invalid(instance) - - if invalid: - self.log.error("Invalid nodes: {0}".format(invalid)) - raise PublishValidationError( - ("Invalid particle caches in instance. " - "See logs for details.")) From 065c11526e81f3018c33b0fb00ecd1b4fe17e5f4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:07:26 +0100 Subject: [PATCH 055/284] Fix redeclared `order` defined above without usage --- .../hosts/photoshop/plugins/publish/collect_auto_image.py | 1 - .../photoshop/plugins/publish/collect_auto_image_refresh.py | 1 - 2 files changed, 2 deletions(-) diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py index b488ab364d..adbe02eb74 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -9,7 +9,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin): """ label = "Collect Auto Image" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py index 0585f4f226..7a5f297c89 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py @@ -8,7 +8,6 @@ class CollectAutoImageRefresh(pyblish.api.ContextPlugin): """ label = "Collect Auto Image Refresh" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 From 81bdf3df3d804fda1e6bc5c3d69af81abf388302 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:09:01 +0100 Subject: [PATCH 056/284] Fix unreachable code - string wasn't actually joined --- .../hosts/maya/plugins/publish/collect_multiverse_look.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py index 31c0d0eaa1..83e743c92e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -40,9 +40,11 @@ class _NodeTypeAttrib(object): return "{}.{}".format(node, self.colour_space) def __str__(self): - return "_NodeTypeAttrib(name={}, fname={}, " - "computed_fname={}, colour_space={})".format( - self.name, self.fname, self.computed_fname, self.colour_space) + return ( + "_NodeTypeAttrib(name={}, fname={}, " + "computed_fname={}, colour_space={})".format( + self.name, self.fname, self.computed_fname, self.colour_space) + ) NODETYPES = { From 7c551c832d9face99d34d76564231ee338d3e2f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:09:25 +0100 Subject: [PATCH 057/284] Fix indentations --- .../hosts/harmony/plugins/load/load_template_workfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py index 7bf634f00c..c7132ce373 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py @@ -50,11 +50,11 @@ class ImportTemplateLoader(load.LoaderPlugin): self.__class__.__name__ ) - def update(self, container, context): - pass + def update(self, container, context): + pass - def remove(self, container): - pass + def remove(self, container): + pass class ImportWorkfileLoader(ImportTemplateLoader): From 3d4ae8838d203d694fdfcf2fa1cb8952d5f7081f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:10:32 +0100 Subject: [PATCH 058/284] Remove type hint since docstring takes care of that --- client/ayon_core/hosts/houdini/api/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index 0809f4e566..b33d0fe297 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -147,7 +147,6 @@ class HoudiniCreatorBase(object): def create_instance_node( folder_path, node_name, parent, node_type="geometry" ): - # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: From 6adc2c9c26eb7a93fa3a30263392ab027bb2ff2a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:10:57 +0100 Subject: [PATCH 059/284] Fix type hint having too few arguments --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index c16c95a270..6a2a41d1d0 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -16,7 +16,7 @@ class CreateHDA(plugin.HoudiniCreator): maintain_selection = False def _check_existing(self, folder_path, product_name): - # type: (str) -> bool + # type: (str, str) -> bool """Check if existing product name versions already exists.""" # Get all products of the current folder project_name = self.project_name From 16b9dbbebe03a6aec56d5b75b2506ae16148d761 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:14:54 +0100 Subject: [PATCH 060/284] Fix typos --- .../plugins/publish/collect_default_deadline_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py index 8123409052..b7ca227b01 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -9,11 +9,11 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): DL webservice addresses must be configured first in System Settings for project settings enum to work. - Default webservice could be overriden by + Default webservice could be overridden by `project_settings/deadline/deadline_servers`. Currently only single url is expected. - This url could be overriden by some hosts directly on instances with + This url could be overridden by some hosts directly on instances with `CollectDeadlineServerFromInstance`. """ From a68fa54661eba181eb1adc44bb7fd8df2e85ae26 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:15:12 +0100 Subject: [PATCH 061/284] Use set literal --- .../plugins/publish/validate_expected_and_rendered_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index a666c5c2dc..6263526d5c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -149,7 +149,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): """ # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' if not frame_placeholder: - return set([file_name_template]) + return {file_name_template} real_expected_rendered = set() src_padding_exp = "%0{}d".format(len(frame_placeholder)) From 3506c07a3c17a52415025590f28fc0065ed110cf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:16:36 +0100 Subject: [PATCH 062/284] Fix docstring --- .../plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py index 9641c16d20..f146aef7b4 100644 --- a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py @@ -404,7 +404,7 @@ class OpenPypeTileAssembler(DeadlinePlugin): Args: output_width (int): Width of output image. output_height (int): Height of output image. - tiles_info (list): List of tile items, each item must be + tile_info (list): List of tile items, each item must be dictionary with `filepath`, `pos_x` and `pos_y` keys representing path to file and x, y coordinates on output image where top-left point of tile item should start. From de481e2e359ae4881f6f0d9def6a656d5bced766 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 01:17:32 +0100 Subject: [PATCH 063/284] Remove invalid type hint missing the optional argument - docstring should take care of this --- client/ayon_core/modules/deadline/deadline_module.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/modules/deadline/deadline_module.py b/client/ayon_core/modules/deadline/deadline_module.py index d2f0e263d4..c0ba83477e 100644 --- a/client/ayon_core/modules/deadline/deadline_module.py +++ b/client/ayon_core/modules/deadline/deadline_module.py @@ -46,7 +46,6 @@ class DeadlineModule(AYONAddon, IPluginPaths): @staticmethod def get_deadline_pools(webservice, log=None): - # type: (str) -> list """Get pools from Deadline. Args: webservice (str): Server url. From 7f83a62192bb8f52018b570105f984804d853011 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 09:56:23 +0100 Subject: [PATCH 064/284] Revert "Fix update to latest - it now updates to latest instead of hero version" This reverts commit 7b5b2a980cf68afd3fa5e8051b0e15d9f88d913f. --- client/ayon_core/pipeline/load/utils.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 66ead70c14..f3d39800cd 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -479,15 +479,10 @@ def update_container(container, version=-1): project_name, current_version["productId"] ) elif version == -1: - # TODO: Use `ayon_api.get_last_version_by_product_id` when fixed - # to not return hero versions instead of the last version - new_version = next(ayon_api.get_versions( - project_name, - product_ids=[current_version["productId"]], - standard=True, - hero=False, - latest=True, - ), None) + new_version = ayon_api.get_last_version_by_product_id( + project_name, current_version["productId"] + ) + else: new_version = ayon_api.get_version_by_name( project_name, version, current_version["productId"] From a71ea3a3e3318d69811dd00a2b257ceeb5ebc30e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 09:58:07 +0100 Subject: [PATCH 065/284] Use more specific api call --- client/ayon_core/tools/sceneinventory/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 92591206fb..b9b18b339e 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -293,14 +293,11 @@ class InventoryModel(TreeModel): # Query the highest available version so the model can know # whether current version is currently up-to-date. - highest_versions = ayon_api.get_versions( + highest_versions = ayon_api.get_last_versions( project_name, product_ids={ group["version"]["productId"] for group in grouped.values() }, - latest=True, - standard=True, - hero=False, fields=["productId", "version"] ) highest_version_by_product_id = { From bb5f83f6c4c49e1bf7086debc052d2ffe6e5bcb4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 10:15:01 +0100 Subject: [PATCH 066/284] Refactor `ayon_api.get_last_versions` already returns `dict` by `productId` --- client/ayon_core/tools/sceneinventory/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index b9b18b339e..9c4d080470 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -293,16 +293,17 @@ class InventoryModel(TreeModel): # Query the highest available version so the model can know # whether current version is currently up-to-date. - highest_versions = ayon_api.get_last_versions( + highest_version_by_product_id = ayon_api.get_last_versions( project_name, product_ids={ group["version"]["productId"] for group in grouped.values() }, fields=["productId", "version"] ) + # Map value to `version` key highest_version_by_product_id = { - version["productId"]: version["version"] - for version in highest_versions + product_id: version["version"] + for product_id, version in highest_version_by_product_id.items() } for repre_id, group_dict in sorted(grouped.items()): From 88e81cca22a257e20330c5ba96092a80387d5492 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 10:40:42 +0100 Subject: [PATCH 067/284] Remove unused import --- client/ayon_core/tools/sceneinventory/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 9c4d080470..5f8b1ee81c 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -8,10 +8,7 @@ import ayon_api from qtpy import QtCore, QtGui import qtawesome -from ayon_core.pipeline import ( - get_current_project_name, - HeroVersionType, -) +from ayon_core.pipeline import get_current_project_name from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import get_qt_icon from ayon_core.tools.utils.models import TreeModel, Item From aea9716f717300abeb956d56d66d1830d62b0442 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:23:38 +0100 Subject: [PATCH 068/284] Blender: Validate Transform Zero - improve validation report + add repair action --- .../publish/validate_transform_zero.py | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py index 267eff47e4..4ca1a86de3 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py @@ -1,3 +1,4 @@ +import inspect from typing import List import mathutils @@ -5,29 +6,26 @@ import bpy import pyblish.api +from ayon_core.hosts.blender.api import plugin import ayon_core.hosts.blender.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, - PublishValidationError + PublishValidationError, + RepairAction ) class ValidateTransformZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Transforms can't have any values - - To solve this issue, try freezing the transforms. So long - as the transforms, rotation and scale values are zero, - you're all good. - - """ + """Transforms can't have any values""" order = ValidateContentsOrder hosts = ["blender"] families = ["model"] label = "Transform Zero" - actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction] + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] _identity = mathutils.Matrix() @@ -51,5 +49,41 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( "Objects found in instance which do not" - f" have transform set to zero: {names}" + f" have transform set to zero: {names}", + description=self.get_description() ) + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + if not invalid: + return + + context = plugin.create_blender_context( + active=invalid[0], selected=invalid + ) + with bpy.context.temp_override(**context): + # TODO: Preferably this does allow custom pivot point locations + # and if so, this should likely apply to the delta instead + # using `bpy.ops.object.transforms_to_deltas(mode="ALL")` + bpy.ops.object.transform_apply(location=True, + rotation=True, + scale=True) + + def get_description(self): + return inspect.cleandoc( + """## Transforms can't have any values. + + The location, rotation and scale on the transform must be at + the default values. This also goes for the delta transforms. + + To solve this issue, try freezing the transforms: + - `Object` > `Apply` > `All Transforms` + + Using the Repair action directly will do the same. + + So long as the transforms, rotation and scale values are zero, + you're all good. + """ + ) From 16096e8407e87a071d38b4f3cee107f6cc11d8d8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 23 Mar 2024 21:29:37 +0100 Subject: [PATCH 069/284] Re-add outputName to _rename_in_representation --- client/ayon_core/plugins/publish/extract_color_transcode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index b5ddebe05b..1130c575a3 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -257,6 +257,7 @@ class ExtractOIIOTranscode(publish.Extractor): return new_repre["ext"] = output_extension + new_repre["outputName"] = output_name renamed_files = [] for file_name in files_to_convert: From 5496484aab7c538cadc3be013c1408bf9c730444 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 10:23:10 +0100 Subject: [PATCH 070/284] Fix settings title (typo) + add description --- server_addon/maya/server/settings/publishers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 3a6de2eb44..fe5c10e93c 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -322,7 +322,9 @@ class ExtractCameraAlembicModel(BaseSettingsModel): optional: bool = SettingsField(title="Optional") active: bool = SettingsField(title="Active") bake_attributes: str = SettingsField( - "[]", title="Base Attributes", widget="textarea" + "[]", title="Bake Attributes", widget="textarea", + description="List of attributes that will be included in the alembic " + "export.", ) @validator("bake_attributes") From 404681e684892e63e4d26915364c24ed56f23c7d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 10:32:08 +0100 Subject: [PATCH 071/284] Add list of attributes to always be included in alembic export even when not specified in publisher UI by user. This matches the Extract Camera Alembic logic for `bake_attributes` --- .../hosts/maya/plugins/publish/extract_pointcache.py | 9 +++++++++ server_addon/maya/server/settings/publishers.py | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py index f2187063fc..f83d2679cb 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py @@ -1,4 +1,5 @@ import os +import json from maya import cmds @@ -25,6 +26,7 @@ class ExtractAlembic(publish.Extractor): hosts = ["maya"] families = ["pointcache", "model", "vrayproxy.alembic"] targets = ["local", "remote"] + bake_attributes = "[]" def process(self, instance): if instance.data.get("farm"): @@ -42,6 +44,13 @@ class ExtractAlembic(publish.Extractor): attrs += instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] + # bake specified attributes in preset + bake_attributes = json.loads(self.bake_attributes) + assert isinstance(bake_attributes, list), ( + "Attributes to bake must be specified as a list" + ) + attrs += bake_attributes + attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 3a6de2eb44..ca99d8c57f 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -299,6 +299,11 @@ class ExtractAlembicModel(BaseSettingsModel): families: list[str] = SettingsField( default_factory=list, title="Families") + bake_attributes: str = SettingsField( + "[]", title="Bake Attributes", widget="textarea", + description="List of attributes that will be included in the alembic " + "export.", + ) class ExtractObjModel(BaseSettingsModel): @@ -1193,7 +1198,8 @@ DEFAULT_PUBLISH_SETTINGS = { "pointcache", "model", "vrayproxy.alembic" - ] + ], + "bake_attributes": "[]" }, "ExtractObj": { "enabled": False, From 3d9cd57df01aacde07e3424a9d9c0af8a5f834ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 10:37:31 +0100 Subject: [PATCH 072/284] Expose bake attribute prefixes, plus make settings just a list of strings --- .../maya/plugins/publish/extract_pointcache.py | 15 ++++++--------- server_addon/maya/server/settings/publishers.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py index f83d2679cb..5de72f7674 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py @@ -1,5 +1,4 @@ import os -import json from maya import cmds @@ -26,7 +25,10 @@ class ExtractAlembic(publish.Extractor): hosts = ["maya"] families = ["pointcache", "model", "vrayproxy.alembic"] targets = ["local", "remote"] - bake_attributes = "[]" + + # From settings + bake_attributes = [] + bake_attribute_prefixes = [] def process(self, instance): if instance.data.get("farm"): @@ -42,17 +44,12 @@ class ExtractAlembic(publish.Extractor): attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] attrs += instance.data.get("userDefinedAttributes", []) + attrs += self.bake_attributes attrs += ["cbId"] - # bake specified attributes in preset - bake_attributes = json.loads(self.bake_attributes) - assert isinstance(bake_attributes, list), ( - "Attributes to bake must be specified as a list" - ) - attrs += bake_attributes - attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] + attr_prefixes += self.bake_attribute_prefixes self.log.debug("Extracting pointcache..") dirname = self.staging_dir(instance) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index ca99d8c57f..7ba2522e60 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -299,10 +299,15 @@ class ExtractAlembicModel(BaseSettingsModel): families: list[str] = SettingsField( default_factory=list, title="Families") - bake_attributes: str = SettingsField( - "[]", title="Bake Attributes", widget="textarea", + bake_attributes: list[str] = SettingsField( + "", title="Bake Attributes", widget="textarea", description="List of attributes that will be included in the alembic " "export.", + ), + bake_attribute_prefixes: list[str] = SettingsField( + "", title="Bake Attribute Prefixes", widget="textarea", + description="List of attribute prefixes for attributes that will be " + "included in the alembic export.", ) @@ -1199,7 +1204,8 @@ DEFAULT_PUBLISH_SETTINGS = { "model", "vrayproxy.alembic" ], - "bake_attributes": "[]" + "bake_attributes": [], + "bake_attribute_prefixes": [], }, "ExtractObj": { "enabled": False, From fadf720c9a9b33299a057527183f58d9d89027a6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 10:38:20 +0100 Subject: [PATCH 073/284] Bump version --- server_addon/maya/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 8202425a2d..a86a3ce0a1 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.9" +__version__ = "0.1.10" From 9a5d6427d5741186b4767107ae495bf36241ed09 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 11:22:53 +0100 Subject: [PATCH 074/284] Fix settings --- server_addon/maya/server/settings/publishers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 7ba2522e60..7c2688af55 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -300,12 +300,12 @@ class ExtractAlembicModel(BaseSettingsModel): default_factory=list, title="Families") bake_attributes: list[str] = SettingsField( - "", title="Bake Attributes", widget="textarea", + default_factory=list, title="Bake Attributes", description="List of attributes that will be included in the alembic " "export.", - ), + ) bake_attribute_prefixes: list[str] = SettingsField( - "", title="Bake Attribute Prefixes", widget="textarea", + default_factory=list, title="Bake Attribute Prefixes", description="List of attribute prefixes for attributes that will be " "included in the alembic export.", ) @@ -1205,7 +1205,7 @@ DEFAULT_PUBLISH_SETTINGS = { "vrayproxy.alembic" ], "bake_attributes": [], - "bake_attribute_prefixes": [], + "bake_attribute_prefixes": [] }, "ExtractObj": { "enabled": False, From 7f0098f92fae0d29c4808e17a00ed9f2233fa65c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 11:48:51 +0100 Subject: [PATCH 075/284] Houdini: Add generic filepath loader --- .../houdini/plugins/load/load_filepath.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 client/ayon_core/hosts/houdini/plugins/load/load_filepath.py diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_filepath.py b/client/ayon_core/hosts/houdini/plugins/load/load_filepath.py new file mode 100644 index 0000000000..515ffa6027 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/load/load_filepath.py @@ -0,0 +1,129 @@ +import os +import re + +from ayon_core.pipeline import load +from openpype.hosts.houdini.api import pipeline + +import hou + + +class FilePathLoader(load.LoaderPlugin): + """Load a managed filepath to a null node. + + This is useful if for a particular workflow there is no existing loader + yet. A Houdini artists can load as the generic filepath loader and then + reference the relevant Houdini parm to use the exact value. The benefit + is that this filepath will be managed and can be updated as usual. + + """ + + label = "Load filepath to node" + order = 9 + icon = "link" + color = "white" + product_types = {"*"} + representations = ["*"] + + def load(self, context, name=None, namespace=None, data=None): + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["folder"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create a null node + container = obj.createNode("null", node_name=node_name) + + # Destroy any children + for node in container.children(): + node.destroy() + + # Add filepath attribute, set value as default value + filepath = self.format_path( + path=self.filepath_from_context(context), + representation=context["representation"] + ) + parm_template_group = container.parmTemplateGroup() + attr_folder = hou.FolderParmTemplate("attributes_folder", "Attributes") + parm = hou.StringParmTemplate(name="filepath", + label="Filepath", + num_components=1, + default_value=(filepath,)) + attr_folder.addParmTemplate(parm) + parm_template_group.append(attr_folder) + + # Hide some default labels + for folder_label in ["Transform", "Render", "Misc", "Redshift OBJ"]: + folder = parm_template_group.findFolder(folder_label) + if not folder: + continue + parm_template_group.hideFolder(folder_label, True) + + container.setParmTemplateGroup(parm_template_group) + + container.setDisplayFlag(False) + container.setSelectableInViewport(False) + container.useXray(False) + + nodes = [container] + + self[:] = nodes + + return pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + def update(self, container, context): + + # Update the file path + representation_entity = context["representation"] + file_path = self.format_path( + path=self.filepath_from_context(context), + representation=representation_entity + ) + + node = container["node"] + node.setParms({ + "filepath": file_path, + "representation": str(representation_entity["id"]) + }) + + # Update the parameter default value (cosmetics) + parm_template_group = node.parmTemplateGroup() + parm = parm_template_group.find("filepath") + parm.setDefaultValue((file_path,)) + parm_template_group.replace(parm_template_group.find("filepath"), + parm) + node.setParmTemplateGroup(parm_template_group) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + + node = container["node"] + node.destroy() + + @staticmethod + def format_path(path: str, representation: dict) -> str: + """Format file path for sequence with $F.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + # The path is either a single file or sequence in a folder. + frame = representation["context"].get("frame") + if frame is not None: + # Substitute frame number in sequence with $F with padding + ext = representation.get("ext", representation["name"]) + token = "$F{}".format(len(frame)) # e.g. $F4 + pattern = r"\.(\d+)\.{ext}$".format(ext=re.escape(ext)) + path = re.sub(pattern, ".{}.{}".format(token, ext), path) + + return os.path.normpath(path).replace("\\", "/") From 3888ce8486bc4910557e8eab33484a227beb0206 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 11:52:38 +0100 Subject: [PATCH 076/284] Houdini: Implement `switch` method for Camera Loader --- client/ayon_core/hosts/houdini/plugins/load/load_camera.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py index 605e5724e6..c57b14043a 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py @@ -167,6 +167,9 @@ class CameraLoader(load.LoaderPlugin): temp_camera.destroy() + def switch(self, container, context): + self.update(container, context) + def remove(self, container): node = container["node"] From 4f3391eb8a7d99aa97ea527b176fad2299d0704d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 11:52:58 +0100 Subject: [PATCH 077/284] Remove redundant commented print --- client/ayon_core/hosts/houdini/plugins/load/load_camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py index c57b14043a..7cb4542d0c 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py @@ -198,7 +198,6 @@ class CameraLoader(load.LoaderPlugin): def _match_maya_render_mask(self, camera): """Workaround to match Maya render mask in Houdini""" - # print("Setting match maya render mask ") parm = camera.parm("aperture") expression = parm.expression() expression = expression.replace("return ", "aperture = ") From e5bc1fd3b7ddd2563b7e080fcf96bedf5b57239d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:04:33 +0100 Subject: [PATCH 078/284] Blender: Add model publisher validator to ensure uv set is `map1` (Disabled by default) --- .../plugins/publish/validate_model_uv_map1.py | 94 +++++++++++++++++++ .../server/settings/publish_plugins.py | 9 ++ 2 files changed, 103 insertions(+) create mode 100644 client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py new file mode 100644 index 0000000000..2315d54a2c --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py @@ -0,0 +1,94 @@ +import inspect +from typing import List + +import bpy + +import pyblish.api + +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError, + RepairAction +) +import ayon_core.hosts.blender.api.action + + +class ValidateModelMeshUvMap1( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): + """Validate model mesh uvs are named `map1`. + + This is solely to get them to work nicely for the Maya pipeline. + """ + + order = ValidateContentsOrder + hosts = ["blender"] + families = ["model"] + label = "Mesh UVs named map1" + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] + optional = True + enabled = False + + @classmethod + def get_invalid(cls, instance) -> List: + + invalid = [] + for obj in instance: + if obj.mode != "OBJECT": + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + + obj_data = obj.data + if isinstance(obj_data, bpy.types.Mesh): + mesh = obj_data + + # Ignore mesh without UVs + if not mesh.uv_layers: + continue + + # If mesh has map1 all is ok + if mesh.uv_layers.get("map1"): + continue + + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + invalid.append(obj) + + return invalid + + @classmethod + def repair(cls, instance): + for obj in cls.get_invalid(instance): + mesh = obj.data + + # Rename the first UV set to map1 + mesh.uv_layers[0].name = "map1" + + def process(self, instance): + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + f"Meshes found in instance without valid UV's: {invalid}", + description=self.get_description() + ) + + def get_description(self): + return inspect.cleandoc( + """## Meshes must have `map1` uv set + + To accompany a better Maya-focused pipeline with Alembics it is + expected that a Mesh has a `map1` UV set. Blender defaults to + a UV set named `UVMap` and thus needs to be renamed. + + """ + ) diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 79c489d080..c742fdc5bd 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -89,6 +89,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate Mesh No Negative Scale" ) + ValidateModelMeshUvMap1: ValidatePluginModel = SettingsField( + default_factory=ValidatePluginModel, + title="Validate Model Mesh Has UV map named map1" + ) ValidateTransformZero: ValidatePluginModel = SettingsField( default_factory=ValidatePluginModel, title="Validate Transform Zero" @@ -181,6 +185,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateModelMeshUvMap1": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateTransformZero": { "enabled": False, "optional": True, From cc82af1c3363ef98ebba09de9e0b30114ef4b0e6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:04:55 +0100 Subject: [PATCH 079/284] Bump addon version --- server_addon/blender/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index f1380eede2..9cb17e7976 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.7" +__version__ = "0.1.8" From c147553e8aab3691ccf8ff6933428832269a69d3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:15:08 +0100 Subject: [PATCH 080/284] Fusion: Add support for "custom frame range" per saver --- .../fusion/plugins/create/create_saver.py | 72 ++++++++++++++++++- .../plugins/publish/collect_instances.py | 8 +++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py index b6cda1f302..2abec597da 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py @@ -1,6 +1,11 @@ -from ayon_core.lib import EnumDef +from ayon_core.lib import ( + UILabelDef, + NumberDef, + EnumDef +) from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver +from ayon_core.hosts.fusion.api.lib import get_current_comp class CreateSaver(GenericCreateSaver): @@ -44,6 +49,7 @@ class CreateSaver(GenericCreateSaver): self._get_render_target_enum(), self._get_reviewable_bool(), self._get_frame_range_enum(), + self._get_custom_frame_range_attribute_defs(), self._get_image_format_enum(), ] return attr_defs @@ -53,6 +59,7 @@ class CreateSaver(GenericCreateSaver): "current_folder": "Current Folder context", "render_range": "From render in/out", "comp_range": "From composition timeline", + "custom_range": "Custom frame range", } return EnumDef( @@ -61,3 +68,66 @@ class CreateSaver(GenericCreateSaver): label="Frame range source", default=self.default_frame_range_option ) + + @staticmethod + def _get_custom_frame_range_attribute_defs() -> list: + + # Define custom frame range defaults based on current comp + # timeline settings (if a comp is currently open) + comp = get_current_comp() + if comp is not None: + attrs = comp.GetAttrs() + frame_defaults = { + "frameStart": int(attrs["COMPN_GlobalStart"]), + "frameEnd": int(attrs["COMPN_GlobalEnd"]), + "handleStart": int( + attrs["COMPN_RenderStart"] - attrs["COMPN_GlobalStart"] + ), + "handleEnd": int( + attrs["COMPN_GlobalEnd"] - attrs["COMPN_RenderEnd"] + ), + } + else: + frame_defaults = { + "frameStart": 1001, + "frameEnd": 1100, + "handleStart": 0, + "handleEnd": 0 + } + + return [ + UILabelDef( + label="
Custom Frame Range" + ), + UILabelDef( + label="only used with 'Custom frame range' source" + ), + NumberDef( + "custom_frameStart", + label="Frame Start", + default=frame_defaults["frameStart"], + minimum=0, + decimals=0 + ), + NumberDef( + "custom_frameEnd", + label="Frame End", + default=frame_defaults["frameEnd"], + minimum=0, + decimals=0 + ), + NumberDef( + "custom_handleStart", + label="Handle Start", + default=frame_defaults["handleStart"], + minimum=0, + decimals=0 + ), + NumberDef( + "custom_handleEnd", + label="Handle End", + default=frame_defaults["handleEnd"], + minimum=0, + decimals=0 + ) + ] diff --git a/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py b/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py index 51d7e68fb6..921c282877 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py @@ -57,6 +57,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_with_handle = comp_start end_with_handle = comp_end + if frame_range_source == "custom_range": + start = int(instance.data["custom_frameStart"]) + end = int(instance.data["custom_frameEnd"]) + handle_start = int(instance.data["custom_handleStart"]) + handle_end = int(instance.data["custom_handleEnd"]) + start_with_handle = start - handle_start + end_with_handle = end + handle_end + frame = instance.data["creator_attributes"].get("frame") # explicitly publishing only single frame if frame is not None: From f84a9115562b333616bf42c793b64636f4cf134c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:21:00 +0100 Subject: [PATCH 081/284] Fix attribute definitions list + add tooltips --- .../fusion/plugins/create/create_saver.py | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py index 2abec597da..20c7b99851 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py @@ -49,8 +49,8 @@ class CreateSaver(GenericCreateSaver): self._get_render_target_enum(), self._get_reviewable_bool(), self._get_frame_range_enum(), - self._get_custom_frame_range_attribute_defs(), self._get_image_format_enum(), + *self._get_custom_frame_range_attribute_defs() ] return attr_defs @@ -97,37 +97,53 @@ class CreateSaver(GenericCreateSaver): return [ UILabelDef( - label="
Custom Frame Range" - ), - UILabelDef( - label="only used with 'Custom frame range' source" + label="
Custom Frame Range
" + "only used with 'Custom frame range' source" ), NumberDef( "custom_frameStart", label="Frame Start", default=frame_defaults["frameStart"], minimum=0, - decimals=0 + decimals=0, + tooltip=( + "Set the start frame for the export.\n" + "Only used if frame range source is 'Custom frame range'." + ) ), NumberDef( "custom_frameEnd", label="Frame End", default=frame_defaults["frameEnd"], minimum=0, - decimals=0 + decimals=0, + tooltip=( + "Set the end frame for the export.\n" + "Only used if frame range source is 'Custom frame range'." + ) ), NumberDef( "custom_handleStart", label="Handle Start", default=frame_defaults["handleStart"], minimum=0, - decimals=0 + decimals=0, + tooltip=( + "Set the start handles for the export, this will be " + "added before the start frame.\n" + "Only used if frame range source is 'Custom frame range'." + ) ), NumberDef( "custom_handleEnd", label="Handle End", default=frame_defaults["handleEnd"], minimum=0, - decimals=0 + decimals=0, + tooltip=( + "Set the end handles for the export, this will be added " + "after the end frame.\n" + "Only used if frame range source is 'Custom frame range'." + ) ) ] From 085e2aa82679ae9ba4962f8b01342e50fa7b98b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:29:30 +0100 Subject: [PATCH 082/284] Remove unused import --- .../ayon_core/hosts/fusion/plugins/create/create_image_saver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py index 8110898ae9..729843d078 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py @@ -1,7 +1,6 @@ from ayon_core.lib import NumberDef from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver -from ayon_core.hosts.fusion.api import get_current_comp class CreateImageSaver(GenericCreateSaver): From 5b0826a16da2678ca26581dc6df933593550bed4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:40:26 +0100 Subject: [PATCH 083/284] Fix repair when objects are not currently selected --- .../publish/validate_transform_zero.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py index 4ca1a86de3..465ec15d7b 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py @@ -6,7 +6,7 @@ import bpy import pyblish.api -from ayon_core.hosts.blender.api import plugin +from ayon_core.hosts.blender.api import plugin, lib import ayon_core.hosts.blender.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, @@ -63,13 +63,18 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, context = plugin.create_blender_context( active=invalid[0], selected=invalid ) - with bpy.context.temp_override(**context): - # TODO: Preferably this does allow custom pivot point locations - # and if so, this should likely apply to the delta instead - # using `bpy.ops.object.transforms_to_deltas(mode="ALL")` - bpy.ops.object.transform_apply(location=True, - rotation=True, - scale=True) + with lib.maintained_selection(): + with bpy.context.temp_override(**context): + plugin.deselect_all() + for obj in invalid: + obj.select_set(True) + + # TODO: Preferably this does allow custom pivot point locations + # and if so, this should likely apply to the delta instead + # using `bpy.ops.object.transforms_to_deltas(mode="ALL")` + bpy.ops.object.transform_apply(location=True, + rotation=True, + scale=True) def get_description(self): return inspect.cleandoc( From bfd6325d18232c11069078fe8b07c85fb0ffb362 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 12:54:24 +0100 Subject: [PATCH 084/284] Houdini: Allow loading any alembic file by .abc extension, regardless of representation name --- client/ayon_core/hosts/houdini/plugins/load/load_alembic.py | 3 ++- .../hosts/houdini/plugins/load/load_alembic_archive.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py index 3398920e87..a77d06d409 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py @@ -11,7 +11,8 @@ class AbcLoader(load.LoaderPlugin): product_types = {"model", "animation", "pointcache", "gpuCache"} label = "Load Alembic" - representations = ["abc"] + representations = ["*"] + extensions = {"abc"} order = -10 icon = "code-fork" color = "orange" diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py b/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py index 8d3becb973..39928fd952 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py @@ -11,7 +11,8 @@ class AbcArchiveLoader(load.LoaderPlugin): product_types = {"model", "animation", "pointcache", "gpuCache"} label = "Load Alembic as Archive" - representations = ["abc"] + representations = ["*"] + extensions = {"abc"} order = -5 icon = "code-fork" color = "orange" From 83fa71982a109cc41460b8ca510538a5dfa67c5e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 24 Mar 2024 19:21:04 +0100 Subject: [PATCH 085/284] Update client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py --- .../hosts/blender/plugins/publish/validate_model_uv_map1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py index 2315d54a2c..752bc5fa58 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py @@ -84,7 +84,7 @@ class ValidateModelMeshUvMap1( def get_description(self): return inspect.cleandoc( - """## Meshes must have `map1` uv set + """## Meshes must have map1 uv set To accompany a better Maya-focused pipeline with Alembics it is expected that a Mesh has a `map1` UV set. Blender defaults to From da31aa2359fad434f204e7e72bc5790424cd9517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:08:47 +0100 Subject: [PATCH 086/284] Update cli_commands.py import qt related stuff only when using qt --- client/ayon_core/cli_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 4335a3f2d9..bc0a22382c 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -67,8 +67,6 @@ class Commands: install_ayon_plugins, get_global_context, ) - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context # Register target and host import pyblish.api @@ -134,6 +132,8 @@ class Commands: print(plugin) if gui: + from ayon_core.tools.utils.host_tools import show_publish + from ayon_core.tools.utils.lib import qt_app_context with qt_app_context(): show_publish() else: From 55ad0586dccb15331701299526f9733242e49e12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:17:10 +0100 Subject: [PATCH 087/284] use AYON in docstrings, comments and readme --- client/ayon_core/hosts/flame/api/__init__.py | 2 +- client/ayon_core/hosts/flame/api/constants.py | 6 +++--- .../ayon_core/hosts/flame/api/scripts/wiretap_com.py | 2 +- client/ayon_core/hosts/flame/api/utils.py | 2 +- .../ayon_core/hosts/flame/startup/openpype_in_flame.py | 2 +- client/ayon_core/hosts/fusion/api/menu.py | 2 +- client/ayon_core/hosts/fusion/api/pipeline.py | 10 +++++----- .../hosts/fusion/deploy/MenuScripts/README.md | 4 ++-- .../hosts/fusion/hooks/pre_fusion_profile_hook.py | 2 +- .../ayon_core/hosts/fusion/hooks/pre_fusion_setup.py | 2 +- client/ayon_core/hosts/max/api/pipeline.py | 4 ++-- .../ayon_core/hosts/max/hooks/force_startup_script.py | 2 +- client/ayon_core/hosts/max/hooks/inject_python.py | 2 +- client/ayon_core/hosts/max/startup/startup.ms | 2 +- client/ayon_core/hosts/maya/api/commands.py | 2 +- client/ayon_core/hosts/maya/api/lib.py | 6 +++--- client/ayon_core/hosts/maya/api/lib_rendersetup.py | 2 +- client/ayon_core/hosts/maya/api/render_setup_tools.py | 2 +- .../hosts/maya/plugins/create/convert_legacy.py | 2 +- .../hosts/maya/plugins/publish/extract_look.py | 6 +++--- .../maya/plugins/publish/validate_loaded_plugin.py | 2 +- .../ayon_core/hosts/nuke/startup/custom_write_node.py | 2 +- .../hosts/nuke/startup/frame_setting_for_read_nodes.py | 2 +- client/ayon_core/hosts/unreal/api/pipeline.py | 2 +- client/ayon_core/pipeline/create/legacy_create.py | 2 +- client/ayon_core/pipeline/publish/README.md | 6 +++--- .../pipeline/workfile/workfile_template_builder.py | 2 +- 27 files changed, 41 insertions(+), 41 deletions(-) diff --git a/client/ayon_core/hosts/flame/api/__init__.py b/client/ayon_core/hosts/flame/api/__init__.py index c00ee958b6..e2c5ee154a 100644 --- a/client/ayon_core/hosts/flame/api/__init__.py +++ b/client/ayon_core/hosts/flame/api/__init__.py @@ -1,5 +1,5 @@ """ -OpenPype Autodesk Flame api +AYON Autodesk Flame api """ from .constants import ( COLOR_MAP, diff --git a/client/ayon_core/hosts/flame/api/constants.py b/client/ayon_core/hosts/flame/api/constants.py index 1833031e13..04191c539d 100644 --- a/client/ayon_core/hosts/flame/api/constants.py +++ b/client/ayon_core/hosts/flame/api/constants.py @@ -1,14 +1,14 @@ """ -OpenPype Flame api constances +AYON Flame api constances """ -# OpenPype marker workflow variables +# AYON marker workflow variables MARKER_NAME = "OpenPypeData" MARKER_DURATION = 0 MARKER_COLOR = "cyan" MARKER_PUBLISH_DEFAULT = False -# OpenPype color definitions +# AYON color definitions COLOR_MAP = { "red": (1.0, 0.0, 0.0), "orange": (1.0, 0.5, 0.0), diff --git a/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py b/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py index cffc6ec782..42b9257cbe 100644 --- a/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py +++ b/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py @@ -61,7 +61,7 @@ class WireTapCom(object): def get_launch_args( self, project_name, project_data, user_name, *args, **kwargs): - """Forming launch arguments for OpenPype launcher. + """Forming launch arguments for AYON launcher. Args: project_name (str): name of project diff --git a/client/ayon_core/hosts/flame/api/utils.py b/client/ayon_core/hosts/flame/api/utils.py index 91584456a6..e37555567e 100644 --- a/client/ayon_core/hosts/flame/api/utils.py +++ b/client/ayon_core/hosts/flame/api/utils.py @@ -11,7 +11,7 @@ log = Logger.get_logger(__name__) def _sync_utility_scripts(env=None): """ Synchronizing basic utlility scripts for flame. - To be able to run start OpenPype within Flame we have to copy + To be able to run start AYON within Flame we have to copy all utility_scripts and additional FLAME_SCRIPT_DIR into `/opt/Autodesk/shared/python`. This will be always synchronizing those folders. diff --git a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py index cf0a24ede2..c57c76f73a 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py +++ b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py @@ -12,7 +12,7 @@ from ayon_core.pipeline import ( def openpype_install(): - """Registering OpenPype in context + """Registering AYON in context """ install_host(opfapi) print("Registered host: {}".format(registered_host())) diff --git a/client/ayon_core/hosts/fusion/api/menu.py b/client/ayon_core/hosts/fusion/api/menu.py index 642287eb10..7fdc3cc21c 100644 --- a/client/ayon_core/hosts/fusion/api/menu.py +++ b/client/ayon_core/hosts/fusion/api/menu.py @@ -125,7 +125,7 @@ class OpenPypeMenu(QtWidgets.QWidget): self._pulse = FusionPulse(parent=self) self._pulse.start() - # Detect Fusion events as OpenPype events + # Detect Fusion events as AYON events self._event_handler = FusionEventHandler(parent=self) self._event_handler.start() diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 3bb66619a9..50157cfae6 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -70,7 +70,7 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "fusion" def install(self): - """Install fusion-specific functionality of OpenPype. + """Install fusion-specific functionality of AYON. This is where you install menus and register families, data and loaders into fusion. @@ -177,7 +177,7 @@ def on_after_open(event): if any_outdated_containers(): log.warning("Scene has outdated content.") - # Find OpenPype menu to attach to + # Find AYON menu to attach to from . import menu def _on_show_scene_inventory(): @@ -326,9 +326,9 @@ class FusionEventThread(QtCore.QThread): class FusionEventHandler(QtCore.QObject): - """Emits OpenPype events based on Fusion events captured in a QThread. + """Emits AYON events based on Fusion events captured in a QThread. - This will emit the following OpenPype events based on Fusion actions: + This will emit the following AYON events based on Fusion actions: save: Comp_Save, Comp_SaveAs open: Comp_Opened new: Comp_New @@ -374,7 +374,7 @@ class FusionEventHandler(QtCore.QObject): self._event_thread.stop() def _on_event(self, event): - """Handle Fusion events to emit OpenPype events""" + """Handle Fusion events to emit AYON events""" if not event: return diff --git a/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md b/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md index f87eaea4a2..e291b8d8f2 100644 --- a/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md +++ b/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md @@ -1,6 +1,6 @@ -### OpenPype deploy MenuScripts +### AYON deploy MenuScripts Note that this `MenuScripts` is not an official Fusion folder. -OpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions. +AYON only uses this folder in `{fusion}/deploy/` to trigger the AYON menu actions. They are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`. \ No newline at end of file diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py index 5aa2783129..10b1c9c45d 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py @@ -19,7 +19,7 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook): Prepares local Fusion profile directory, copies existing Fusion profile. This also sets FUSION MasterPrefs variable, which is used to apply Master.prefs file to override some Fusion profile settings to: - - enable the OpenPype menu + - enable the AYON menu - force Python 3 over Python 2 - force English interface Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py index 7eaf2ddc02..5e97ae3de1 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py @@ -13,7 +13,7 @@ from ayon_core.hosts.fusion import ( class FusionPrelaunch(PreLaunchHook): """ - Prepares OpenPype Fusion environment. + Prepares AYON Fusion environment. Requires correct Python home variable to be defined in the environment settings for Fusion to point at a valid Python 3 build for Fusion. Python3 versions that are supported by Fusion: diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index 6fd0a501ff..fc42eada7c 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pipeline tools for OpenPype Houdini integration.""" +"""Pipeline tools for AYON Houdini integration.""" import os import logging from operator import attrgetter @@ -148,7 +148,7 @@ attributes "OpenPypeContext" def ls() -> list: - """Get all OpenPype instances.""" + """Get all AYON containers.""" objs = rt.objects containers = [ obj for obj in objs diff --git a/client/ayon_core/hosts/max/hooks/force_startup_script.py b/client/ayon_core/hosts/max/hooks/force_startup_script.py index 659be7dfc6..8ccd658e8f 100644 --- a/client/ayon_core/hosts/max/hooks/force_startup_script.py +++ b/client/ayon_core/hosts/max/hooks/force_startup_script.py @@ -6,7 +6,7 @@ from ayon_core.lib.applications import PreLaunchHook, LaunchTypes class ForceStartupScript(PreLaunchHook): - """Inject OpenPype environment to 3ds max. + """Inject AYON environment to 3ds max. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH diff --git a/client/ayon_core/hosts/max/hooks/inject_python.py b/client/ayon_core/hosts/max/hooks/inject_python.py index 36d53551ba..b1b36e75bd 100644 --- a/client/ayon_core/hosts/max/hooks/inject_python.py +++ b/client/ayon_core/hosts/max/hooks/inject_python.py @@ -5,7 +5,7 @@ from ayon_core.lib.applications import PreLaunchHook, LaunchTypes class InjectPythonPath(PreLaunchHook): - """Inject OpenPype environment to 3dsmax. + """Inject AYON environment to 3dsmax. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH diff --git a/client/ayon_core/hosts/max/startup/startup.ms b/client/ayon_core/hosts/max/startup/startup.ms index 4c597901f3..2dfe53a6a5 100644 --- a/client/ayon_core/hosts/max/startup/startup.ms +++ b/client/ayon_core/hosts/max/startup/startup.ms @@ -1,4 +1,4 @@ --- OpenPype Init Script +-- AYON Init Script ( local sysPath = dotNetClass "System.IO.Path" local sysDir = dotNetClass "System.IO.Directory" diff --git a/client/ayon_core/hosts/maya/api/commands.py b/client/ayon_core/hosts/maya/api/commands.py index e63800e542..22cf0871e2 100644 --- a/client/ayon_core/hosts/maya/api/commands.py +++ b/client/ayon_core/hosts/maya/api/commands.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""OpenPype script commands to be used directly in Maya.""" +"""AYON script commands to be used directly in Maya.""" from maya import cmds from ayon_api import get_project, get_folder_by_path diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 7c3c739d7c..8ca898f621 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2931,13 +2931,13 @@ def bake_to_world_space(nodes, def load_capture_preset(data): - """Convert OpenPype Extract Playblast settings to `capture` arguments + """Convert AYON Extract Playblast settings to `capture` arguments Input data is the settings from: `project_settings/maya/publish/ExtractPlayblast/capture_preset` Args: - data (dict): Capture preset settings from OpenPype settings + data (dict): Capture preset settings from AYON settings Returns: dict: `capture.capture` compatible keyword arguments @@ -3288,7 +3288,7 @@ def set_colorspace(): else: # TODO: deprecated code from 3.15.5 - remove # Maya 2022+ introduces new OCIO v2 color management settings that - # can override the old color management preferences. OpenPype has + # can override the old color management preferences. AYON has # separate settings for both so we fall back when necessary. use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"] if use_ocio_v2 and not ocio_v2_support: diff --git a/client/ayon_core/hosts/maya/api/lib_rendersetup.py b/client/ayon_core/hosts/maya/api/lib_rendersetup.py index fb6dd13ce0..c2b5ec843c 100644 --- a/client/ayon_core/hosts/maya/api/lib_rendersetup.py +++ b/client/ayon_core/hosts/maya/api/lib_rendersetup.py @@ -3,7 +3,7 @@ https://github.com/Colorbleed/colorbleed-config/blob/acre/colorbleed/maya/lib_rendersetup.py Credits: Roy Nieterau (BigRoy) / Colorbleed -Modified for use in OpenPype +Modified for use in AYON """ diff --git a/client/ayon_core/hosts/maya/api/render_setup_tools.py b/client/ayon_core/hosts/maya/api/render_setup_tools.py index a6b46e1e9a..a5e04de184 100644 --- a/client/ayon_core/hosts/maya/api/render_setup_tools.py +++ b/client/ayon_core/hosts/maya/api/render_setup_tools.py @@ -5,7 +5,7 @@ Export Maya nodes from Render Setup layer as if flattened in that layer instead of exporting the defaultRenderLayer as Maya forces by default Credits: Roy Nieterau (BigRoy) / Colorbleed -Modified for use in OpenPype +Modified for use in AYON """ diff --git a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py index 9907b5f340..685602ef0b 100644 --- a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py @@ -19,7 +19,7 @@ class MayaLegacyConvertor(ProductConvertorPlugin, Its limitation is that you can have multiple creators creating product of the same type and there is no way to handle it. This code should - nevertheless cover all creators that came with OpenPype. + nevertheless cover all creators that came with AYON. """ identifier = "io.openpype.creators.maya.legacy" diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py index fa74dd18d3..2a86b20131 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py @@ -106,10 +106,10 @@ class TextureProcessor: self.log = log def apply_settings(self, project_settings): - """Apply OpenPype system/project settings to the TextureProcessor + """Apply AYON system/project settings to the TextureProcessor Args: - project_settings (dict): OpenPype project settings + project_settings (dict): AYON project settings Returns: None @@ -278,7 +278,7 @@ class MakeTX(TextureProcessor): """Process the texture. This function requires the `maketx` executable to be available in an - OpenImageIO toolset detectable by OpenPype. + OpenImageIO toolset detectable by AYON. Args: source (str): Path to source file. diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py b/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py index d155a9565a..a05920a21e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -24,7 +24,7 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin, invalid = [] loaded_plugin = cmds.pluginInfo(query=True, listPlugins=True) - # get variable from OpenPype settings + # get variable from AYON settings whitelist_native_plugins = cls.whitelist_native_plugins authorized_plugins = cls.authorized_plugins or [] diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/client/ayon_core/hosts/nuke/startup/custom_write_node.py index 075c8e7a17..d5b504a44b 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/client/ayon_core/hosts/nuke/startup/custom_write_node.py @@ -1,4 +1,4 @@ -""" OpenPype custom script for setting up write nodes for non-publish """ +""" AYON custom script for setting up write nodes for non-publish """ import os import nuke import nukescripts diff --git a/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py b/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py index f0cbabe20f..3e1430c3b1 100644 --- a/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py +++ b/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -1,4 +1,4 @@ -""" OpenPype custom script for resetting read nodes start frame values """ +""" AYON custom script for resetting read nodes start frame values """ import nuke import nukescripts diff --git a/client/ayon_core/hosts/unreal/api/pipeline.py b/client/ayon_core/hosts/unreal/api/pipeline.py index 1f937437a4..a60564d5b0 100644 --- a/client/ayon_core/hosts/unreal/api/pipeline.py +++ b/client/ayon_core/hosts/unreal/api/pipeline.py @@ -98,7 +98,7 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): def install(): - """Install Unreal configuration for OpenPype.""" + """Install Unreal configuration for AYON.""" print("-=" * 40) logo = '''. . diff --git a/client/ayon_core/pipeline/create/legacy_create.py b/client/ayon_core/pipeline/create/legacy_create.py index a8a39b41e3..ab939343c9 100644 --- a/client/ayon_core/pipeline/create/legacy_create.py +++ b/client/ayon_core/pipeline/create/legacy_create.py @@ -44,7 +44,7 @@ class LegacyCreator(object): @classmethod def apply_settings(cls, project_settings): - """Apply OpenPype settings to a plugin class.""" + """Apply AYON settings to a plugin class.""" host_name = os.environ.get("AYON_HOST_NAME") plugin_type = "create" diff --git a/client/ayon_core/pipeline/publish/README.md b/client/ayon_core/pipeline/publish/README.md index 2a0f45d093..ee2124dfd3 100644 --- a/client/ayon_core/pipeline/publish/README.md +++ b/client/ayon_core/pipeline/publish/README.md @@ -1,8 +1,8 @@ # Publish -OpenPype is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. +AYON is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. ## Exceptions -OpenPype define few specific exceptions that should be used in publish plugins. +AYON define few specific exceptions that should be used in publish plugins. ### Validation exception Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. @@ -35,4 +35,4 @@ class MyExtendedPlugin( ### Extensions Currently only extension is ability to define attributes for instances during creation. Method `get_attribute_defs` returns attribute definitions for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be implemented `convert_attribute_values`. Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. -Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. +Possible attribute definitions can be found in `ayon_core/lib/attribute_definitions.py`. diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 124952b2c0..1d7b5ed5a7 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1726,7 +1726,7 @@ class PlaceholderCreateMixin(object): items=creator_items, tooltip=( "Creator" - "\nDefines what OpenPype creator will be used to" + "\nDefines what AYON creator will be used to" " create publishable instance." "\nUseable creator depends on current host's creator list." "\nField is case sensitive." From cd0ee3b85c9accee167f68a1c2f1e366c6df1e03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:22:29 +0100 Subject: [PATCH 088/284] more AYON in comments and docstrings --- client/ayon_core/addon/README.md | 2 +- client/ayon_core/addon/base.py | 2 +- client/ayon_core/lib/applications.py | 2 +- client/ayon_core/lib/ayon_info.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index a15e8bdc69..8b6eccfc69 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -89,4 +89,4 @@ AYON addons should contain separated logic of specific kind of implementation, s ### TrayAddonsManager - inherits from `AddonsManager` -- has specific implementation for Pype Tray tool and handle `ITrayAddon` methods +- has specific implementation for AYON Tray and handle `ITrayAddon` methods diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index bbd5a486fe..42b53c59e3 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -741,7 +741,7 @@ class AddonsManager: addon_classes = [] for module in openpype_modules: - # Go through globals in `pype.modules` + # Go through globals in `ayon_core.modules` for name in dir(module): modules_item = getattr(module, name, None) # Filter globals that are not classes which inherit from diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 4bf0c31d93..8912deaede 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -204,7 +204,7 @@ class ApplicationGroup: Application group wraps different versions(variants) of application. e.g. "maya" is group and "maya_2020" is variant. - Group hold `host_name` which is implementation name used in pype. Also + Group hold `host_name` which is implementation name used in AYON. Also holds `enabled` if whole app group is enabled or `icon` for application icon path in resources. diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index ec37d735d8..3975b35bc3 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -102,8 +102,8 @@ def get_all_current_info(): def extract_ayon_info_to_file(dirpath, filename=None): """Extract all current info to a file. - It is possible to define only directory path. Filename is concatenated with - pype version, workstation site id and timestamp. + It is possible to define only directory path. Filename is concatenated + with AYON version, workstation site id and timestamp. Args: dirpath (str): Path to directory where file will be stored. From 096a1f1f1f8908536d99f438741ddf37822fb3a3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:23:19 +0100 Subject: [PATCH 089/284] use ayon in logs --- client/ayon_core/hosts/flame/api/pipeline.py | 6 +++--- client/ayon_core/hosts/flame/api/utils.py | 2 +- .../maya/plugins/publish/validate_vray_referenced_aovs.py | 2 +- client/ayon_core/lib/terminal.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/flame/api/pipeline.py b/client/ayon_core/hosts/flame/api/pipeline.py index a902b9ee73..4578d7bb4b 100644 --- a/client/ayon_core/hosts/flame/api/pipeline.py +++ b/client/ayon_core/hosts/flame/api/pipeline.py @@ -38,12 +38,12 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info("OpenPype Flame plug-ins registered ...") + log.info("AYON Flame plug-ins registered ...") # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) - log.info("OpenPype Flame host installed ...") + log.info("AYON Flame host installed ...") def uninstall(): @@ -57,7 +57,7 @@ def uninstall(): # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) - log.info("OpenPype Flame host uninstalled ...") + log.info("AYON Flame host uninstalled ...") def containerise(flame_clip_segment, diff --git a/client/ayon_core/hosts/flame/api/utils.py b/client/ayon_core/hosts/flame/api/utils.py index e37555567e..b76dd92ada 100644 --- a/client/ayon_core/hosts/flame/api/utils.py +++ b/client/ayon_core/hosts/flame/api/utils.py @@ -124,7 +124,7 @@ def setup(env=None): # synchronize resolve utility scripts _sync_utility_scripts(env) - log.info("Flame OpenPype wrapper has been installed") + log.info("Flame AYON wrapper has been installed") def get_flame_version(): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py index 0ad08b7d14..7c480a6bf7 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py @@ -45,7 +45,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, self.log.warning(( "Referenced AOVs are enabled in Vray " "Render Settings and are detected in scene, but " - "Pype render instance option for referenced AOVs is " + "AYON render instance option for referenced AOVs is " "disabled. Those AOVs will be rendered but not published " "by Pype." )) diff --git a/client/ayon_core/lib/terminal.py b/client/ayon_core/lib/terminal.py index f822a37286..a22f2358aa 100644 --- a/client/ayon_core/lib/terminal.py +++ b/client/ayon_core/lib/terminal.py @@ -69,7 +69,7 @@ class Terminal: Terminal.use_colors = False print( "Module `blessed` failed on import or terminal creation." - " Pype terminal won't use colors." + " AYON terminal won't use colors." ) Terminal._initialized = True return From 23584110d4828a67951da7751c8a32fabf45cf6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:24:00 +0100 Subject: [PATCH 090/284] use AYON in uis --- client/ayon_core/hosts/flame/api/plugin.py | 2 +- client/ayon_core/hosts/flame/hooks/pre_flame_setup.py | 2 +- .../flame/startup/openpype_babypublisher/modules/panel_app.py | 2 +- .../startup/openpype_babypublisher/openpype_babypublisher.py | 2 +- client/ayon_core/hosts/flame/startup/openpype_in_flame.py | 2 +- client/ayon_core/hosts/hiero/api/plugin.py | 2 +- .../ayon_core/hosts/hiero/plugins/create/create_shot_clip.py | 2 +- client/ayon_core/hosts/maya/startup/userSetup.py | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/client/ayon_core/hosts/flame/api/plugin.py index c5667eb75a..c57d021c69 100644 --- a/client/ayon_core/hosts/flame/api/plugin.py +++ b/client/ayon_core/hosts/flame/api/plugin.py @@ -38,7 +38,7 @@ class CreatorWidget(QtWidgets.QDialog): | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) - self.setWindowTitle(name or "Pype Creator Input") + self.setWindowTitle(name or "AYON Creator Input") self.resize(500, 700) # Where inputs and labels are set diff --git a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py index b7fc431352..1ff7ad7ccf 100644 --- a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py +++ b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py @@ -72,7 +72,7 @@ class FlamePrelaunch(PreLaunchHook): project_data = { "Name": project_entity["name"], "Nickname": project_entity["code"], - "Description": "Created by OpenPype", + "Description": "Created by AYON", "SetupDir": project_entity["name"], "FrameWidth": int(width), "FrameHeight": int(height), diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py index 5c5bb0b4a1..ce023a9e4d 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py +++ b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py @@ -79,7 +79,7 @@ class FlameBabyPublisherPanel(object): # creating ui self.window.setMinimumSize(1500, 600) - self.window.setWindowTitle('OpenPype: Baby-publisher') + self.window.setWindowTitle('AYON: Baby-publisher') self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.window.setFocusPolicy(QtCore.Qt.StrongFocus) diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py index 4675d163e3..76d74b5970 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py +++ b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py @@ -31,7 +31,7 @@ def scope_sequence(selection): def get_media_panel_custom_ui_actions(): return [ { - "name": "OpenPype: Baby-publisher", + "name": "AYON: Baby-publisher", "actions": [ { "name": "Create Shots", diff --git a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py index c57c76f73a..b9cbf9700b 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py +++ b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py @@ -28,7 +28,7 @@ def exeption_handler(exctype, value, _traceback): tb (str): traceback to show """ import traceback - msg = "OpenPype: Python exception {} in {}".format(value, exctype) + msg = "AYON: Python exception {} in {}".format(value, exctype) mbox = QtWidgets.QMessageBox() mbox.setText(msg) mbox.setDetailedText( diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 6a665dc9c5..4878368716 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -45,7 +45,7 @@ class CreatorWidget(QtWidgets.QDialog): | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) - self.setWindowTitle(name or "Pype Creator Input") + self.setWindowTitle(name or "AYON Creator Input") self.resize(500, 700) # Where inputs and labels are set diff --git a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py index 90ea9ef50f..62e7041286 100644 --- a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py @@ -16,7 +16,7 @@ class CreateShotClip(phiero.Creator): gui_tracks = [track.name() for track in phiero.get_current_sequence().videoTracks()] - gui_name = "Pype publish attributes creator" + gui_name = "AYON publish attributes creator" gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { "renameHierarchy": { diff --git a/client/ayon_core/hosts/maya/startup/userSetup.py b/client/ayon_core/hosts/maya/startup/userSetup.py index adbbfe4f44..3112e2bf12 100644 --- a/client/ayon_core/hosts/maya/startup/userSetup.py +++ b/client/ayon_core/hosts/maya/startup/userSetup.py @@ -10,7 +10,7 @@ from maya import cmds host = MayaHost() install_host(host) -print("Starting OpenPype usersetup...") +print("Starting AYON usersetup...") project_name = get_current_project_name() settings = get_project_settings(project_name) @@ -47,4 +47,4 @@ if bool(int(os.environ.get(key, "0"))): ) -print("Finished OpenPype usersetup.") +print("Finished AYON usersetup.") From 1ef27f28390ccfd6da0501441a810dcffa26d047 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:27:10 +0100 Subject: [PATCH 091/284] use ayon name in classes and functions --- client/ayon_core/hosts/fusion/api/__init__.py | 4 +- client/ayon_core/hosts/fusion/api/menu.py | 14 ++--- .../fusion/deploy/MenuScripts/launch_menu.py | 2 +- .../hosts/houdini/startup/MainMenuCommon.xml | 2 +- client/ayon_core/hosts/max/api/menu.py | 58 +++++++++---------- client/ayon_core/hosts/max/api/pipeline.py | 6 +- .../ayon_core/hosts/resolve/api/__init__.py | 4 +- client/ayon_core/hosts/resolve/api/menu.py | 12 ++-- .../hosts/resolve/api/menu_style.qss | 2 +- client/ayon_core/hosts/resolve/startup.py | 2 +- .../resolve/utility_scripts/AYON__Menu.py | 4 +- 11 files changed, 55 insertions(+), 55 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/__init__.py b/client/ayon_core/hosts/fusion/api/__init__.py index ddd718e606..d2feee6d23 100644 --- a/client/ayon_core/hosts/fusion/api/__init__.py +++ b/client/ayon_core/hosts/fusion/api/__init__.py @@ -15,7 +15,7 @@ from .lib import ( comp_lock_and_undo_chunk ) -from .menu import launch_openpype_menu +from .menu import launch_ayon_menu __all__ = [ @@ -35,5 +35,5 @@ __all__ = [ "comp_lock_and_undo_chunk", # menu - "launch_openpype_menu", + "launch_ayon_menu", ] diff --git a/client/ayon_core/hosts/fusion/api/menu.py b/client/ayon_core/hosts/fusion/api/menu.py index 7fdc3cc21c..6a64ad2120 100644 --- a/client/ayon_core/hosts/fusion/api/menu.py +++ b/client/ayon_core/hosts/fusion/api/menu.py @@ -28,9 +28,9 @@ self = sys.modules[__name__] self.menu = None -class OpenPypeMenu(QtWidgets.QWidget): +class AYONMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AYONMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") @@ -174,16 +174,16 @@ class OpenPypeMenu(QtWidgets.QWidget): set_current_context_framerange() -def launch_openpype_menu(): +def launch_ayon_menu(): app = get_qt_app() - pype_menu = OpenPypeMenu() + ayon_menu = AYONMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() - self.menu = pype_menu + ayon_menu.show() + self.menu = ayon_menu result = app.exec_() print("Shutting down..") diff --git a/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py b/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py index 23b02b1b69..640f78eeb8 100644 --- a/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py +++ b/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py @@ -35,7 +35,7 @@ def main(env): log = Logger.get_logger(__name__) log.info(f"Registered host: {registered_host()}") - menu.launch_openpype_menu() + menu.launch_ayon_menu() # Initiate a QTimer to check if Fusion is still alive every X interval # If Fusion is not found - kill itself diff --git a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml index b93445a974..b6e78cbdc8 100644 --- a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml +++ b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml @@ -1,7 +1,7 @@ - + QtWidgets.QAction: """Create AYON menu. @@ -73,7 +73,7 @@ class OpenPypeMenu(object): help_action = None for item in menu_items: if name in item.title(): - # we already have OpenPype menu + # we already have AYON menu return item if before in item.title(): @@ -85,50 +85,50 @@ class OpenPypeMenu(object): self.menu = op_menu return op_menu - def build_openpype_menu(self) -> QtWidgets.QAction: + def _build_ayon_menu(self) -> QtWidgets.QAction: """Build items in AYON menu.""" - openpype_menu = self.get_or_create_openpype_menu() - load_action = QtWidgets.QAction("Load...", openpype_menu) + ayon_menu = self._get_or_create_ayon_menu() + load_action = QtWidgets.QAction("Load...", ayon_menu) load_action.triggered.connect(self.load_callback) - openpype_menu.addAction(load_action) + ayon_menu.addAction(load_action) - publish_action = QtWidgets.QAction("Publish...", openpype_menu) + publish_action = QtWidgets.QAction("Publish...", ayon_menu) publish_action.triggered.connect(self.publish_callback) - openpype_menu.addAction(publish_action) + ayon_menu.addAction(publish_action) - manage_action = QtWidgets.QAction("Manage...", openpype_menu) + manage_action = QtWidgets.QAction("Manage...", ayon_menu) manage_action.triggered.connect(self.manage_callback) - openpype_menu.addAction(manage_action) + ayon_menu.addAction(manage_action) - library_action = QtWidgets.QAction("Library...", openpype_menu) + library_action = QtWidgets.QAction("Library...", ayon_menu) library_action.triggered.connect(self.library_callback) - openpype_menu.addAction(library_action) + ayon_menu.addAction(library_action) - openpype_menu.addSeparator() + ayon_menu.addSeparator() - workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) + workfiles_action = QtWidgets.QAction("Work Files...", ayon_menu) workfiles_action.triggered.connect(self.workfiles_callback) - openpype_menu.addAction(workfiles_action) + ayon_menu.addAction(workfiles_action) - openpype_menu.addSeparator() + ayon_menu.addSeparator() - res_action = QtWidgets.QAction("Set Resolution", openpype_menu) + res_action = QtWidgets.QAction("Set Resolution", ayon_menu) res_action.triggered.connect(self.resolution_callback) - openpype_menu.addAction(res_action) + ayon_menu.addAction(res_action) - frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) + frame_action = QtWidgets.QAction("Set Frame Range", ayon_menu) frame_action.triggered.connect(self.frame_range_callback) - openpype_menu.addAction(frame_action) + ayon_menu.addAction(frame_action) - colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) + colorspace_action = QtWidgets.QAction("Set Colorspace", ayon_menu) colorspace_action.triggered.connect(self.colorspace_callback) - openpype_menu.addAction(colorspace_action) + ayon_menu.addAction(colorspace_action) - unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu) + unit_scale_action = QtWidgets.QAction("Set Unit Scale", ayon_menu) unit_scale_action.triggered.connect(self.unit_scale_callback) - openpype_menu.addAction(unit_scale_action) + ayon_menu.addAction(unit_scale_action) - return openpype_menu + return ayon_menu def load_callback(self): """Callback to show Loader tool.""" diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index fc42eada7c..afad6cbe75 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -14,7 +14,7 @@ from ayon_core.pipeline import ( AVALON_CONTAINER_ID, AYON_CONTAINER_ID, ) -from ayon_core.hosts.max.api.menu import OpenPypeMenu +from ayon_core.hosts.max.api.menu import AYONMenu from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from ayon_core.hosts.max import MAX_HOST_DIR @@ -48,7 +48,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_creator_plugin_path(CREATE_PATH) # self._register_callbacks() - self.menu = OpenPypeMenu() + self.menu = AYONMenu() self._has_been_setup = True @@ -94,7 +94,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _deferred_menu_creation(self): self.log.info("Building menu ...") - self.menu = OpenPypeMenu() + self.menu = AYONMenu() @staticmethod def create_context_node(): diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index dba275e6c4..3359430ef5 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -44,7 +44,7 @@ from .lib import ( get_reformated_path ) -from .menu import launch_pype_menu +from .menu import launch_ayon_menu from .plugin import ( ClipLoader, @@ -113,7 +113,7 @@ __all__ = [ "get_reformated_path", # menu - "launch_pype_menu", + "launch_ayon_menu", # plugin "ClipLoader", diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index 59eba14d83..dd8573acc0 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -38,9 +38,9 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -class OpenPypeMenu(QtWidgets.QWidget): +class AYONMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AYONMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") @@ -170,14 +170,14 @@ class OpenPypeMenu(QtWidgets.QWidget): host_tools.show_experimental_tools_dialog() -def launch_pype_menu(): +def launch_ayon_menu(): app = QtWidgets.QApplication(sys.argv) - pype_menu = OpenPypeMenu() + ayon_menu = AYONMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() + ayon_menu.show() sys.exit(app.exec_()) diff --git a/client/ayon_core/hosts/resolve/api/menu_style.qss b/client/ayon_core/hosts/resolve/api/menu_style.qss index 3d51c7139f..ad8932d881 100644 --- a/client/ayon_core/hosts/resolve/api/menu_style.qss +++ b/client/ayon_core/hosts/resolve/api/menu_style.qss @@ -51,7 +51,7 @@ QLineEdit { qproperty-alignment: AlignCenter; } -#OpenPypeMenu { +#AYONMenu { qproperty-alignment: AlignLeft; min-width: 10em; border: 1px solid #fef9ef; diff --git a/client/ayon_core/hosts/resolve/startup.py b/client/ayon_core/hosts/resolve/startup.py index b3c1a024d9..3ad0a6bf7b 100644 --- a/client/ayon_core/hosts/resolve/startup.py +++ b/client/ayon_core/hosts/resolve/startup.py @@ -35,7 +35,7 @@ def ensure_installed_host(): def launch_menu(): print("Launching Resolve AYON menu..") ensure_installed_host() - ayon_core.hosts.resolve.api.launch_pype_menu() + ayon_core.hosts.resolve.api.launch_ayon_menu() def open_workfile(path): diff --git a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py index 08cefb9d61..b10b477beb 100644 --- a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py +++ b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py @@ -8,13 +8,13 @@ log = Logger.get_logger(__name__) def main(env): - from ayon_core.hosts.resolve.api import ResolveHost, launch_pype_menu + from ayon_core.hosts.resolve.api import ResolveHost, launch_ayon_menu # activate resolve from openpype host = ResolveHost() install_host(host) - launch_pype_menu() + launch_ayon_menu() if __name__ == "__main__": From f5fd0169aa8c9a76c1662ab4fc78e92cc69b25c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:27:41 +0100 Subject: [PATCH 092/284] maya icon buttons use ayon prefix --- client/ayon_core/hosts/maya/api/customize.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/customize.py b/client/ayon_core/hosts/maya/api/customize.py index da046b538d..4db8819ff5 100644 --- a/client/ayon_core/hosts/maya/api/customize.py +++ b/client/ayon_core/hosts/maya/api/customize.py @@ -109,7 +109,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_lookmanager", + "ayon_toolbox_lookmanager", annotation="Look Manager", label="Look Manager", image=os.path.join(icons, "lookmanager.png"), @@ -122,7 +122,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_workfiles", + "ayon_toolbox_workfiles", annotation="Work Files", label="Work Files", image=os.path.join(icons, "workfiles.png"), @@ -137,7 +137,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_loader", + "ayon_toolbox_loader", annotation="Loader", label="Loader", image=os.path.join(icons, "loader.png"), @@ -152,7 +152,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_manager", + "ayon_toolbox_manager", annotation="Inventory", label="Inventory", image=os.path.join(icons, "inventory.png"), From f20ab5328594fe2fdecefbdee8f6996275db9b20 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:27:55 +0100 Subject: [PATCH 093/284] use ayon prefix in tvpaint temp file --- client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py index 182d95d6db..d8c6a7a430 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py @@ -54,7 +54,7 @@ class ImportSound(plugin.Loader): def load(self, context, name, namespace, options): # Create temp file for output output_file = tempfile.NamedTemporaryFile( - mode="w", prefix="pype_tvp_", suffix=".txt", delete=False + mode="w", prefix="ayon_tvp_", suffix=".txt", delete=False ) output_file.close() output_filepath = output_file.name.replace("\\", "/") From 4b9f9dbf5e2c19cd093a324e17a7dd5cc629833d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:28:12 +0100 Subject: [PATCH 094/284] remove pypeclub role from clockify ftrack actions --- .../clockify/ftrack/server/action_clockify_sync_server.py | 2 +- .../modules/clockify/ftrack/user/action_clockify_sync_local.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py b/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py index 985cf49b97..7854f0ceba 100644 --- a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -11,7 +11,7 @@ class SyncClockifyServer(ServerAction): label = "Sync To Clockify (server)" description = "Synchronise data to Clockify workspace" - role_list = ["Pypeclub", "Administrator", "project Manager"] + role_list = ["Administrator", "project Manager"] def __init__(self, *args, **kwargs): super(SyncClockifyServer, self).__init__(*args, **kwargs) diff --git a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py b/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py index 0e8cf6bd37..4701653a0b 100644 --- a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py +++ b/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py @@ -13,7 +13,7 @@ class SyncClockifyLocal(BaseAction): #: Action description. description = 'Synchronise data to Clockify workspace' #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "project Manager"] + role_list = ["Administrator", "project Manager"] #: icon icon = statics_icon("app_icons", "clockify-white.png") From dc9eb3f2605393364679f8c5c02f1720372ce2a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:28:29 +0100 Subject: [PATCH 095/284] removed unused 'OpenPypeCreatorError' from max --- client/ayon_core/hosts/max/api/plugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/hosts/max/api/plugin.py b/client/ayon_core/hosts/max/api/plugin.py index 4d5d18a42d..cd71d068f2 100644 --- a/client/ayon_core/hosts/max/api/plugin.py +++ b/client/ayon_core/hosts/max/api/plugin.py @@ -156,10 +156,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" )""" -class OpenPypeCreatorError(CreatorError): - pass - - class MaxCreatorBase(object): @staticmethod From 9683090ee6d1ba31c190b90e9e19c6611a262cee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:28:48 +0100 Subject: [PATCH 096/284] removed config name validation --- client/ayon_core/pipeline/schema/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/ayon_core/pipeline/schema/__init__.py b/client/ayon_core/pipeline/schema/__init__.py index 3abc576f89..67cf120b59 100644 --- a/client/ayon_core/pipeline/schema/__init__.py +++ b/client/ayon_core/pipeline/schema/__init__.py @@ -44,11 +44,6 @@ def validate(data, schema=None): _precache() root, schema = data["schema"].rsplit(":", 1) - # assert root in ( - # "mindbender-core", # Backwards compatiblity - # "avalon-core", - # "pype" - # ) if isinstance(schema, six.string_types): schema = _cache[schema + ".json"] From 47b2d86829b8843bd211f47fb152e04893fb92e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:29:10 +0100 Subject: [PATCH 097/284] houdini create plugins are using 'CreatorError' --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 5 +++-- .../hosts/houdini/plugins/create/create_redshift_rop.py | 3 ++- .../hosts/houdini/plugins/create/create_vray_rop.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 6a2a41d1d0..b307293dc8 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -2,6 +2,7 @@ """Creator plugin for creating publishable Houdini Digital Assets.""" import ayon_api +from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin import hou @@ -52,7 +53,7 @@ class CreateHDA(plugin.HoudiniCreator): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. if not to_hda.canCreateDigitalAsset(): - raise plugin.OpenPypeCreatorError( + raise CreatorError( "cannot create hda from node {}".format(to_hda)) hda_node = to_hda.createDigitalAsset( @@ -61,7 +62,7 @@ class CreateHDA(plugin.HoudiniCreator): ) hda_node.layoutChildren() elif self._check_existing(folder_path, node_name): - raise plugin.OpenPypeCreatorError( + raise CreatorError( ("product {} is already published with different HDA" "definition.").format(node_name)) else: diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 3d6d657cf0..ba0795a26e 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -2,6 +2,7 @@ """Creator plugin to create Redshift ROP.""" import hou # noqa +from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin from ayon_core.lib import EnumDef, BoolDef @@ -42,7 +43,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): "Redshift_IPR", node_name=f"{basename}_IPR" ) except hou.OperationFailed as e: - raise plugin.OpenPypeCreatorError( + raise CreatorError( ( "Cannot create Redshift node. Is Redshift " "installed and enabled?" diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py index 739796dc7c..6b2396bffb 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py @@ -3,7 +3,7 @@ import hou from ayon_core.hosts.houdini.api import plugin -from ayon_core.pipeline import CreatedInstance +from ayon_core.pipeline import CreatedInstance, CreatorError from ayon_core.lib import EnumDef, BoolDef @@ -42,7 +42,7 @@ class CreateVrayROP(plugin.HoudiniCreator): "vray", node_name=basename + "_IPR" ) except hou.OperationFailed: - raise plugin.OpenPypeCreatorError( + raise CreatorError( "Cannot create Vray render node. " "Make sure Vray installed and enabled!" ) From 6572bead963fb206c7b40434d771f06e0f2a17b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:29:20 +0100 Subject: [PATCH 098/284] removed 'OpenPypeCreatorError' from houdini --- client/ayon_core/hosts/houdini/api/plugin.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index b33d0fe297..a9c8c313b9 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -19,10 +19,6 @@ from ayon_core.lib import BoolDef from .lib import imprint, read, lsattr, add_self_publish_button -class OpenPypeCreatorError(CreatorError): - pass - - class Creator(LegacyCreator): """Creator plugin to create instances in Houdini @@ -92,8 +88,8 @@ class Creator(LegacyCreator): except hou.Error as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError("Creator error: {}".format(er)), + CreatorError, + CreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) @@ -209,8 +205,8 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): except hou.Error as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError("Creator error: {}".format(er)), + CreatorError, + CreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) def lock_parameters(self, node, parameters): From 519218016debc69a296f6181a7ccf62bbb09ea6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 11:56:53 +0100 Subject: [PATCH 099/284] more AYON replacements --- client/ayon_core/hosts/maya/api/menu.py | 2 +- client/ayon_core/hosts/maya/api/pipeline.py | 2 +- client/ayon_core/hosts/resolve/README.markdown | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/menu.py b/client/ayon_core/hosts/maya/api/menu.py index 8de025bfe5..5a266b5f25 100644 --- a/client/ayon_core/hosts/maya/api/menu.py +++ b/client/ayon_core/hosts/maya/api/menu.py @@ -50,7 +50,7 @@ def get_context_label(): def install(project_settings): if cmds.about(batch=True): - log.info("Skipping openpype.menu initialization in batch mode..") + log.info("Skipping AYON menu initialization in batch mode..") return def add_menu(): diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 9792a4a5fe..d30d405a9e 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -673,7 +673,7 @@ def workfile_save_before_xgen(event): switching context. Args: - event (Event) - openpype/lib/events.py + event (Event) - ayon_core/lib/events.py """ if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True): return diff --git a/client/ayon_core/hosts/resolve/README.markdown b/client/ayon_core/hosts/resolve/README.markdown index a8bb071e7e..064e791f65 100644 --- a/client/ayon_core/hosts/resolve/README.markdown +++ b/client/ayon_core/hosts/resolve/README.markdown @@ -18,7 +18,7 @@ This is how it looks on my testing project timeline ![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png) Notice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence. -1. you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** +1. you need to start AYON menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** 2. then select any clips in `main` track and change their color to `Chocolate` 3. in OpenPype Menu select `Create` 4. in Creator select `Create Publishable Clip [New]` (temporary name) From cf1d9b191973063a4496d8c4b6fd1fa8f366dae7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:57:49 +0100 Subject: [PATCH 100/284] Use correct host name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- client/ayon_core/hosts/max/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index afad6cbe75..4b1dcc25d3 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pipeline tools for AYON Houdini integration.""" +"""Pipeline tools for AYON 3ds max integration.""" import os import logging from operator import attrgetter From 0f0f29a7b006fa394b2a7e1c4e0d8593c35f63f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 12:05:58 +0100 Subject: [PATCH 101/284] also change avalon to AYON --- client/ayon_core/addon/README.md | 2 +- client/ayon_core/hosts/max/api/plugin.py | 2 +- client/ayon_core/hosts/maya/api/menu.py | 2 +- client/ayon_core/hosts/maya/api/pipeline.py | 6 +++--- client/ayon_core/hosts/maya/api/plugin.py | 2 +- client/ayon_core/hosts/maya/api/setdress.py | 8 ++++---- .../hosts/maya/plugins/load/load_vdb_to_arnold.py | 2 +- .../hosts/maya/plugins/load/load_vdb_to_redshift.py | 2 +- .../ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py | 2 +- .../hosts/maya/plugins/publish/collect_inputs.py | 6 +++--- client/ayon_core/hosts/nuke/api/pipeline.py | 6 +++--- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index 8b6eccfc69..88c27db154 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -27,7 +27,7 @@ AYON addons should contain separated logic of specific kind of implementation, s - default interfaces are defined in `interfaces.py` ## IPluginPaths -- addon wants to add directory path/s to avalon or publish plugins +- addon wants to add directory path/s to publish, load, create or inventory plugins - addon must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - each key may contain list or string with a path to directory with plugins diff --git a/client/ayon_core/hosts/max/api/plugin.py b/client/ayon_core/hosts/max/api/plugin.py index cd71d068f2..e5d12ce87d 100644 --- a/client/ayon_core/hosts/max/api/plugin.py +++ b/client/ayon_core/hosts/max/api/plugin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""3dsmax specific Avalon/Pyblish plugin definitions.""" +"""3dsmax specific AYON/Pyblish plugin definitions.""" from abc import ABCMeta import six diff --git a/client/ayon_core/hosts/maya/api/menu.py b/client/ayon_core/hosts/maya/api/menu.py index 5a266b5f25..0cb7edd40d 100644 --- a/client/ayon_core/hosts/maya/api/menu.py +++ b/client/ayon_core/hosts/maya/api/menu.py @@ -261,7 +261,7 @@ def popup(): def update_menu_task_label(): - """Update the task label in Avalon menu to current session""" + """Update the task label in AYON menu to current session""" if IS_HEADLESS: return diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index d30d405a9e..b3e401b91e 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -361,13 +361,13 @@ def parse_container(container): def _ls(): - """Yields Avalon container node names. + """Yields AYON container node names. Used by `ls()` to retrieve the nodes and then query the full container's data. Yields: - str: Avalon container node name (objectSet) + str: AYON container node name (objectSet) """ @@ -384,7 +384,7 @@ def _ls(): } # Iterate over all 'set' nodes in the scene to detect whether - # they have the avalon container ".id" attribute. + # they have the ayon container ".id" attribute. fn_dep = om.MFnDependencyNode() iterator = om.MItDependencyNodes(om.MFn.kSet) for mobject in _maya_iterate(iterator): diff --git a/client/ayon_core/hosts/maya/api/plugin.py b/client/ayon_core/hosts/maya/api/plugin.py index bdb0cb1c99..6f8b74c906 100644 --- a/client/ayon_core/hosts/maya/api/plugin.py +++ b/client/ayon_core/hosts/maya/api/plugin.py @@ -899,7 +899,7 @@ class ReferenceLoader(Loader): cmds.disconnectAttr(input, node_attr) cmds.setAttr(node_attr, data["value"]) - # Fix PLN-40 for older containers created with Avalon that had the + # Fix PLN-40 for older containers created with AYON that had the # `.verticesOnlySet` set to True. if cmds.getAttr("{}.verticesOnlySet".format(node)): self.log.info("Setting %s.verticesOnlySet to False", node) diff --git a/client/ayon_core/hosts/maya/api/setdress.py b/client/ayon_core/hosts/maya/api/setdress.py index 7276ae254c..b1d5beb343 100644 --- a/client/ayon_core/hosts/maya/api/setdress.py +++ b/client/ayon_core/hosts/maya/api/setdress.py @@ -150,7 +150,7 @@ def load_package(filepath, name, namespace=None): containers.append(container) # TODO: Do we want to cripple? Or do we want to add a 'parent' parameter? - # Cripple the original avalon containers so they don't show up in the + # Cripple the original AYON containers so they don't show up in the # manager # for container in containers: # cmds.setAttr("%s.id" % container, @@ -175,7 +175,7 @@ def _add(instance, representation_id, loaders, namespace, root="|"): namespace (str): Returns: - str: The created Avalon container. + str: The created AYON container. """ @@ -244,7 +244,7 @@ def _instances_by_namespace(data): def get_contained_containers(container): - """Get the Avalon containers in this container + """Get the AYON containers in this container Args: container (dict): The container dict. @@ -256,7 +256,7 @@ def get_contained_containers(container): from .pipeline import parse_container - # Get avalon containers in this package setdress container + # Get AYON containers in this package setdress container containers = [] members = cmds.sets(container['objectName'], query=True) for node in cmds.ls(members, type="objectSet"): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py index eaa7ff1ae3..f0fb89e5a4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -108,7 +108,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): from maya import cmds - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py index 1707008b67..cad0900590 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -115,7 +115,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): def remove(self, container): from maya import cmds - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py index 42d4583d76..88f62e81a4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -277,7 +277,7 @@ class LoadVDBtoVRay(load.LoaderPlugin): def remove(self, container): - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py b/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py index d084804e05..fa5a694a76 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py @@ -79,12 +79,12 @@ def iter_history(nodes, def collect_input_containers(containers, nodes): """Collect containers that contain any of the node in `nodes`. - This will return any loaded Avalon container that contains at least one of - the nodes. As such, the Avalon container is an input for it. Or in short, + This will return any loaded AYON container that contains at least one of + the nodes. As such, the AYON container is an input for it. Or in short, there are member nodes of that container. Returns: - list: Input avalon containers + list: Input loaded containers """ # Assume the containers have collected their cached '_members' data diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index a8876f6aa7..2255276c56 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -128,7 +128,7 @@ class NukeHost( register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) - # Register Avalon event for workfiles loading. + # Register AYON event for workfiles loading. register_event_callback("workio.open_file", check_inventory_versions) register_event_callback("taskChanged", change_context_label) @@ -230,9 +230,9 @@ def get_context_label(): def _install_menu(): - """Install Avalon menu into Nuke's main menu bar.""" + """Install AYON menu into Nuke's main menu bar.""" - # uninstall original avalon menu + # uninstall original AYON menu main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) From 0b0506392bee97ee9309ad52eb91ab4c8e90d916 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 12:06:14 +0100 Subject: [PATCH 102/284] fix docstring in 'classes_from_module' --- client/ayon_core/lib/python_module_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/python_module_tools.py b/client/ayon_core/lib/python_module_tools.py index 4f9eb7f667..cb6e4c14c4 100644 --- a/client/ayon_core/lib/python_module_tools.py +++ b/client/ayon_core/lib/python_module_tools.py @@ -118,8 +118,8 @@ def classes_from_module(superclass, module): Arguments: superclass (superclass): Superclass of subclasses to look for - module (types.ModuleType): Imported module from which to - parse valid Avalon plug-ins. + module (types.ModuleType): Imported module where to look for + 'superclass' subclasses. Returns: List of plug-ins, or empty list if none is found. From 0be8f7a3e1e93ab83b57351e873261385332530e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 12:06:54 +0100 Subject: [PATCH 103/284] fix docstrings in applications lib --- client/ayon_core/lib/applications.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 8912deaede..cb8087905a 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1897,12 +1897,12 @@ def should_start_last_workfile( `"0", "1", "true", "false", "yes", "no"`. Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. + project_name (str): Project name. + host_name (str): Host name. + task_name (str): Task name. + task_type (str): Task type. + default_output (Optional[bool]): Default output if no profile is + found. Returns: bool: True if host should start workfile. @@ -1947,12 +1947,12 @@ def should_workfile_tool_start( `"0", "1", "true", "false", "yes", "no"`. Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. + project_name (str): Project name. + host_name (str): Host name. + task_name (str): Task name. + task_type (str): Task type. + default_output (Optional[bool]): Default output if no profile is + found. Returns: bool: True if host should start workfile. From ed3ccf090f358d042404971b670dedf7e44838b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 12:28:36 +0100 Subject: [PATCH 104/284] Remove settings --- .../maya/server/settings/publishers.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 3a6de2eb44..ef23b77d19 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -753,14 +753,6 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Ass Relative Paths" ) - ValidateInstancerContent: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Instancer Content" - ) - ValidateInstancerFrameRanges: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Instancer Cache Frame Ranges" - ) ValidateNoDefaultCameras: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Validate No Default Cameras" @@ -1300,16 +1292,6 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": False, "active": True }, - "ValidateInstancerContent": { - "enabled": True, - "optional": False, - "active": True - }, - "ValidateInstancerFrameRanges": { - "enabled": True, - "optional": False, - "active": True - }, "ValidateNoDefaultCameras": { "enabled": True, "optional": False, From 27b6dcafba66fa82695219e582700b34c9a52908 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 12:28:52 +0100 Subject: [PATCH 105/284] Bump version --- server_addon/maya/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 8202425a2d..a86a3ce0a1 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.9" +__version__ = "0.1.10" From e4c8cf2f7e5745a7d703f391c3b1537b90322c36 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 13:04:15 +0100 Subject: [PATCH 106/284] Resolve: Allow to minimize the menu --- client/ayon_core/hosts/resolve/api/menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index 59eba14d83..3324295615 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -48,6 +48,7 @@ class OpenPypeMenu(QtWidgets.QWidget): QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) From d2396140106182cc4b8de4a175fa2f6fc2d9cd43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 13:27:26 +0100 Subject: [PATCH 107/284] Refactor `OpenPype` to `AYON` --- client/ayon_core/hosts/resolve/api/menu.py | 10 +++++----- client/ayon_core/hosts/resolve/api/menu_style.qss | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index 3324295615..7f3a669ecb 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -38,9 +38,9 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -class OpenPypeMenu(QtWidgets.QWidget): +class AYONMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AYONMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") @@ -174,11 +174,11 @@ class OpenPypeMenu(QtWidgets.QWidget): def launch_pype_menu(): app = QtWidgets.QApplication(sys.argv) - pype_menu = OpenPypeMenu() + ayon_menu = AYONMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() + ayon_menu.show() sys.exit(app.exec_()) diff --git a/client/ayon_core/hosts/resolve/api/menu_style.qss b/client/ayon_core/hosts/resolve/api/menu_style.qss index 3d51c7139f..ad8932d881 100644 --- a/client/ayon_core/hosts/resolve/api/menu_style.qss +++ b/client/ayon_core/hosts/resolve/api/menu_style.qss @@ -51,7 +51,7 @@ QLineEdit { qproperty-alignment: AlignCenter; } -#OpenPypeMenu { +#AYONMenu { qproperty-alignment: AlignLeft; min-width: 10em; border: 1px solid #fef9ef; From 3c23f6cc3f833ff085ef1c146511595ef5e16dc7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:00:57 +0100 Subject: [PATCH 108/284] Houdini: Improve load image - Support setting OCIO colorspace - Support single frame file versus sequence - Support 'switch' folder functionality - Allow loading more product types --- .../hosts/houdini/plugins/load/load_image.py | 96 ++++++++++++++----- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_image.py b/client/ayon_core/hosts/houdini/plugins/load/load_image.py index b77e4f662a..72bb873eff 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_image.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_image.py @@ -1,4 +1,5 @@ import os +import re from ayon_core.pipeline import ( load, @@ -44,7 +45,14 @@ def get_image_avalon_container(): class ImageLoader(load.LoaderPlugin): """Load images into COP2""" - product_types = {"imagesequence"} + product_types = { + "imagesequence", + "review", + "render", + "plate", + "image", + "online", + } label = "Load Image (COP2)" representations = ["*"] order = -10 @@ -55,10 +63,8 @@ class ImageLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes - file_path = self.filepath_from_context(context) - file_path = os.path.normpath(file_path) - file_path = file_path.replace("\\", "/") - file_path = self._get_file_sequence(file_path) + path = self.filepath_from_context(context) + path = self.format_path(path, representation=context["representation"]) # Get the root node parent = get_image_avalon_container() @@ -70,7 +76,10 @@ class ImageLoader(load.LoaderPlugin): node = parent.createNode("file", node_name=node_name) node.moveToGoodPosition() - node.setParms({"filename1": file_path}) + parms = {"filename1": path} + parms.update(self.get_colorspace_parms(context["representation"])) + + node.setParms(parms) # Imprint it manually data = { @@ -93,16 +102,17 @@ class ImageLoader(load.LoaderPlugin): # Update the file path file_path = get_representation_path(repre_entity) - file_path = file_path.replace("\\", "/") - file_path = self._get_file_sequence(file_path) + file_path = self.format_path(file_path, repre_entity) + + parms = { + "filename1": file_path, + "representation": repre_entity["id"], + } + + parms.update(self.get_colorspace_parms(repre_entity)) # Update attributes - node.setParms( - { - "filename1": file_path, - "representation": repre_entity["id"], - } - ) + node.setParms(parms) def remove(self, container): @@ -119,14 +129,54 @@ class ImageLoader(load.LoaderPlugin): if not parent.children(): parent.destroy() - def _get_file_sequence(self, file_path): - root = os.path.dirname(file_path) - files = sorted(os.listdir(root)) + @staticmethod + def format_path(path, representation): + """Format file path correctly for single image or sequence.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) - first_fname = files[0] - prefix, padding, suffix = first_fname.rsplit(".", 2) - fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) - return os.path.join(root, fname).replace("\\", "/") + ext = os.path.splitext(path)[-1] - def switch(self, container, context): - self.update(container, context) + is_sequence = bool(representation["context"].get("frame")) + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path + else: + filename = re.sub(r"(.*)\.(\d+){}$".format(re.escape(ext)), + "\\1.$F4{}".format(ext), + path) + + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename + + def get_colorspace_parms(self, representation: dict) -> dict: + """Return the color space parameters. + + Returns the values for the colorspace parameters on the node if there + is colorspace data on the representation. + + Arguments: + representation (dict): The representation entity. + + Returns: + dict: Parm to value mapping if colorspace data is defined. + + """ + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return {} + + colorspace = data["colorspace"] + if colorspace: + return { + "colorspace": 3, # Use OpenColorIO + "ocio_space": colorspace + } + + def switch(self, container, representation): + self.update(container, representation) From 44ed77a9f655c03ed585596b08eafc2e6dd2a402 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:20:14 +0100 Subject: [PATCH 109/284] Houdini: Implement USD loader to SOPs via `usdimport` --- .../houdini/plugins/load/load_usd_sop.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py new file mode 100644 index 0000000000..b32210e399 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py @@ -0,0 +1,91 @@ +import os + +from ayon_core.pipeline import load +from ayon_core.hosts.houdini.api import pipeline + + +class SopUsdImportLoader(load.LoaderPlugin): + """Load USD to SOPs via `usdimport`""" + + label = "Load USD to SOPs" + product_types = {"*"} + representations = ["usd"] + order = -6 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + import hou + + # Format file name, Houdini only wants forward slashes + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["folder"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create a new geo node + container = obj.createNode("geo", node_name=node_name) + + # Remove the file node, it only loads static meshes + # Houdini 17 has removed the file node from the geo node + file_node = container.node("file1") + if file_node: + file_node.destroy() + + # Create a usdimport node + usdimport = container.createNode("usdimport", node_name=node_name) + usdimport.setParms({"filepath1": file_path}) + + # Ensure display flag is on the first input node and not on the OUT + # node to optimize "debug" displaying in the viewport. + usdimport.setDisplayFlag(True) + + # Set new position for unpack node else it gets cluttered + nodes = [container, usdimport] + for nr, node in enumerate(nodes): + node.setPosition([0, (0 - nr)]) + + self[:] = nodes + + return pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + def update(self, container, context): + + node = container["node"] + try: + usdimport_node = next( + n for n in node.children() if n.type().name() == "usdimport" + ) + except StopIteration: + self.log.error("Could not find node of type `usdimport`") + return + + # Update the file path + file_path = self.filepath_from_context(context) + file_path = file_path.replace("\\", "/") + + usdimport_node.setParms({"filepath1": file_path}) + + # Update attribute + node.setParms({"representation": context["representation"]["id"]}) + + def remove(self, container): + + node = container["node"] + node.destroy() + + def switch(self, container, representation): + self.update(container, representation) From 24d87bf92a8be56fe837013860c2bb944f7520f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:22:27 +0100 Subject: [PATCH 110/284] Remove redundant logic --- .../hosts/houdini/plugins/load/load_usd_sop.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py index b32210e399..3607987e15 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py @@ -32,24 +32,12 @@ class SopUsdImportLoader(load.LoaderPlugin): # Create a new geo node container = obj.createNode("geo", node_name=node_name) - # Remove the file node, it only loads static meshes - # Houdini 17 has removed the file node from the geo node - file_node = container.node("file1") - if file_node: - file_node.destroy() - # Create a usdimport node usdimport = container.createNode("usdimport", node_name=node_name) usdimport.setParms({"filepath1": file_path}) - # Ensure display flag is on the first input node and not on the OUT - # node to optimize "debug" displaying in the viewport. - usdimport.setDisplayFlag(True) - # Set new position for unpack node else it gets cluttered nodes = [container, usdimport] - for nr, node in enumerate(nodes): - node.setPosition([0, (0 - nr)]) self[:] = nodes From 135dd15870feb230231ce888949f5a4c56ce8749 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:26:18 +0100 Subject: [PATCH 111/284] Remove redundant assignment to `self[:]` --- client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py index 3607987e15..5b7e022e73 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py @@ -39,8 +39,6 @@ class SopUsdImportLoader(load.LoaderPlugin): # Set new position for unpack node else it gets cluttered nodes = [container, usdimport] - self[:] = nodes - return pipeline.containerise( node_name, namespace, From c18103710db402d0923e28b41fdea9d66130583a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:28:07 +0100 Subject: [PATCH 112/284] Remove legacy collect instances --- .../plugins/publish/collect_instances.py | 100 ------------------ 1 file changed, 100 deletions(-) delete mode 100644 client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py deleted file mode 100644 index 63537811cd..0000000000 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py +++ /dev/null @@ -1,100 +0,0 @@ -import hou - -import pyblish.api - -from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID -from ayon_core.hosts.houdini.api import lib - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by all node in out graph and pre-defined attributes - - This collector takes into account folders that are associated with - an specific node and marked with a unique identifier; - - Identifier: - id (str): "ayon.create.instance" - - Specific node: - The specific node is important because it dictates in which way the - product is being exported. - - alembic: will export Alembic file which supports cascading attributes - like 'cbId' and 'path' - geometry: Can export a wide range of file types, default out - - """ - - order = pyblish.api.CollectorOrder - 0.01 - label = "Collect Instances" - hosts = ["houdini"] - - def process(self, context): - - nodes = hou.node("/out").children() - nodes += hou.node("/obj").children() - - # Include instances in USD stage only when it exists so it - # remains backwards compatible with version before houdini 18 - stage = hou.node("/stage") - if stage: - nodes += stage.recursiveGlob("*", filter=hou.nodeTypeFilter.Rop) - - for node in nodes: - - if not node.parm("id"): - continue - - if node.evalParm("id") not in { - AYON_INSTANCE_ID, AVALON_INSTANCE_ID - }: - continue - - # instance was created by new creator code, skip it as - # it is already collected. - if node.parm("creator_identifier"): - continue - - has_family = node.evalParm("family") - assert has_family, "'%s' is missing 'family'" % node.name() - - self.log.info( - "Processing legacy instance node {}".format(node.path()) - ) - - data = lib.read(node) - # Check bypass state and reverse - if hasattr(node, "isBypassed"): - data.update({"active": not node.isBypassed()}) - - # temporarily translation of `active` to `publish` till issue has - # been resolved. - # https://github.com/pyblish/pyblish-base/issues/307 - if "active" in data: - data["publish"] = data["active"] - - # Create nice name if the instance has a frame range. - label = data.get("name", node.name()) - label += " (%s)" % data["folderPath"] # include folder in name - - instance = context.create_instance(label) - - # Include `families` using `family` data - product_type = data["family"] - data["productType"] = product_type - instance.data["families"] = [product_type] - - instance[:] = [node] - instance.data["instance_node"] = node.path() - instance.data.update(data) - - def sort_by_family(instance): - """Sort by family""" - return instance.data.get( - "families", instance.data.get("productType") - ) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=sort_by_family) - - return context From b8d1377904d4f830145b53dc8ad4d8ef1b3a1c3a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 14:35:16 +0100 Subject: [PATCH 113/284] Do not consider hero versions outdated ever --- client/ayon_core/tools/sceneinventory/model.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 5f8b1ee81c..befd0f5ab5 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -322,15 +322,15 @@ class InventoryModel(TreeModel): group_node["representation"] = repre_id group_node["version"] = version_entity["version"] - # We check against `abs(version)` because we allow a hero version - # which is represented by a negative number to also count as - # latest version - # If a hero version for whatever reason does not match the latest - # positive version number, we also consider it outdated - group_node["isOutdated"] = ( - abs(version_entity["version"]) != - highest_version_by_product_id.get(version_entity["productId"]) - ) + # Check if the version is outdated. If the version is below 0 it + # is a hero version and will never be considered outdated + is_outdated = False + if version_entity["version"] >= 0: + last_version = highest_version_by_product_id.get( + version_entity["productId"]) + if last_version is not None: + is_outdated = version_entity["version"] != last_version + group_node["isOutdated"] = is_outdated group_node["productType"] = product_type or "" group_node["productTypeIcon"] = product_type_icon From ebebc8fc9eeb3c9aa74720a5d56dc1edf3a522fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 14:36:33 +0100 Subject: [PATCH 114/284] fix hero version integration --- client/ayon_core/plugins/publish/integrate_hero_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index c352e67f89..bbc20927c0 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -353,7 +353,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): repre_entity["context"] = repre_context repre_entity["attrib"] = { "path": str(template_filled), - "template": hero_template + "template": hero_template.template } dst_paths = [] From 092d6fcf46a2658d8472b149d75450a27f573259 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 14:36:43 +0100 Subject: [PATCH 115/284] removed unused '_default_template_name' variable --- client/ayon_core/plugins/publish/integrate_hero_version.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index bbc20927c0..6a10bbd891 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -84,8 +84,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # permissions error on files (files were used or user didn't have perms) # *but all other plugins must be sucessfully completed - _default_template_name = "hero" - def process(self, instance): self.log.debug( "--- Integration of Hero version for product `{}` begins.".format( From 7682137190ef015f8a9dd5581e02faece9dedc85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 14:36:55 +0100 Subject: [PATCH 116/284] make hero integrator optional --- .../ayon_core/plugins/publish/integrate_hero_version.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 6a10bbd891..7969457697 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -13,7 +13,10 @@ from ayon_api.operations import ( from ayon_api.utils import create_entity_id from ayon_core.lib import create_hard_link, source_hash -from ayon_core.pipeline.publish import get_publish_template_name +from ayon_core.pipeline.publish import ( + get_publish_template_name, + OptionalPyblishPluginMixin, +) def prepare_changes(old_entity, new_entity): @@ -46,7 +49,9 @@ def prepare_changes(old_entity, new_entity): return changes -class IntegrateHeroVersion(pyblish.api.InstancePlugin): +class IntegrateHeroVersion( + OptionalPyblishPluginMixin, pyblish.api.InstancePlugin +): label = "Integrate Hero Version" # Must happen after IntegrateNew order = pyblish.api.IntegratorOrder + 0.1 From ccb00be0b3428a25d91a88956136cc6f7756af2a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:11:30 +0100 Subject: [PATCH 117/284] Update client/ayon_core/tools/sceneinventory/model.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/sceneinventory/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index befd0f5ab5..14379664c6 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -295,7 +295,7 @@ class InventoryModel(TreeModel): product_ids={ group["version"]["productId"] for group in grouped.values() }, - fields=["productId", "version"] + fields={"productId", "version"} ) # Map value to `version` key highest_version_by_product_id = { From 5d9fe5d86f82556ee9aa7e2be01a4fb732590cb4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:12:52 +0100 Subject: [PATCH 118/284] Use `HeroVersionType` instead of negative version number check --- client/ayon_core/tools/sceneinventory/model.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index befd0f5ab5..c7f20a974d 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -8,7 +8,10 @@ import ayon_api from qtpy import QtCore, QtGui import qtawesome -from ayon_core.pipeline import get_current_project_name +from ayon_core.pipeline import ( + get_current_project_name, + HeroVersionType, +) from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import get_qt_icon from ayon_core.tools.utils.models import TreeModel, Item @@ -320,12 +323,17 @@ class InventoryModel(TreeModel): repre_entity["name"] ) group_node["representation"] = repre_id - group_node["version"] = version_entity["version"] - # Check if the version is outdated. If the version is below 0 it - # is a hero version and will never be considered outdated + # Detect hero version type + version = version_entity["version"] + if version < 0: + version = HeroVersionType(version) + group_node["version"] = version + + # Check if the version is outdated. + # Hero versions are never considered to be outdated. is_outdated = False - if version_entity["version"] >= 0: + if not isinstance(version, HeroVersionType): last_version = highest_version_by_product_id.get( version_entity["productId"]) if last_version is not None: From 910c79f886274d66e616229ad88afd90d3f5aa90 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:18:28 +0100 Subject: [PATCH 119/284] Remove redundant logic --- client/ayon_core/tools/sceneinventory/model.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index df251ef69e..8de74b978a 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -519,14 +519,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if version is None: return True - # If either a version or highest is present but not the other - # consider the item invalid. - if not self._hierarchy_view: - # Skip this check if in hierarchy view, or the child item - # node will be hidden even it's actually outdated. - if version is None: - return False - return node.get("isOutdated", True) index = self.sourceModel().index(row, self.filterKeyColumn(), parent) From e735c5dc723bc127eba840a03569e7e0c3db05e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:20:12 +0100 Subject: [PATCH 120/284] Simplify further --- client/ayon_core/tools/sceneinventory/model.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 8de74b978a..dd5f369fed 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -513,12 +513,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): """ def outdated(node): - version = node.get("version", None) - - # Always allow indices that have no version data at all - if version is None: - return True - return node.get("isOutdated", True) index = self.sourceModel().index(row, self.filterKeyColumn(), parent) From 54db9178a3d8ad33630eda59aa9e5a5692f88a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 25 Mar 2024 15:20:51 +0100 Subject: [PATCH 121/284] :recycle: create manage script --- poetry.toml | 1 + scripts/setup_env.ps1 | 67 ---------- tools/manage.ps1 | 283 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 67 deletions(-) delete mode 100644 scripts/setup_env.ps1 create mode 100644 tools/manage.ps1 diff --git a/poetry.toml b/poetry.toml index ab1033bd37..62e2dff2a2 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,2 +1,3 @@ [virtualenvs] in-project = true +create = true diff --git a/scripts/setup_env.ps1 b/scripts/setup_env.ps1 deleted file mode 100644 index 82ff515bc6..0000000000 --- a/scripts/setup_env.ps1 +++ /dev/null @@ -1,67 +0,0 @@ -$current_dir = Get-Location -$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -$repo_root = (Get-Item $script_dir).parent.FullName -& git submodule update --init --recursive - - -function Exit-WithCode($exitcode) { - # Only exit this host process if it's a child of another PowerShell parent process... - $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId - $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name - if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) } - - exit $exitcode - } - - -function Install-Poetry() { - Write-Host ">>> Installing Poetry ... " - $python = "python" - if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { - if (-not (Test-Path -PathType Leaf -Path "$($repo_root)\.python-version")) { - $result = & pyenv global - if ($result -eq "no global version configured") { - Write-Host "!!! Using pyenv but having no local or global version of Python set." -Color Red, Yellow - Exit-WithCode 1 - } - } - $python = & pyenv which python - - } - - $env:POETRY_HOME="$repo_root\.poetry" - (Invoke-WebRequest -Uri https://install.python-poetry.org/ -UseBasicParsing).Content | & $($python) - -} - -Write-Host ">>> Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { - Write-Host "NOT FOUND" - Install-Poetry - Write-Host "INSTALLED" -} else { - Write-Host "OK" -} - -if (-not (Test-Path -PathType Leaf -Path "$($repo_root)\poetry.lock")) { - Write-Host ">>> Installing virtual environment and creating lock." -} else { - Write-Host ">>> Installing virtual environment from lock." -} -$startTime = [int][double]::Parse((Get-Date -UFormat %s)) -& "$env:POETRY_HOME\bin\poetry" install --no-root $poetry_verbosity --ansi -if ($LASTEXITCODE -ne 0) { - Write-Host "!!! ", "Poetry command failed." - Set-Location -Path $current_dir - Exit-WithCode 1 -} -Write-Host ">>> Installing pre-commit hooks ..." -& "$env:POETRY_HOME\bin\poetry" run pre-commit install -if ($LASTEXITCODE -ne 0) { - Write-Host "!!! Installation of pre-commit hooks failed." - Set-Location -Path $current_dir - Exit-WithCode 1 -} - -$endTime = [int][double]::Parse((Get-Date -UFormat %s)) -Set-Location -Path $current_dir -Write-Host ">>> Done in $( $endTime - $startTime ) secs." diff --git a/tools/manage.ps1 b/tools/manage.ps1 new file mode 100644 index 0000000000..b2187cbaa2 --- /dev/null +++ b/tools/manage.ps1 @@ -0,0 +1,283 @@ +<# +.SYNOPSIS + Helper script to run various tasks on ayon-core addon repository. + +.DESCRIPTION + This script will detect Python installation, and build OpenPype to `build` + directory using existing virtual environment created by Poetry (or + by running `/tools/create_venv.ps1`). It will then shuffle dependencies in + build folder to optimize for different Python versions (2/3) in Python host. + +.EXAMPLE + +PS> .\tools\manage.ps1 + +.EXAMPLE + +To create virtual environment using Poetry: +PS> .\tools\manage.ps1 create-env + +.EXAMPLE + +To run Ruff check: +PS> .\tools\manage.ps1 ruff-check + +.LINK +https://github.com/ynput/ayon-core + +#> + +# Settings and gitmodule init +$CurrentDir = Get-Location +$ScriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$RepoRoot = (Get-Item $ScriptDir).parent.FullName +& git submodule update --init --recursive +$env:PSModulePath = $env:PSModulePath + ";$($openpype_root)\tools\modules\powershell" + +$FunctionName=$ARGS[0] +$Arguments=@() +if ($ARGS.Length -gt 1) { + $Arguments = $ARGS[1..($ARGS.Length - 1)] +} + +function Exit-WithCode($exitcode) { + # Only exit this host process if it's a child of another PowerShell parent process... + $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId + $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name + if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) } + + exit $exitcode +} + +function Test-CommandExists { + param ( + [Parameter(Mandatory=$true)] + [string]$command + ) + + $commandExists = $null -ne (Get-Command $command -ErrorAction SilentlyContinue) + return $commandExists +} + +function Write-Info { + <# + .SYNOPSIS + Write-Info function to write information messages. + + It uses Write-Color if that is available, otherwise falls back to Write-Host. + + #> + [CmdletBinding()] + param ( + [alias ('T')] [String[]]$Text, + [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, + [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, + [alias ('Indent')][int] $StartTab = 0, + [int] $LinesBefore = 0, + [int] $LinesAfter = 0, + [int] $StartSpaces = 0, + [alias ('L')] [string] $LogFile = '', + [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', + [alias ('LogTimeStamp')][bool] $LogTime = $true, + [int] $LogRetry = 2, + [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', + [switch] $ShowTime, + [switch] $NoNewLine + ) + if (Test-CommandExists "Write-Color") { + Write-Color -Text $Text -Color $Color -BackGroundColor $BackGroundColor -StartTab $StartTab -LinesBefore $LinesBefore -LinesAfter $LinesAfter -StartSpaces $StartSpaces -LogFile $LogFile -DateTimeFormat $DateTimeFormat -LogTime $LogTime -LogRetry $LogRetry -Encoding $Encoding -ShowTime $ShowTime -NoNewLine $NoNewLine + } else { + $message = $Text -join ' ' + if ($NoNewLine) + { + Write-Host $message -NoNewline + } + else + { + Write-Host $message + } + } +} + +$art = @" + + ▄██▄ + ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ + ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ + ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ + ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ + ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ + + · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · + +"@ + +function Write-AsciiArt() { + Write-Host $art -ForegroundColor DarkGreen +} + +function Show-PSWarning() { + if ($PSVersionTable.PSVersion.Major -lt 7) { + Write-Info -Text "!!! ", "You are using old version of PowerShell - ", "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)" -Color Red, Yellow, White + Write-Info -Text " Please update to at least 7.0 - ", "https://github.com/PowerShell/PowerShell/releases" -Color Yellow, White + Exit-WithCode 1 + } +} + +function Install-Poetry() { + Write-Info -Text ">>> ", "Installing Poetry ... " -Color Green, Gray + $python = "python" + if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + if (-not (Test-Path -PathType Leaf -Path "$($RepoRoot)\.python-version")) { + $result = & pyenv global + if ($result -eq "no global version configured") { + Write-Info "!!! Using pyenv but having no local or global version of Python set." -Color Red, Yellow + Exit-WithCode 1 + } + } + $python = & pyenv which python + + } + + $env:POETRY_HOME="$RepoRoot\.poetry" + (Invoke-WebRequest -Uri https://install.python-poetry.org/ -UseBasicParsing).Content | & $($python) - +} + +function Set-Cwd() { + Set-Location -Path $RepoRoot +} + +function Restore-Cwd() { + $tmp_current_dir = Get-Location + if ("$tmp_current_dir" -ne "$CurrentDir") { + Write-Info -Text ">>> ", "Restoring current directory" -Color Green, Gray + Set-Location -Path $CurrentDir + } +} + +function Initialize-Environment { + Write-Info -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline + if (-not(Test-Path -PathType Container -Path "$( $env:POETRY_HOME )\bin")) + { + Write-Info -Text "NOT FOUND" -Color Yellow + Install-Poetry + Write-Info -Text "INSTALLED" -Color Cyan + } + else + { + Write-Info -Text "OK" -Color Green + } + + if (-not(Test-Path -PathType Leaf -Path "$( $repo_root )\poetry.lock")) + { + Write-Info -Text ">>> ", "Installing virtual environment and creating lock." -Color Green, Gray + } + else + { + Write-Info -Text ">>> ", "Installing virtual environment from lock." -Color Green, Gray + } + $startTime = [int][double]::Parse((Get-Date -UFormat %s)) + & "$env:POETRY_HOME\bin\poetry" config virtualenvs.in-project true --local + & "$env:POETRY_HOME\bin\poetry" config virtualenvs.create true --local + & "$env:POETRY_HOME\bin\poetry" install --no-root $poetry_verbosity --ansi + if ($LASTEXITCODE -ne 0) + { + Write-Info -Text "!!! ", "Poetry command failed." -Color Red, Yellow + Restore-Cwd + Exit-WithCode 1 + } + if (Test-Path -PathType Container -Path "$( $repo_root )\.git") + { + Write-Info -Text ">>> ", "Installing pre-commit hooks ..." -Color Green, White + & "$env:POETRY_HOME\bin\poetry" run pre-commit install + if ($LASTEXITCODE -ne 0) + { + Write-Info -Text "!!! ", "Installation of pre-commit hooks failed." -Color Red, Yellow + } + } + $endTime = [int][double]::Parse((Get-Date -UFormat %s)) + Restore-Cwd + try + { + if (Test-CommandExists "New-BurntToastNotification") + { + $app_logo = "$repo_root\tools\icons\ayon.ico" + New-BurntToastNotification -AppLogo "$app_logo" -Text "AYON", "Virtual environment created.", "All done in $( $endTime - $startTime ) secs." + } + } + catch {} + Write-Info -Text ">>> ", "Virtual environment created." -Color Green, White +} + +function Invoke-Ruff { + param ( + [switch] $Fix + ) + $Poetry = "$RepoRoot\.poetry\bin\poetry.exe" + $RuffArgs = @( "run", "ruff", "check" ) + if ($Fix) { + $RuffArgs += "--fix" + } + & $Poetry $RuffArgs +} + +function Invoke-Codespell { + param ( + [switch] $Fix + ) + $Poetry = "$RepoRoot\.poetry\bin\poetry.exe" + $CodespellArgs = @( "run", "codespell" ) + if ($Fix) { + $CodespellArgs += "--fix" + } + & $Poetry $CodespellArgs +} + +function Write-Help { + <# + .SYNOPSIS + Write-Help function to write help messages. + #> + Write-Host "" + Write-Host "AYON Addon management script" + Write-Host "" + Write-Info -Text "Usage: ", "./manage.ps1 ", "[command]" -Color Gray, White, Cyan + Write-Host "" + Write-Host "Runtime targets:" + Write-Info -Text " create-env ", "Install Poetry and update venv by lock file" -Color White, Cyan + Write-Info -Text " ruff-check ", "Run Ruff check for the repository" -Color White, Cyan + Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan + Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan + Write-Host "" +} + +function Resolve-Function { + if ($null -eq $FunctionName) { + Write-Help + return + } + $FunctionName = $FunctionName.ToLower() -replace "\W" + if ($FunctionName -eq "createenv") { + Set-Cwd + Initialize-Environment + } elseif ($FunctionName -eq "ruffcheck") { + Set-Cwd + Invoke-Ruff + } elseif ($FunctionName -eq "rufffix") { + Set-Cwd + Invoke-Ruff -Fix + } elseif ($FunctionName -eq "codespell") { + Set-Cwd + Invoke-CodeSpell + } else { + Write-Host "Unknown function ""$FunctionName""" + Write-Help + } +} + +# ----------------------------------------------------- + +Show-PSWarning +Write-AsciiArt + +Resolve-Function From 11f5d9d5717aab775f05149bbb1c1ea99d32b48e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:24:57 +0100 Subject: [PATCH 122/284] Fix docstring --- client/ayon_core/tools/sceneinventory/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index dd5f369fed..330b174218 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -508,8 +508,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): def _is_outdated(self, row, parent): """Return whether row is outdated. - A row is considered outdated if it has no "version" or the "isOutdated" - value is True. + A row is considered outdated if `isOutdated` data is true or not set. """ def outdated(node): From 44641bc63071bca836bf1079254028df5326dddd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 15:37:22 +0100 Subject: [PATCH 123/284] Fix displaying HeroVersionType in version delegate --- client/ayon_core/tools/utils/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index 8bcfe8b985..4b7ca5425e 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -123,6 +123,7 @@ def paint_image_with_color(image, color): def format_version(value): """Formats integer to displayable version name""" + value = int(value) # convert e.g. HeroVersionType to its version value label = "v{0:03d}".format(abs(value)) if value < 0: return "[{}]".format(label) From a046c0145649c0b21cbf0bb057529bd88c3e073d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 25 Mar 2024 15:42:02 +0100 Subject: [PATCH 124/284] :pencil2: fix text --- tools/manage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/manage.ps1 b/tools/manage.ps1 index b2187cbaa2..23c52d57be 100644 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -243,7 +243,7 @@ function Write-Help { Write-Host "" Write-Info -Text "Usage: ", "./manage.ps1 ", "[command]" -Color Gray, White, Cyan Write-Host "" - Write-Host "Runtime targets:" + Write-Host "Commands:" Write-Info -Text " create-env ", "Install Poetry and update venv by lock file" -Color White, Cyan Write-Info -Text " ruff-check ", "Run Ruff check for the repository" -Color White, Cyan Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan From 26b411b2d1f2d06d902cc4efb36428e2343f4b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 25 Mar 2024 15:42:24 +0100 Subject: [PATCH 125/284] :recycle: add linux script --- scripts/setup_env.sh => tools/manage.sh | 122 +++++++++++++++++++++--- 1 file changed, 110 insertions(+), 12 deletions(-) rename scripts/setup_env.sh => tools/manage.sh (55%) diff --git a/scripts/setup_env.sh b/tools/manage.sh similarity index 55% rename from scripts/setup_env.sh rename to tools/manage.sh index 16298cb8bc..f40df80790 100644 --- a/scripts/setup_env.sh +++ b/tools/manage.sh @@ -83,16 +83,19 @@ realpath () { echo $(cd $(dirname "$1"); pwd)/$(basename "$1") } -main () { - detect_python || return 1 - +############################################################################## +# Create Virtual Environment +# Globals: +# repo_root +# POETRY_HOME +# poetry_verbosity +# Arguments: +# Path to resolve +# Returns: +# None +############################################################################### +create_env () { # Directories - repo_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) - - if [[ -z $POETRY_HOME ]]; then - export POETRY_HOME="$repo_root/.poetry" - fi - pushd "$repo_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" @@ -115,10 +118,105 @@ main () { return 1 fi - echo -e "${BIGreen}>>>${RST} Installing pre-commit hooks ..." - "$POETRY_HOME/bin/poetry" run pre-commit install + echo -e "${BIGreen}>>>${RST} Cleaning cache files ..." + clean_pyc + + "$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip + + if [ -d "$repo_root/.git" ]; then + echo -e "${BIGreen}>>>${RST} Installing pre-commit hooks ..." + "$POETRY_HOME/bin/poetry" run pre-commit install + fi +} + +print_art() { + echo -e "${BGreen}" + cat <<-EOF + + ▄██▄ + ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ + ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ + ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ + ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ + ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ + + · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · + +EOF + echo -e "${RST}" +} + +default_help() { + print_art + echo -e "${BWhite}AYON Addon management script${RST}" + echo "" + echo -e "Usage: ${BWhite}./manage.sh${RST} ${BICyan}[command]${RST}" + echo "" + echo "${BWhite}Commands:${RST}" + echo " ${BWhite}create-env${RST} ${BCyan}Install Poetry and update venv by lock file${RST}" + echo " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" + echo " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" + echo " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" + echo "" +} + +run_ruff () { + echo -e "${BIGreen}>>>${RST} Running Ruff check ..." + "$POETRY_HOME/bin/poetry" run ruff +} + +run_ruff_check () { + echo -e "${BIGreen}>>>${RST} Running Ruff fix ..." + "$POETRY_HOME/bin/poetry" run ruff --fix +} + +run_codespell () { + echo -e "${BIGreen}>>>${RST} Running codespell check ..." + "$POETRY_HOME/bin/poetry" run codespell +} + +main () { + detect_python || return 1 + + # Directories + repo_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + if [[ -z $POETRY_HOME ]]; then + export POETRY_HOME="$repo_root/.poetry" + fi + + pushd "$repo_root" > /dev/null || return > /dev/null + + # Use first argument, lower and keep only characters + function_name="$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z]*//g')" + + case $function_name in + "createenv") + create_env || return_code=$? + exit $return_code + ;; + "ruffcheck") + run_ruff || return_code=$? + exit $return_code + ;; + "rufffix") + run_ruff_check || return_code=$? + exit $return_code + ;; + "codespell") + run_codespell || return_code=$? + exit $return_code + ;; + esac + + if [ "$function_name" != "" ]; then + echo -e "${BIRed}!!!${RST} Unknown function name: $function_name" + fi + + default_help + exit $return_code } return_code=0 -main || return_code=$? +main "$@" || return_code=$? exit $return_code From 1f275ecf4145dbd7b29ff78c85f25f8a4d1ffd7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:44:04 +0100 Subject: [PATCH 126/284] removed mindbender from harmony readme --- client/ayon_core/hosts/harmony/api/README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/harmony/api/README.md b/client/ayon_core/hosts/harmony/api/README.md index 6d1e400476..7ac185638a 100644 --- a/client/ayon_core/hosts/harmony/api/README.md +++ b/client/ayon_core/hosts/harmony/api/README.md @@ -204,7 +204,7 @@ class CreateComposite(harmony.Creator): name = "compositeDefault" label = "Composite" - product_type = "mindbender.template" + product_type = "template" def __init__(self, *args, **kwargs): super(CreateComposite, self).__init__(*args, **kwargs) @@ -221,7 +221,7 @@ class CreateRender(harmony.Creator): name = "writeDefault" label = "Write" - product_type = "mindbender.imagesequence" + product_type = "render" node_type = "WRITE" def __init__(self, *args, **kwargs): @@ -304,7 +304,7 @@ class ExtractImage(pyblish.api.InstancePlugin): label = "Extract Image Sequence" order = pyblish.api.ExtractorOrder hosts = ["harmony"] - families = ["mindbender.imagesequence"] + families = ["render"] def process(self, instance): project_path = harmony.send( @@ -582,8 +582,16 @@ class ImageSequenceLoader(load.LoaderPlugin): """Load images Stores the imported asset in a container named after the asset. """ - product_types = {"mindbender.imagesequence"} + product_types = { + "shot", + "render", + "image", + "plate", + "reference", + "review", + } representations = ["*"] + extensions = {"jpeg", "png", "jpg"} def load(self, context, name=None, namespace=None, data=None): files = [] From 2b1e31001c74a6dd263236e0f7a5c0600c372249 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:52:11 +0100 Subject: [PATCH 127/284] duplicated non python host launch script for photoshop --- .../hosts/photoshop/api/launch_script.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 client/ayon_core/hosts/photoshop/api/launch_script.py diff --git a/client/ayon_core/hosts/photoshop/api/launch_script.py b/client/ayon_core/hosts/photoshop/api/launch_script.py new file mode 100644 index 0000000000..c036b63c46 --- /dev/null +++ b/client/ayon_core/hosts/photoshop/api/launch_script.py @@ -0,0 +1,93 @@ +"""Script wraps launch mechanism of Photoshop implementations. + +Arguments passed to the script are passed to launch function in host +implementation. In all cases requires host app executable and may contain +workfile or others. +""" + +import os +import sys + +from ayon_core.hosts.photoshop.api.lib import main + +# Get current file to locate start point of sys.argv +CURRENT_FILE = os.path.abspath(__file__) + + +def show_error_messagebox(title, message, detail_message=None): + """Function will show message and process ends after closing it.""" + from qtpy import QtWidgets, QtCore + from ayon_core import style + + app = QtWidgets.QApplication([]) + app.setStyleSheet(style.load_stylesheet()) + + msgbox = QtWidgets.QMessageBox() + msgbox.setWindowTitle(title) + msgbox.setText(message) + + if detail_message: + msgbox.setDetailedText(detail_message) + + msgbox.setWindowModality(QtCore.Qt.ApplicationModal) + msgbox.show() + + sys.exit(app.exec_()) + + +def on_invalid_args(script_not_found): + """Show to user message box saying that something went wrong. + + Tell user that arguments to launch implementation are invalid with + arguments details. + + Args: + script_not_found (bool): Use different message based on this value. + """ + + title = "Invalid arguments" + joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) + if script_not_found: + submsg = "Where couldn't find script path:\n\"{}\"" + else: + submsg = "Expected Host executable after script path:\n\"{}\"" + + message = "BUG: Got invalid arguments so can't launch Host application." + detail_message = "Process was launched with arguments:\n{}\n\n{}".format( + joined_args, + submsg.format(CURRENT_FILE) + ) + + show_error_messagebox(title, message, detail_message) + + +def main(argv): + # Modify current file path to find match in sys.argv which may be different + # on windows (different letter cases and slashes). + modified_current_file = CURRENT_FILE.replace("\\", "/").lower() + + # Create a copy of sys argv + sys_args = list(argv) + after_script_idx = None + # Find script path in sys.argv to know index of argv where host + # executable should be. + for idx, item in enumerate(sys_args): + if item.replace("\\", "/").lower() == modified_current_file: + after_script_idx = idx + 1 + break + + # Validate that there is at least one argument after script path + launch_args = None + if after_script_idx is not None: + launch_args = sys_args[after_script_idx:] + + if launch_args: + # Launch host implementation + main(*launch_args) + else: + # Show message box + on_invalid_args(after_script_idx is None) + + +if __name__ == "__main__": + main(sys.argv) From fee6a9b4485de958af128c9db0f7da8f4dbfbfa6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:52:52 +0100 Subject: [PATCH 128/284] renamed 'PHOTOSHOP_HOST_DIR' to 'PHOTOSHOP_ADDON_ROOT' --- client/ayon_core/hosts/photoshop/__init__.py | 4 ++-- client/ayon_core/hosts/photoshop/addon.py | 2 +- client/ayon_core/hosts/photoshop/api/pipeline.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/photoshop/__init__.py b/client/ayon_core/hosts/photoshop/__init__.py index 773f73d624..0c79afaab8 100644 --- a/client/ayon_core/hosts/photoshop/__init__.py +++ b/client/ayon_core/hosts/photoshop/__init__.py @@ -1,10 +1,10 @@ from .addon import ( + PHOTOSHOP_ADDON_ROOT, PhotoshopAddon, - PHOTOSHOP_HOST_DIR, ) __all__ = ( + "PHOTOSHOP_ADDON_ROOT", "PhotoshopAddon", - "PHOTOSHOP_HOST_DIR", ) diff --git a/client/ayon_core/hosts/photoshop/addon.py b/client/ayon_core/hosts/photoshop/addon.py index 3016912960..28833121c4 100644 --- a/client/ayon_core/hosts/photoshop/addon.py +++ b/client/ayon_core/hosts/photoshop/addon.py @@ -1,7 +1,7 @@ import os from ayon_core.addon import AYONAddon, IHostAddon -PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) +PHOTOSHOP_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class PhotoshopAddon(AYONAddon, IHostAddon): diff --git a/client/ayon_core/hosts/photoshop/api/pipeline.py b/client/ayon_core/hosts/photoshop/api/pipeline.py index 32f66cf7fb..27cfa5a7b5 100644 --- a/client/ayon_core/hosts/photoshop/api/pipeline.py +++ b/client/ayon_core/hosts/photoshop/api/pipeline.py @@ -21,14 +21,14 @@ from ayon_core.host import ( ) from ayon_core.pipeline.load import any_outdated_containers -from ayon_core.hosts.photoshop import PHOTOSHOP_HOST_DIR +from ayon_core.hosts.photoshop import PHOTOSHOP_ADDON_ROOT from ayon_core.tools.utils import get_ayon_qt_app from . import lib log = Logger.get_logger(__name__) -PLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(PHOTOSHOP_ADDON_ROOT, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") From aaea1e14859799dd8c443c832dce2ffa75be8e3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:53:17 +0100 Subject: [PATCH 129/284] photoshop has own prelaunch hook to modify launch arguments --- .../hooks/pre_non_python_host_launch.py | 2 +- client/ayon_core/hosts/photoshop/__init__.py | 2 + client/ayon_core/hosts/photoshop/addon.py | 14 +++ .../hosts/photoshop/hooks/pre_launch_args.py | 95 +++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py diff --git a/client/ayon_core/hooks/pre_non_python_host_launch.py b/client/ayon_core/hooks/pre_non_python_host_launch.py index fed4c99447..3d3c24b308 100644 --- a/client/ayon_core/hooks/pre_non_python_host_launch.py +++ b/client/ayon_core/hooks/pre_non_python_host_launch.py @@ -17,7 +17,7 @@ class NonPythonHostHook(PreLaunchHook): python script which launch the host. For these cases it is necessary to prepend python (or ayon) executable and script path before application's. """ - app_groups = {"harmony", "photoshop", "aftereffects"} + app_groups = {"harmony", "aftereffects"} order = 20 launch_types = {LaunchTypes.local} diff --git a/client/ayon_core/hosts/photoshop/__init__.py b/client/ayon_core/hosts/photoshop/__init__.py index 0c79afaab8..cf21b7df75 100644 --- a/client/ayon_core/hosts/photoshop/__init__.py +++ b/client/ayon_core/hosts/photoshop/__init__.py @@ -1,10 +1,12 @@ from .addon import ( PHOTOSHOP_ADDON_ROOT, PhotoshopAddon, + get_launch_script_path, ) __all__ = ( "PHOTOSHOP_ADDON_ROOT", "PhotoshopAddon", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/photoshop/addon.py b/client/ayon_core/hosts/photoshop/addon.py index 28833121c4..65fe6a7cd1 100644 --- a/client/ayon_core/hosts/photoshop/addon.py +++ b/client/ayon_core/hosts/photoshop/addon.py @@ -20,3 +20,17 @@ class PhotoshopAddon(AYONAddon, IHostAddon): def get_workfile_extensions(self): return [".psd", ".psb"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(PHOTOSHOP_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + PHOTOSHOP_ADDON_ROOT, "api", "launch_script.py" + ) + diff --git a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py new file mode 100644 index 0000000000..0689a6c2f0 --- /dev/null +++ b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py @@ -0,0 +1,95 @@ +import os +import platform +import subprocess + +from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.photoshop import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for Photoshop. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + executable_path = os.environ.get("AYON_EXECUTABLE") + + executable_filename = "" + if executable_path: + executable_filename = os.path.basename(executable_path) + + is_gui_executable = "ayon_console" not in executable_filename + if is_gui_executable: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + return kwargs + + +class PhotoshopPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to Photoshop implementation + before Photoshop executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"photoshop"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) From 0d75102d8a255237b5d8fa180e13d0febe250c31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:53:54 +0100 Subject: [PATCH 130/284] aftereffects have own launch script --- .../hosts/aftereffects/api/launch_script.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 client/ayon_core/hosts/aftereffects/api/launch_script.py diff --git a/client/ayon_core/hosts/aftereffects/api/launch_script.py b/client/ayon_core/hosts/aftereffects/api/launch_script.py new file mode 100644 index 0000000000..ad4e779bd0 --- /dev/null +++ b/client/ayon_core/hosts/aftereffects/api/launch_script.py @@ -0,0 +1,93 @@ +"""Script wraps launch mechanism of AfterEffects implementations. + +Arguments passed to the script are passed to launch function in host +implementation. In all cases requires host app executable and may contain +workfile or others. +""" + +import os +import sys + +from ayon_core.hosts.aftereffects.api.launch_logic import main + +# Get current file to locate start point of sys.argv +CURRENT_FILE = os.path.abspath(__file__) + + +def show_error_messagebox(title, message, detail_message=None): + """Function will show message and process ends after closing it.""" + from qtpy import QtWidgets, QtCore + from ayon_core import style + + app = QtWidgets.QApplication([]) + app.setStyleSheet(style.load_stylesheet()) + + msgbox = QtWidgets.QMessageBox() + msgbox.setWindowTitle(title) + msgbox.setText(message) + + if detail_message: + msgbox.setDetailedText(detail_message) + + msgbox.setWindowModality(QtCore.Qt.ApplicationModal) + msgbox.show() + + sys.exit(app.exec_()) + + +def on_invalid_args(script_not_found): + """Show to user message box saying that something went wrong. + + Tell user that arguments to launch implementation are invalid with + arguments details. + + Args: + script_not_found (bool): Use different message based on this value. + """ + + title = "Invalid arguments" + joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) + if script_not_found: + submsg = "Where couldn't find script path:\n\"{}\"" + else: + submsg = "Expected Host executable after script path:\n\"{}\"" + + message = "BUG: Got invalid arguments so can't launch Host application." + detail_message = "Process was launched with arguments:\n{}\n\n{}".format( + joined_args, + submsg.format(CURRENT_FILE) + ) + + show_error_messagebox(title, message, detail_message) + + +def main(argv): + # Modify current file path to find match in sys.argv which may be different + # on windows (different letter cases and slashes). + modified_current_file = CURRENT_FILE.replace("\\", "/").lower() + + # Create a copy of sys argv + sys_args = list(argv) + after_script_idx = None + # Find script path in sys.argv to know index of argv where host + # executable should be. + for idx, item in enumerate(sys_args): + if item.replace("\\", "/").lower() == modified_current_file: + after_script_idx = idx + 1 + break + + # Validate that there is at least one argument after script path + launch_args = None + if after_script_idx is not None: + launch_args = sys_args[after_script_idx:] + + if launch_args: + # Launch host implementation + main(*launch_args) + else: + # Show message box + on_invalid_args(after_script_idx is None) + + +if __name__ == "__main__": + main(sys.argv) From 720de823cf973dab3d75bede890cf49dc7fb7efc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 15:54:36 +0100 Subject: [PATCH 131/284] aftereffects has own prelaunch hook to modify launch arguments --- .../hooks/pre_non_python_host_launch.py | 2 +- .../ayon_core/hosts/aftereffects/__init__.py | 8 +- client/ayon_core/hosts/aftereffects/addon.py | 17 ++++ .../hosts/aftereffects/api/launch_logic.py | 1 - .../aftereffects/hooks/pre_launch_args.py | 95 +++++++++++++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py diff --git a/client/ayon_core/hooks/pre_non_python_host_launch.py b/client/ayon_core/hooks/pre_non_python_host_launch.py index 3d3c24b308..a65e0ecba4 100644 --- a/client/ayon_core/hooks/pre_non_python_host_launch.py +++ b/client/ayon_core/hooks/pre_non_python_host_launch.py @@ -17,7 +17,7 @@ class NonPythonHostHook(PreLaunchHook): python script which launch the host. For these cases it is necessary to prepend python (or ayon) executable and script path before application's. """ - app_groups = {"harmony", "aftereffects"} + app_groups = {"harmony"} order = 20 launch_types = {LaunchTypes.local} diff --git a/client/ayon_core/hosts/aftereffects/__init__.py b/client/ayon_core/hosts/aftereffects/__init__.py index ae750d05b6..02ab287629 100644 --- a/client/ayon_core/hosts/aftereffects/__init__.py +++ b/client/ayon_core/hosts/aftereffects/__init__.py @@ -1,6 +1,12 @@ -from .addon import AfterEffectsAddon +from .addon import ( + AFTEREFFECTS_ADDON_ROOT, + AfterEffectsAddon, + get_launch_script_path, +) __all__ = ( + "AFTEREFFECTS_ADDON_ROOT", "AfterEffectsAddon", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/aftereffects/addon.py b/client/ayon_core/hosts/aftereffects/addon.py index 46d0818247..fc54043c1d 100644 --- a/client/ayon_core/hosts/aftereffects/addon.py +++ b/client/ayon_core/hosts/aftereffects/addon.py @@ -1,5 +1,9 @@ +import os + from ayon_core.addon import AYONAddon, IHostAddon +AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) + class AfterEffectsAddon(AYONAddon, IHostAddon): name = "aftereffects" @@ -17,3 +21,16 @@ class AfterEffectsAddon(AYONAddon, IHostAddon): def get_workfile_extensions(self): return [".aep"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(AFTEREFFECTS_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + AFTEREFFECTS_ADDON_ROOT, "api", "launch_script.py" + ) diff --git a/client/ayon_core/hosts/aftereffects/api/launch_logic.py b/client/ayon_core/hosts/aftereffects/api/launch_logic.py index d0e4e8beae..5a23f2cb35 100644 --- a/client/ayon_core/hosts/aftereffects/api/launch_logic.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_logic.py @@ -7,7 +7,6 @@ import asyncio import functools import traceback - from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync diff --git a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py new file mode 100644 index 0000000000..2c5baa3b68 --- /dev/null +++ b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py @@ -0,0 +1,95 @@ +import os +import platform +import subprocess + +from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.aftereffects import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for AfterEffects. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + executable_path = os.environ.get("AYON_EXECUTABLE") + + executable_filename = "" + if executable_path: + executable_filename = os.path.basename(executable_path) + + is_in_ui_launcher = "ayon_console" not in executable_filename + if is_in_ui_launcher: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + return kwargs + + +class AEPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to AE implementation before + AE executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"aftereffects"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) From afd2e3518c36353f3ed206ffc4f9456c7b64ba03 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Mar 2024 16:20:41 +0100 Subject: [PATCH 132/284] :recycle: fix linux version --- .hound.yml | 6 +++--- tools/manage.ps1 | 0 tools/manage.sh | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) mode change 100644 => 100755 tools/manage.ps1 mode change 100644 => 100755 tools/manage.sh diff --git a/.hound.yml b/.hound.yml index df9cdab64a..de5adb3154 100644 --- a/.hound.yml +++ b/.hound.yml @@ -1,3 +1,3 @@ -flake8: - enabled: true - config_file: setup.cfg +flake8: + enabled: true + config_file: setup.cfg diff --git a/tools/manage.ps1 b/tools/manage.ps1 old mode 100644 new mode 100755 diff --git a/tools/manage.sh b/tools/manage.sh old mode 100644 new mode 100755 index f40df80790..923953bf96 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -152,22 +152,22 @@ default_help() { echo "" echo -e "Usage: ${BWhite}./manage.sh${RST} ${BICyan}[command]${RST}" echo "" - echo "${BWhite}Commands:${RST}" - echo " ${BWhite}create-env${RST} ${BCyan}Install Poetry and update venv by lock file${RST}" - echo " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" - echo " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" - echo " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" + echo -e "${BWhite}Commands:${RST}" + echo -e " ${BWhite}create-env${RST} ${BCyan}Install Poetry and update venv by lock file${RST}" + echo -e " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" + echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" + echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" echo "" } run_ruff () { echo -e "${BIGreen}>>>${RST} Running Ruff check ..." - "$POETRY_HOME/bin/poetry" run ruff + "$POETRY_HOME/bin/poetry" run ruff check } run_ruff_check () { echo -e "${BIGreen}>>>${RST} Running Ruff fix ..." - "$POETRY_HOME/bin/poetry" run ruff --fix + "$POETRY_HOME/bin/poetry" run ruff check --fix } run_codespell () { From 96aac985c2545b5e8e5bf74a96e448a3b667463c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:23:23 +0100 Subject: [PATCH 133/284] copied non python launch script to harmony --- .../hosts/harmony/api/launch_script.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 client/ayon_core/hosts/harmony/api/launch_script.py diff --git a/client/ayon_core/hosts/harmony/api/launch_script.py b/client/ayon_core/hosts/harmony/api/launch_script.py new file mode 100644 index 0000000000..1f2c36b7e6 --- /dev/null +++ b/client/ayon_core/hosts/harmony/api/launch_script.py @@ -0,0 +1,93 @@ +"""Script wraps launch mechanism of Harmony implementations. + +Arguments passed to the script are passed to launch function in host +implementation. In all cases requires host app executable and may contain +workfile or others. +""" + +import os +import sys + +from ayon_core.hosts.harmony.api.lib import main + +# Get current file to locate start point of sys.argv +CURRENT_FILE = os.path.abspath(__file__) + + +def show_error_messagebox(title, message, detail_message=None): + """Function will show message and process ends after closing it.""" + from qtpy import QtWidgets, QtCore + from ayon_core import style + + app = QtWidgets.QApplication([]) + app.setStyleSheet(style.load_stylesheet()) + + msgbox = QtWidgets.QMessageBox() + msgbox.setWindowTitle(title) + msgbox.setText(message) + + if detail_message: + msgbox.setDetailedText(detail_message) + + msgbox.setWindowModality(QtCore.Qt.ApplicationModal) + msgbox.show() + + sys.exit(app.exec_()) + + +def on_invalid_args(script_not_found): + """Show to user message box saying that something went wrong. + + Tell user that arguments to launch implementation are invalid with + arguments details. + + Args: + script_not_found (bool): Use different message based on this value. + """ + + title = "Invalid arguments" + joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) + if script_not_found: + submsg = "Where couldn't find script path:\n\"{}\"" + else: + submsg = "Expected Host executable after script path:\n\"{}\"" + + message = "BUG: Got invalid arguments so can't launch Host application." + detail_message = "Process was launched with arguments:\n{}\n\n{}".format( + joined_args, + submsg.format(CURRENT_FILE) + ) + + show_error_messagebox(title, message, detail_message) + + +def main(argv): + # Modify current file path to find match in sys.argv which may be different + # on windows (different letter cases and slashes). + modified_current_file = CURRENT_FILE.replace("\\", "/").lower() + + # Create a copy of sys argv + sys_args = list(argv) + after_script_idx = None + # Find script path in sys.argv to know index of argv where host + # executable should be. + for idx, item in enumerate(sys_args): + if item.replace("\\", "/").lower() == modified_current_file: + after_script_idx = idx + 1 + break + + # Validate that there is at least one argument after script path + launch_args = None + if after_script_idx is not None: + launch_args = sys_args[after_script_idx:] + + if launch_args: + # Launch host implementation + main(*launch_args) + else: + # Show message box + on_invalid_args(after_script_idx is None) + + +if __name__ == "__main__": + main(sys.argv) From da395ae5305cb63cd19140cc2b48296facc94809 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:23:57 +0100 Subject: [PATCH 134/284] renamed 'HARMONY_HOST_DIR' to 'HARMONY_ADDON_ROOT' --- client/ayon_core/hosts/harmony/__init__.py | 4 ++-- client/ayon_core/hosts/harmony/addon.py | 4 ++-- client/ayon_core/hosts/harmony/api/pipeline.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/harmony/__init__.py b/client/ayon_core/hosts/harmony/__init__.py index 9177eaa285..449bb96a4f 100644 --- a/client/ayon_core/hosts/harmony/__init__.py +++ b/client/ayon_core/hosts/harmony/__init__.py @@ -1,10 +1,10 @@ from .addon import ( - HARMONY_HOST_DIR, + HARMONY_ADDON_ROOT, HarmonyAddon, ) __all__ = ( - "HARMONY_HOST_DIR", + "HARMONY_ADDON_ROOT", "HarmonyAddon", ) diff --git a/client/ayon_core/hosts/harmony/addon.py b/client/ayon_core/hosts/harmony/addon.py index 476d569415..7f8776cd74 100644 --- a/client/ayon_core/hosts/harmony/addon.py +++ b/client/ayon_core/hosts/harmony/addon.py @@ -1,7 +1,7 @@ import os from ayon_core.addon import AYONAddon, IHostAddon -HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) +HARMONY_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class HarmonyAddon(AYONAddon, IHostAddon): @@ -11,7 +11,7 @@ class HarmonyAddon(AYONAddon, IHostAddon): def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" openharmony_path = os.path.join( - HARMONY_HOST_DIR, "vendor", "OpenHarmony" + HARMONY_ADDON_ROOT, "vendor", "OpenHarmony" ) # TODO check if is already set? What to do if is already set? env["LIB_OPENHARMONY_PATH"] = openharmony_path diff --git a/client/ayon_core/hosts/harmony/api/pipeline.py b/client/ayon_core/hosts/harmony/api/pipeline.py index a753a32ebb..d842ccd414 100644 --- a/client/ayon_core/hosts/harmony/api/pipeline.py +++ b/client/ayon_core/hosts/harmony/api/pipeline.py @@ -15,13 +15,13 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.load import get_outdated_containers from ayon_core.pipeline.context_tools import get_current_project_folder -from ayon_core.hosts.harmony import HARMONY_HOST_DIR +from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT import ayon_core.hosts.harmony.api as harmony log = logging.getLogger("ayon_core.hosts.harmony") -PLUGINS_DIR = os.path.join(HARMONY_HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(HARMONY_ADDON_ROOT, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") From 2119e735828c44ea23fc4a9d463382e474f7367a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:25:14 +0100 Subject: [PATCH 135/284] harmony has own prelaunch hook --- .../hooks/pre_non_python_host_launch.py | 58 ----------- client/ayon_core/hosts/harmony/__init__.py | 2 + client/ayon_core/hosts/harmony/addon.py | 13 +++ .../hosts/harmony/hooks/pre_launch_args.py | 95 +++++++++++++++++++ 4 files changed, 110 insertions(+), 58 deletions(-) delete mode 100644 client/ayon_core/hooks/pre_non_python_host_launch.py create mode 100644 client/ayon_core/hosts/harmony/hooks/pre_launch_args.py diff --git a/client/ayon_core/hooks/pre_non_python_host_launch.py b/client/ayon_core/hooks/pre_non_python_host_launch.py deleted file mode 100644 index a65e0ecba4..0000000000 --- a/client/ayon_core/hooks/pre_non_python_host_launch.py +++ /dev/null @@ -1,58 +0,0 @@ -import os - -from ayon_core.lib import get_ayon_launcher_args -from ayon_core.lib.applications import ( - get_non_python_host_kwargs, - PreLaunchHook, - LaunchTypes, -) - -from ayon_core import AYON_CORE_ROOT - - -class NonPythonHostHook(PreLaunchHook): - """Launch arguments preparation. - - Non python host implementation do not launch host directly but use - python script which launch the host. For these cases it is necessary to - prepend python (or ayon) executable and script path before application's. - """ - app_groups = {"harmony"} - - order = 20 - launch_types = {LaunchTypes.local} - - def execute(self): - # Pop executable - executable_path = self.launch_context.launch_args.pop(0) - - # Pop rest of launch arguments - There should not be other arguments! - remainders = [] - while self.launch_context.launch_args: - remainders.append(self.launch_context.launch_args.pop(0)) - - script_path = os.path.join( - AYON_CORE_ROOT, - "scripts", - "non_python_host_launch.py" - ) - - new_launch_args = get_ayon_launcher_args( - "run", script_path, executable_path - ) - # Add workfile path if exists - workfile_path = self.data["last_workfile_path"] - if ( - self.data.get("start_last_workfile") - and workfile_path - and os.path.exists(workfile_path)): - new_launch_args.append(workfile_path) - - # Append as whole list as these areguments should not be separated - self.launch_context.launch_args.append(new_launch_args) - - if remainders: - self.launch_context.launch_args.extend(remainders) - - self.launch_context.kwargs = \ - get_non_python_host_kwargs(self.launch_context.kwargs) diff --git a/client/ayon_core/hosts/harmony/__init__.py b/client/ayon_core/hosts/harmony/__init__.py index 449bb96a4f..6454d6f9d7 100644 --- a/client/ayon_core/hosts/harmony/__init__.py +++ b/client/ayon_core/hosts/harmony/__init__.py @@ -1,10 +1,12 @@ from .addon import ( HARMONY_ADDON_ROOT, HarmonyAddon, + get_launch_script_path, ) __all__ = ( "HARMONY_ADDON_ROOT", "HarmonyAddon", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/harmony/addon.py b/client/ayon_core/hosts/harmony/addon.py index 7f8776cd74..1915a7eb6f 100644 --- a/client/ayon_core/hosts/harmony/addon.py +++ b/client/ayon_core/hosts/harmony/addon.py @@ -18,3 +18,16 @@ class HarmonyAddon(AYONAddon, IHostAddon): def get_workfile_extensions(self): return [".zip"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(HARMONY_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + HARMONY_ADDON_ROOT, "api", "launch_script.py" + ) diff --git a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py new file mode 100644 index 0000000000..1a36c890a7 --- /dev/null +++ b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py @@ -0,0 +1,95 @@ +import os +import platform +import subprocess + +from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.harmony import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for Harmony. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + executable_path = os.environ.get("AYON_EXECUTABLE") + + executable_filename = "" + if executable_path: + executable_filename = os.path.basename(executable_path) + + is_gui_executable = "ayon_console" not in executable_filename + if is_gui_executable: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + return kwargs + + +class HarmonyPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to Harmony implementation + before Harmony executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"harmony"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) From 4fb898615bff67a20925622e297859b369db6477 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:28:59 +0100 Subject: [PATCH 136/284] remove unused global script --- .../scripts/non_python_host_launch.py | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 client/ayon_core/scripts/non_python_host_launch.py diff --git a/client/ayon_core/scripts/non_python_host_launch.py b/client/ayon_core/scripts/non_python_host_launch.py deleted file mode 100644 index 4c18fd0ccc..0000000000 --- a/client/ayon_core/scripts/non_python_host_launch.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Script wraps launch mechanism of non python host implementations. - -Arguments passed to the script are passed to launch function in host -implementation. In all cases requires host app executable and may contain -workfile or others. -""" - -import os -import sys - -# Get current file to locate start point of sys.argv -CURRENT_FILE = os.path.abspath(__file__) - - -def show_error_messagebox(title, message, detail_message=None): - """Function will show message and process ends after closing it.""" - from qtpy import QtWidgets, QtCore - from ayon_core import style - - app = QtWidgets.QApplication([]) - app.setStyleSheet(style.load_stylesheet()) - - msgbox = QtWidgets.QMessageBox() - msgbox.setWindowTitle(title) - msgbox.setText(message) - - if detail_message: - msgbox.setDetailedText(detail_message) - - msgbox.setWindowModality(QtCore.Qt.ApplicationModal) - msgbox.show() - - sys.exit(app.exec_()) - - -def on_invalid_args(script_not_found): - """Show to user message box saying that something went wrong. - - Tell user that arguments to launch implementation are invalid with - arguments details. - - Args: - script_not_found (bool): Use different message based on this value. - """ - - title = "Invalid arguments" - joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) - if script_not_found: - submsg = "Where couldn't find script path:\n\"{}\"" - else: - submsg = "Expected Host executable after script path:\n\"{}\"" - - message = "BUG: Got invalid arguments so can't launch Host application." - detail_message = "Process was launched with arguments:\n{}\n\n{}".format( - joined_args, - submsg.format(CURRENT_FILE) - ) - - show_error_messagebox(title, message, detail_message) - - -def main(argv): - # Modify current file path to find match in sys.argv which may be different - # on windows (different letter cases and slashes). - modified_current_file = CURRENT_FILE.replace("\\", "/").lower() - - # Create a copy of sys argv - sys_args = list(argv) - after_script_idx = None - # Find script path in sys.argv to know index of argv where host - # executable should be. - for idx, item in enumerate(sys_args): - if item.replace("\\", "/").lower() == modified_current_file: - after_script_idx = idx + 1 - break - - # Validate that there is at least one argument after script path - launch_args = None - if after_script_idx is not None: - launch_args = sys_args[after_script_idx:] - - host_name = os.environ["AYON_HOST_NAME"].lower() - if host_name == "photoshop": - # TODO refactor launch logic according to AE - from ayon_core.hosts.photoshop.api.lib import main - elif host_name == "aftereffects": - from ayon_core.hosts.aftereffects.api.launch_logic import main - elif host_name == "harmony": - from ayon_core.hosts.harmony.api.lib import main - else: - title = "Unknown host name" - message = ( - "BUG: Environment variable AYON_HOST_NAME contains unknown" - " host name \"{}\"" - ).format(host_name) - show_error_messagebox(title, message) - return - - if launch_args: - # Launch host implementation - main(*launch_args) - else: - # Show message box - on_invalid_args(after_script_idx is None) - - -if __name__ == "__main__": - main(sys.argv) From 2c07fb45032d28162c32661cd977c36f20058aec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:29:16 +0100 Subject: [PATCH 137/284] removed unused 'get_non_python_host_kwargs' --- client/ayon_core/lib/applications.py | 39 ---------------------------- 1 file changed, 39 deletions(-) diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 4bf0c31d93..68fc3ea201 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1985,42 +1985,3 @@ def should_workfile_tool_start( if output is None: return default_output return output - - -def get_non_python_host_kwargs(kwargs, allow_console=True): - """Explicit setting of kwargs for Popen for AE/PS/Harmony. - - Expected behavior - - ayon_console opens window with logs - - ayon has stdout/stderr available for capturing - - Args: - kwargs (dict) or None - allow_console (bool): use False for inner Popen opening app itself or - it will open additional console (at least for Harmony) - """ - - if kwargs is None: - kwargs = {} - - if platform.system().lower() != "windows": - return kwargs - - executable_path = os.environ.get("AYON_EXECUTABLE") - - executable_filename = "" - if executable_path: - executable_filename = os.path.basename(executable_path) - - is_gui_executable = "ayon_console" not in executable_filename - if is_gui_executable: - kwargs.update({ - "creationflags": subprocess.CREATE_NO_WINDOW, - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL - }) - elif allow_console: - kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE - }) - return kwargs From ca39fc429130942c85883d471dd5604efad235dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:31:11 +0100 Subject: [PATCH 138/284] don't use 'get_non_python_host_kwargs' when launching harmony --- client/ayon_core/hosts/harmony/api/lib.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index bc73e19066..34cb14636d 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Utility functions used for Avalon - Harmony integration.""" +import platform import subprocess import threading import os @@ -22,7 +23,6 @@ from .server import Server from ayon_core.tools.stdout_broker.app import StdOutBroker from ayon_core.tools.utils import host_tools from ayon_core import style -from ayon_core.lib.applications import get_non_python_host_kwargs # Setup logging. log = logging.getLogger(__name__) @@ -324,7 +324,18 @@ def launch_zip_file(filepath): return print("Launching {}".format(scene_path)) - kwargs = get_non_python_host_kwargs({}, False) + kwargs = {} + if platform.system().lower() == "windows": + executable_filename = os.path.basename( + os.getenv("AYON_EXECUTABLE", "") + ) + if "ayon_console" not in executable_filename: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + process = subprocess.Popen( [ProcessContext.application_path, scene_path], **kwargs From 508d4b559e7ed8db2da14c3e1959b403efb33b43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:32:17 +0100 Subject: [PATCH 139/284] added comment with question --- client/ayon_core/hosts/harmony/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index 34cb14636d..b78fe468b5 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -324,6 +324,7 @@ def launch_zip_file(filepath): return print("Launching {}".format(scene_path)) + # QUESTION Could we use 'run_detached_process' from 'ayon_core.lib'? kwargs = {} if platform.system().lower() == "windows": executable_filename = os.path.basename( From 562730fa65384e89fe1c72cd981c0e997200641a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:33:02 +0100 Subject: [PATCH 140/284] added helper function to determine if ui executable is used --- .../aftereffects/hooks/pre_launch_args.py | 14 +++----- client/ayon_core/hosts/harmony/api/lib.py | 25 +++++++------- .../hosts/harmony/hooks/pre_launch_args.py | 14 +++----- .../hosts/photoshop/hooks/pre_launch_args.py | 14 +++----- client/ayon_core/lib/__init__.py | 2 ++ client/ayon_core/lib/ayon_info.py | 34 ++++++++++++++++++- 6 files changed, 63 insertions(+), 40 deletions(-) diff --git a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py index 2c5baa3b68..76ccd2bd4f 100644 --- a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py @@ -2,7 +2,10 @@ import os import platform import subprocess -from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ui_executable, +) from ayon_core.lib.applications import ( PreLaunchHook, LaunchTypes, @@ -27,14 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - executable_path = os.environ.get("AYON_EXECUTABLE") - - executable_filename = "" - if executable_path: - executable_filename = os.path.basename(executable_path) - - is_in_ui_launcher = "ayon_console" not in executable_filename - if is_in_ui_launcher: + if is_using_ui_executable(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index b78fe468b5..828ee3863e 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -15,15 +15,17 @@ import json import signal import time from uuid import uuid4 -from qtpy import QtWidgets, QtCore, QtGui import collections -from .server import Server +from qtpy import QtWidgets, QtCore, QtGui +from ayon_core.lib import is_using_ui_executable from ayon_core.tools.stdout_broker.app import StdOutBroker from ayon_core.tools.utils import host_tools from ayon_core import style +from .server import Server + # Setup logging. log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -326,16 +328,15 @@ def launch_zip_file(filepath): print("Launching {}".format(scene_path)) # QUESTION Could we use 'run_detached_process' from 'ayon_core.lib'? kwargs = {} - if platform.system().lower() == "windows": - executable_filename = os.path.basename( - os.getenv("AYON_EXECUTABLE", "") - ) - if "ayon_console" not in executable_filename: - kwargs.update({ - "creationflags": subprocess.CREATE_NO_WINDOW, - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL - }) + if ( + platform.system().lower() == "windows" + and is_using_ui_executable() + ): + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) process = subprocess.Popen( [ProcessContext.application_path, scene_path], diff --git a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py index 1a36c890a7..c2c667c1d8 100644 --- a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py @@ -2,7 +2,10 @@ import os import platform import subprocess -from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ui_executable, +) from ayon_core.lib.applications import ( PreLaunchHook, LaunchTypes, @@ -27,14 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - executable_path = os.environ.get("AYON_EXECUTABLE") - - executable_filename = "" - if executable_path: - executable_filename = os.path.basename(executable_path) - - is_gui_executable = "ayon_console" not in executable_filename - if is_gui_executable: + if is_using_ui_executable(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py index 0689a6c2f0..228413e1ea 100644 --- a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py @@ -2,7 +2,10 @@ import os import platform import subprocess -from ayon_core.lib import get_ayon_launcher_args +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ui_executable, +) from ayon_core.lib.applications import ( PreLaunchHook, LaunchTypes, @@ -27,14 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - executable_path = os.environ.get("AYON_EXECUTABLE") - - executable_filename = "" - if executable_path: - executable_filename = os.path.basename(executable_path) - - is_gui_executable = "ayon_console" not in executable_filename - if is_gui_executable: + if is_using_ui_executable(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index f69fd10b07..d82fb0de0e 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -155,6 +155,7 @@ from .path_tools import ( from .ayon_info import ( is_running_from_build, + is_using_ui_executable, is_staging_enabled, is_dev_mode_enabled, is_in_tests, @@ -275,6 +276,7 @@ __all__ = [ "Logger", "is_running_from_build", + "is_using_ui_executable", "is_staging_enabled", "is_dev_mode_enabled", "is_in_tests", diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index ec37d735d8..3d4c38c99a 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -10,6 +10,12 @@ from .local_settings import get_local_site_id def get_ayon_launcher_version(): + """Get AYON launcher version. + + Returns: + str: Version string. + + """ version_filepath = os.path.join(os.environ["AYON_ROOT"], "version.py") if not os.path.exists(version_filepath): return None @@ -24,8 +30,8 @@ def is_running_from_build(): Returns: bool: True if running from build. - """ + """ executable_path = os.environ["AYON_EXECUTABLE"] executable_filename = os.path.basename(executable_path) if "python" in executable_filename.lower(): @@ -33,6 +39,32 @@ def is_running_from_build(): return True +def is_using_ui_executable(): + """AYON launcher UI windows executable is used. + + This function make sense only on Windows platform. For other platforms + always returns False. False is also returned if process is running from + code. + + AYON launcher on windows has 2 executable files. First 'ayon_console.exe' + works as 'python.exe' executable, the second 'ayon.exe' works as + 'pythonw.exe' executable. The difference is way how stdout/stderr is + handled (especially when calling subprocess). + + Returns: + bool: True if UI executable is used. + + """ + if ( + platform.system().lower() != "windows" + or is_running_from_build() + ): + return False + executable_path = os.environ["AYON_EXECUTABLE"] + executable_filename = os.path.basename(executable_path) + return "ayon_console" not in executable_filename + + def is_staging_enabled(): return os.getenv("AYON_USE_STAGING") == "1" From 5cb07274d1fea5818c0e431c99aa0b3c36a7918c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:45:27 +0100 Subject: [PATCH 141/284] renamed 'is_using_ui_executable' to 'is_using_ayon_console' --- client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py | 4 ++-- client/ayon_core/hosts/harmony/api/lib.py | 4 ++-- client/ayon_core/hosts/harmony/hooks/pre_launch_args.py | 4 ++-- client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py | 4 ++-- client/ayon_core/lib/__init__.py | 4 ++-- client/ayon_core/lib/ayon_info.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py index 76ccd2bd4f..959df0eb39 100644 --- a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py @@ -4,7 +4,7 @@ import subprocess from ayon_core.lib import ( get_ayon_launcher_args, - is_using_ui_executable, + is_using_ayon_console, ) from ayon_core.lib.applications import ( PreLaunchHook, @@ -30,7 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - if is_using_ui_executable(): + if is_using_ayon_console(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index 828ee3863e..d9c17c6214 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -19,7 +19,7 @@ import collections from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.lib import is_using_ui_executable +from ayon_core.lib import is_using_ayon_console from ayon_core.tools.stdout_broker.app import StdOutBroker from ayon_core.tools.utils import host_tools from ayon_core import style @@ -330,7 +330,7 @@ def launch_zip_file(filepath): kwargs = {} if ( platform.system().lower() == "windows" - and is_using_ui_executable() + and is_using_ayon_console() ): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, diff --git a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py index c2c667c1d8..66bc0a80d8 100644 --- a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py @@ -4,7 +4,7 @@ import subprocess from ayon_core.lib import ( get_ayon_launcher_args, - is_using_ui_executable, + is_using_ayon_console, ) from ayon_core.lib.applications import ( PreLaunchHook, @@ -30,7 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - if is_using_ui_executable(): + if is_using_ayon_console(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py index 228413e1ea..7fe6e7437f 100644 --- a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py @@ -4,7 +4,7 @@ import subprocess from ayon_core.lib import ( get_ayon_launcher_args, - is_using_ui_executable, + is_using_ayon_console, ) from ayon_core.lib.applications import ( PreLaunchHook, @@ -30,7 +30,7 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - if is_using_ui_executable(): + if is_using_ayon_console(): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index d82fb0de0e..10e290360d 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -155,7 +155,7 @@ from .path_tools import ( from .ayon_info import ( is_running_from_build, - is_using_ui_executable, + is_using_ayon_console, is_staging_enabled, is_dev_mode_enabled, is_in_tests, @@ -276,7 +276,7 @@ __all__ = [ "Logger", "is_running_from_build", - "is_using_ui_executable", + "is_using_ayon_console", "is_staging_enabled", "is_dev_mode_enabled", "is_in_tests", diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index 3d4c38c99a..c3bda70834 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -39,7 +39,7 @@ def is_running_from_build(): return True -def is_using_ui_executable(): +def is_using_ayon_console(): """AYON launcher UI windows executable is used. This function make sense only on Windows platform. For other platforms From e15460d851dd56869cbcdbec52dde1d72aef25bb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 16:51:03 +0100 Subject: [PATCH 142/284] fix the reversed logic --- .../hosts/aftereffects/hooks/pre_launch_args.py | 8 ++++---- client/ayon_core/hosts/harmony/api/lib.py | 2 +- .../ayon_core/hosts/harmony/hooks/pre_launch_args.py | 8 ++++---- .../ayon_core/hosts/photoshop/hooks/pre_launch_args.py | 10 +++++----- client/ayon_core/lib/ayon_info.py | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py index 959df0eb39..979d9ff3e5 100644 --- a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py @@ -32,13 +32,13 @@ def get_launch_kwargs(kwargs): if is_using_ayon_console(): kwargs.update({ - "creationflags": subprocess.CREATE_NO_WINDOW, - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL + "creationflags": subprocess.CREATE_NEW_CONSOLE }) else: kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL }) return kwargs diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index d9c17c6214..3c833c7b69 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -330,7 +330,7 @@ def launch_zip_file(filepath): kwargs = {} if ( platform.system().lower() == "windows" - and is_using_ayon_console() + and not is_using_ayon_console() ): kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, diff --git a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py index 66bc0a80d8..bbad14084a 100644 --- a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py @@ -32,13 +32,13 @@ def get_launch_kwargs(kwargs): if is_using_ayon_console(): kwargs.update({ - "creationflags": subprocess.CREATE_NO_WINDOW, - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL + "creationflags": subprocess.CREATE_NEW_CONSOLE }) else: kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL }) return kwargs diff --git a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py index 7fe6e7437f..8358c11ca1 100644 --- a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py +++ b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py @@ -30,16 +30,16 @@ def get_launch_kwargs(kwargs): if platform.system().lower() != "windows": return kwargs - if is_using_ayon_console(): + if not is_using_ayon_console(): + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + else: kwargs.update({ "creationflags": subprocess.CREATE_NO_WINDOW, "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL }) - else: - kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE - }) return kwargs diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index c3bda70834..adb3c1befc 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -40,10 +40,10 @@ def is_running_from_build(): def is_using_ayon_console(): - """AYON launcher UI windows executable is used. + """AYON launcher console executable is used. This function make sense only on Windows platform. For other platforms - always returns False. False is also returned if process is running from + always returns True. True is also returned if process is running from code. AYON launcher on windows has 2 executable files. First 'ayon_console.exe' @@ -52,17 +52,17 @@ def is_using_ayon_console(): handled (especially when calling subprocess). Returns: - bool: True if UI executable is used. + bool: True if console executable is used. """ if ( platform.system().lower() != "windows" or is_running_from_build() ): - return False + return True executable_path = os.environ["AYON_EXECUTABLE"] executable_filename = os.path.basename(executable_path) - return "ayon_console" not in executable_filename + return "ayon_console" in executable_filename def is_staging_enabled(): From 28db94fd7eb70d3b55c9e57984da9747d6d883d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 17:33:10 +0100 Subject: [PATCH 143/284] fix work with templates in workfiles tool --- .../ayon_core/tools/workfiles/models/workfiles.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 479c8ea849..e645d020af 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -302,10 +302,11 @@ class WorkareaModel: file_template = anatomy.get_template_item( "work", template_key, "file" - ).template + ) + file_template_str = file_template.template - template_has_version = "{version" in file_template - template_has_comment = "{comment" in file_template + template_has_version = "{version" in file_template_str + template_has_comment = "{comment" in file_template_str comment_hints, comment = self._get_comments_from_root( file_template, @@ -315,7 +316,8 @@ class WorkareaModel: current_filename, ) last_version = self._get_last_workfile_version( - workdir, file_template, fill_data, extensions) + workdir, file_template_str, fill_data, extensions + ) return { "template_key": template_key, @@ -340,17 +342,18 @@ class WorkareaModel: ): anatomy = self._controller.project_anatomy fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) workdir = self._get_workdir(anatomy, template_key, fill_data) file_template = anatomy.get_template_item( "work", template_key, "file" - ).template + ) if use_last_version: version = self._get_last_workfile_version( - workdir, file_template, fill_data, self._extensions + workdir, file_template.template, fill_data, self._extensions ) fill_data["version"] = version fill_data["ext"] = extension.lstrip(".") From e8d3e569d1b6c405fedd55add7ff5e7b8c986de4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 17:48:55 +0100 Subject: [PATCH 144/284] added some docstrings and comments --- .../tools/workfiles/models/workfiles.py | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index e645d020af..5f59b99b22 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -25,7 +25,14 @@ from ayon_core.tools.workfiles.abstract import ( class CommentMatcher(object): - """Use anatomy and work file data to parse comments from filenames""" + """Use anatomy and work file data to parse comments from filenames. + + Args: + extensions (set[str]): Set of extensions. + file_template (AnatomyStringTemplate): File template. + data (dict[str, Any]): Data to fill the template with. + + """ def __init__(self, extensions, file_template, data): self.fname_regex = None @@ -199,6 +206,22 @@ class WorkareaModel: def _get_last_workfile_version( self, workdir, file_template, fill_data, extensions ): + """ + + Todos: + Validate if logic of this function is correct. It does return + last version + 1 which might be wrong. + + Args: + workdir (str): Workdir path. + file_template (str): File template. + fill_data (dict[str, Any]): Fill data. + extensions (set[str]): Extensions. + + Returns: + int: Next workfile version. + + """ version = get_last_workfile_with_version( workdir, file_template, fill_data, extensions )[1] @@ -225,8 +248,21 @@ class WorkareaModel: root, current_filename, ): + """Get comments from root directory. + + Args: + file_template (AnatomyStringTemplate): File template. + extensions (set[str]): Extensions. + fill_data (dict[str, Any]): Fill data. + root (str): Root directory. + current_filename (str): Current filename. + + Returns: + Tuple[list[str], Union[str, None]]: Comment hints and current + comment. + + """ current_comment = None - comment_hints = set() filenames = [] if root and os.path.exists(root): for filename in os.listdir(root): @@ -239,10 +275,11 @@ class WorkareaModel: filenames.append(filename) if not filenames: - return comment_hints, current_comment + return [], current_comment matcher = CommentMatcher(extensions, file_template, fill_data) + comment_hints = set() for filename in filenames: comment = matcher.parse_comment(filename) if comment: @@ -259,18 +296,18 @@ class WorkareaModel: return directory_template.format_strict(fill_data).normalized() def get_workarea_save_as_data(self, folder_id, task_id): - folder = None - task = None + folder_entity = None + task_entity = None if folder_id: - folder = self._controller.get_folder_entity( + folder_entity = self._controller.get_folder_entity( self.project_name, folder_id ) - if task_id: - task = self._controller.get_task_entity( - self.project_name, task_id - ) + if folder_entity and task_id: + task_entity = self._controller.get_task_entity( + self.project_name, task_id + ) - if not folder or not task: + if not folder_entity or not task_entity: return { "template_key": None, "template_has_version": None, @@ -340,6 +377,20 @@ class WorkareaModel: version, comment, ): + """Fill workarea filepath based on context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + extension (str): File extension. + use_last_version (bool): Use last version. + version (int): Version number. + comment (str): Comment. + + Returns: + WorkareaFilepathResult: Workarea filepath result. + + """ anatomy = self._controller.project_anatomy fill_data = self._prepare_fill_data(folder_id, task_id) From 553407a4bd828d043bf7e3fe2f43689a40616c42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Mar 2024 18:45:48 +0100 Subject: [PATCH 145/284] move workfile utils functions to workfile pipeline code --- client/ayon_core/lib/applications.py | 108 +--------------- .../ayon_core/pipeline/workfile/__init__.py | 8 ++ client/ayon_core/pipeline/workfile/utils.py | 121 ++++++++++++++++++ .../tools/launcher/models/actions.py | 5 +- 4 files changed, 137 insertions(+), 105 deletions(-) create mode 100644 client/ayon_core/pipeline/workfile/utils.py diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 444a4cf67e..3a6039357c 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1789,6 +1789,10 @@ def _prepare_last_workfile(data, workdir, addons_manager): from ayon_core.addon import AddonsManager from ayon_core.pipeline import HOST_WORKFILE_EXTENSIONS + from ayon_core.pipeline.workfile import ( + should_use_last_workfile_on_launch, + should_open_workfiles_tool_on_launch, + ) if not addons_manager: addons_manager = AddonsManager() @@ -1811,7 +1815,7 @@ def _prepare_last_workfile(data, workdir, addons_manager): start_last_workfile = data.get("start_last_workfile") if start_last_workfile is None: - start_last_workfile = should_start_last_workfile( + start_last_workfile = should_use_last_workfile_on_launch( project_name, app.host_name, task_name, task_type ) else: @@ -1819,7 +1823,7 @@ def _prepare_last_workfile(data, workdir, addons_manager): data["start_last_workfile"] = start_last_workfile - workfile_startup = should_workfile_tool_start( + workfile_startup = should_open_workfiles_tool_on_launch( project_name, app.host_name, task_name, task_type ) data["workfile_startup"] = workfile_startup @@ -1889,106 +1893,6 @@ def _prepare_last_workfile(data, workdir, addons_manager): data["last_workfile_path"] = last_workfile_path -def should_start_last_workfile( - project_name, host_name, task_name, task_type, default_output=False -): - """Define if host should start last version workfile if possible. - - Default output is `False`. Can be overridden with environment variable - `AYON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are - `"0", "1", "true", "false", "yes", "no"`. - - Args: - project_name (str): Project name. - host_name (str): Host name. - task_name (str): Task name. - task_type (str): Task type. - default_output (Optional[bool]): Default output if no profile is - found. - - Returns: - bool: True if host should start workfile. - - """ - - project_settings = get_project_settings(project_name) - profiles = ( - project_settings - ["core"] - ["tools"] - ["Workfiles"] - ["last_workfile_on_startup"] - ) - - if not profiles: - return default_output - - filter_data = { - "tasks": task_name, - "task_types": task_type, - "hosts": host_name - } - matching_item = filter_profiles(profiles, filter_data) - - output = None - if matching_item: - output = matching_item.get("enabled") - - if output is None: - return default_output - return output - - -def should_workfile_tool_start( - project_name, host_name, task_name, task_type, default_output=False -): - """Define if host should start workfile tool at host launch. - - Default output is `False`. Can be overridden with environment variable - `AYON_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are - `"0", "1", "true", "false", "yes", "no"`. - - Args: - project_name (str): Project name. - host_name (str): Host name. - task_name (str): Task name. - task_type (str): Task type. - default_output (Optional[bool]): Default output if no profile is - found. - - Returns: - bool: True if host should start workfile. - - """ - - project_settings = get_project_settings(project_name) - profiles = ( - project_settings - ["core"] - ["tools"] - ["Workfiles"] - ["open_workfile_tool_on_startup"] - ) - - if not profiles: - return default_output - - filter_data = { - "tasks": task_name, - "task_types": task_type, - "hosts": host_name - } - matching_item = filter_profiles(profiles, filter_data) - - output = None - if matching_item: - output = matching_item.get("enabled") - - if output is None: - return default_output - return output - - def get_non_python_host_kwargs(kwargs, allow_console=True): """Explicit setting of kwargs for Popen for AE/PS/Harmony. diff --git a/client/ayon_core/pipeline/workfile/__init__.py b/client/ayon_core/pipeline/workfile/__init__.py index 94ecc81bd6..36766e3a04 100644 --- a/client/ayon_core/pipeline/workfile/__init__.py +++ b/client/ayon_core/pipeline/workfile/__init__.py @@ -13,6 +13,11 @@ from .path_resolving import ( create_workdir_extra_folders, ) +from .utils import ( + should_use_last_workfile_on_launch, + should_open_workfiles_tool_on_launch, +) + from .build_workfile import BuildWorkfile @@ -30,5 +35,8 @@ __all__ = ( "create_workdir_extra_folders", + "should_use_last_workfile_on_launch", + "should_open_workfiles_tool_on_launch", + "BuildWorkfile", ) diff --git a/client/ayon_core/pipeline/workfile/utils.py b/client/ayon_core/pipeline/workfile/utils.py new file mode 100644 index 0000000000..53de3269b2 --- /dev/null +++ b/client/ayon_core/pipeline/workfile/utils.py @@ -0,0 +1,121 @@ +from ayon_core.lib import filter_profiles +from ayon_core.settings import get_project_settings + + +def should_use_last_workfile_on_launch( + project_name, + host_name, + task_name, + task_type, + default_output=False, + project_settings=None, +): + """Define if host should start last version workfile if possible. + + Default output is `False`. Can be overridden with environment variable + `AYON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + task_type (str): Task type. + default_output (Optional[bool]): Default output value if no profile + is found. + project_settings (Optional[dict[str, Any]]): Project settings. + + Returns: + bool: True if host should start workfile. + + """ + if project_settings is None: + project_settings = get_project_settings(project_name) + profiles = ( + project_settings + ["core"] + ["tools"] + ["Workfiles"] + ["last_workfile_on_startup"] + ) + + if not profiles: + return default_output + + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name + } + matching_item = filter_profiles(profiles, filter_data) + + output = None + if matching_item: + output = matching_item.get("enabled") + + if output is None: + return default_output + return output + + +def should_open_workfiles_tool_on_launch( + project_name, + host_name, + task_name, + task_type, + default_output=False, + project_settings=None, +): + """Define if host should start workfile tool at host launch. + + Default output is `False`. Can be overridden with environment variable + `AYON_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + task_type (str): Task type. + default_output (Optional[bool]): Default output value if no profile + is found. + project_settings (Optional[dict[str, Any]]): Project settings. + + Returns: + bool: True if host should start workfile. + + """ + + if project_settings is None: + project_settings = get_project_settings(project_name) + profiles = ( + project_settings + ["core"] + ["tools"] + ["Workfiles"] + ["open_workfile_tool_on_startup"] + ) + + if not profiles: + return default_output + + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name + } + matching_item = filter_profiles(profiles, filter_data) + + output = None + if matching_item: + output = matching_item.get("enabled") + + if output is None: + return default_output + return output diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 50c3e85b0a..97943e6ad7 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -6,6 +6,7 @@ from ayon_core.pipeline.actions import ( discover_launcher_actions, LauncherAction, ) +from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch # class Action: @@ -301,8 +302,6 @@ class ActionsModel: host_name, not_open_workfile_actions ): - from ayon_core.lib.applications import should_start_last_workfile - if identifier in not_open_workfile_actions: return not not_open_workfile_actions[identifier] @@ -315,7 +314,7 @@ class ActionsModel: task_name = task_entity["name"] task_type = task_entity["taskType"] - output = should_start_last_workfile( + output = should_use_last_workfile_on_launch( project_name, host_name, task_name, From 93c22be6bf6e987babd450935374c18cb568b0d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:06:19 +0100 Subject: [PATCH 146/284] Fix syntax error --- .../hosts/maya/plugins/publish/validate_look_shading_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py index 7d249a6021..e70a805de4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py @@ -16,7 +16,7 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin, Shading engines should be named "{surface_shader}SG" """ -`` + order = ValidateContentsOrder families = ["look"] hosts = ["maya"] From 4f60f2b21e2c3cbafe156bfadb97fcb18fb78885 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:22:07 +0100 Subject: [PATCH 147/284] Add a way to globally disable the `cbId` workflow in Maya --- .../validate_animation_out_set_related_node_ids.py | 7 +++++++ .../publish/validate_arnold_scene_source_cbid.py | 7 +++++++ .../publish/validate_look_id_reference_edits.py | 7 +++++++ .../hosts/maya/plugins/publish/validate_node_ids.py | 7 +++++++ .../publish/validate_node_ids_deformed_shapes.py | 7 +++++++ .../plugins/publish/validate_node_ids_in_database.py | 7 +++++++ .../maya/plugins/publish/validate_node_ids_related.py | 7 +++++++ .../maya/plugins/publish/validate_node_ids_unique.py | 7 +++++++ .../plugins/publish/validate_rig_out_set_node_ids.py | 7 +++++++ .../maya/plugins/publish/validate_rig_output_ids.py | 7 +++++++ server_addon/maya/server/settings/main.py | 10 ++++++++++ 11 files changed, 80 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index b96c5f07e2..b6876199e5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -32,6 +32,13 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, ] optional = False + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 8bcd272d01..a9d896952d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -22,6 +22,13 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin, actions = [RepairAction] optional = False + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + @staticmethod def _get_nodes_by_name(nodes): nodes_by_name = {} diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_look_id_reference_edits.py b/client/ayon_core/hosts/maya/plugins/publish/validate_look_id_reference_edits.py index 1d313bdae4..7ae3b4b9b5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_look_id_reference_edits.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_look_id_reference_edits.py @@ -27,6 +27,13 @@ class ValidateLookIdReferenceEdits(pyblish.api.InstancePlugin): actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): invalid = self.get_invalid(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py index f40db988c6..ba748a4fc4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py @@ -31,6 +31,13 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin): actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, ayon_core.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py index 912311cc8d..545ab8e28c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py @@ -26,6 +26,13 @@ class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): RepairAction ] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all the nodes in the instance""" diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 65a779f3f0..5ca9690fd7 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -26,6 +26,13 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, ayon_core.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py index 606abee3d2..992988dc7d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py @@ -24,6 +24,13 @@ class ValidateNodeIDsRelated(pyblish.api.InstancePlugin, actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, ayon_core.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all nodes in instance (including hierarchy)""" if not self.is_active(instance.data): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py index 6c5cd26259..f4994922ce 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -26,6 +26,13 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, ayon_core.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index ab8cc25210..e42cd50977 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -34,6 +34,13 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, allow_history_only = False optional = False + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_output_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_output_ids.py index 93552ccce0..d04006f013 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -32,6 +32,13 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): actions = [RepairAction, ayon_core.hosts.maya.api.action.SelectInvalidAction] + @classmethod + def apply_settings(cls, project_settings): + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): invalid = self.get_invalid(instance, compute=True) if invalid: diff --git a/server_addon/maya/server/settings/main.py b/server_addon/maya/server/settings/main.py index f7f62e219d..a4562f54d7 100644 --- a/server_addon/maya/server/settings/main.py +++ b/server_addon/maya/server/settings/main.py @@ -30,6 +30,15 @@ class ExtMappingItemModel(BaseSettingsModel): class MayaSettings(BaseSettingsModel): """Maya Project Settings.""" + use_cbid_workflow: bool = SettingsField( + True, title="Use cbId workflow", + description=( + "When enabled, a per node `cbId` identifier will be created and " + "validated for many product types. This is then used for look " + "publishing and many others. By disabling this, the `cbId` " + "attribute will still be created on scene save but it will not " + "be validated.")) + open_workfile_post_initialization: bool = SettingsField( True, title="Open Workfile Post Initialization") explicit_plugins_loading: ExplicitPluginsLoadingModel = SettingsField( @@ -88,6 +97,7 @@ DEFAULT_MEL_WORKSPACE_SETTINGS = "\n".join(( )) DEFAULT_MAYA_SETTING = { + "use_cbid_workflow": True, "open_workfile_post_initialization": True, "explicit_plugins_loading": DEFAULT_EXPLITCIT_PLUGINS_LOADING_SETTINGS, "imageio": DEFAULT_IMAGEIO_SETTINGS, From 8af23931ae46d6e2dc6b3243de62093b39590f8f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:22:41 +0100 Subject: [PATCH 148/284] Bump maya server addon version --- server_addon/maya/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index a86a3ce0a1..0b44700d27 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.10" +__version__ = "0.1.11" From b19f640d538abdd7c9572703e792b45996c4f9d8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:29:03 +0100 Subject: [PATCH 149/284] Preserve automatic settings applying when relevant --- .../validate_animation_out_set_related_node_ids.py | 11 ++++++++++- .../plugins/publish/validate_rig_out_set_node_ids.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index b6876199e5..ac18dc7a57 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -7,7 +7,9 @@ from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + get_plugin_settings, + apply_plugin_settings_automatically ) @@ -39,6 +41,13 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, cls.enabled = False return + # Preserve automatic settings applying logic + settings = get_plugin_settings(plugin=cls, + project_settings=project_settings, + log=cls.log, + category="maya") + apply_plugin_settings_automatically(cls, settings, logger=cls.log) + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index e42cd50977..c7afd41a91 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -8,7 +8,9 @@ from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + get_plugin_settings, + apply_plugin_settings_automatically ) @@ -41,6 +43,13 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, cls.enabled = False return + # Preserve automatic settings applying logic + settings = get_plugin_settings(plugin=cls, + project_settings=project_settings, + log=cls.log, + category="maya") + apply_plugin_settings_automatically(cls, settings, logger=cls.log) + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): From e094573b454840b290a68daaaaafeeefebfb4402 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:36:49 +0100 Subject: [PATCH 150/284] Re-order logic so global toggle will always override any other settings --- .../validate_animation_out_set_related_node_ids.py | 10 +++++----- .../plugins/publish/validate_rig_out_set_node_ids.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index ac18dc7a57..2502fd74b2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -36,11 +36,6 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, @classmethod def apply_settings(cls, project_settings): - # Disable plug-in if cbId workflow is disabled - if not project_settings["maya"].get("use_cbid_workflow", True): - cls.enabled = False - return - # Preserve automatic settings applying logic settings = get_plugin_settings(plugin=cls, project_settings=project_settings, @@ -48,6 +43,11 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, category="maya") apply_plugin_settings_automatically(cls, settings, logger=cls.log) + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index c7afd41a91..c55953df7a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -38,11 +38,6 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, @classmethod def apply_settings(cls, project_settings): - # Disable plug-in if cbId workflow is disabled - if not project_settings["maya"].get("use_cbid_workflow", True): - cls.enabled = False - return - # Preserve automatic settings applying logic settings = get_plugin_settings(plugin=cls, project_settings=project_settings, @@ -50,6 +45,11 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, category="maya") apply_plugin_settings_automatically(cls, settings, logger=cls.log) + # Disable plug-in if cbId workflow is disabled + if not project_settings["maya"].get("use_cbid_workflow", True): + cls.enabled = False + return + def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): From 769f80e923a07435c0a6813a4c20048ba4d8f3c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 20:43:55 +0100 Subject: [PATCH 151/284] Import validation message --- .../help/validate_rig_out_set_node_ids.xml | 32 +++++++++++++++++++ .../publish/validate_rig_out_set_node_ids.py | 18 +++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/hosts/maya/plugins/publish/help/validate_rig_out_set_node_ids.xml diff --git a/client/ayon_core/hosts/maya/plugins/publish/help/validate_rig_out_set_node_ids.xml b/client/ayon_core/hosts/maya/plugins/publish/help/validate_rig_out_set_node_ids.xml new file mode 100644 index 0000000000..374b8e59ae --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/publish/help/validate_rig_out_set_node_ids.xml @@ -0,0 +1,32 @@ + + + +Shape IDs mismatch original shape +## Shapes mismatch IDs with original shape + +Meshes are detected in the **rig** where the (deformed) mesh has a different +`cbId` than the same mesh in its deformation history. +Theses should normally be the same. + +### How to repair? + +By using the repair action the IDs from the shape in history will be +copied to the deformed shape. For rig instances, in many cases the +correct fix is to use the repair action **unless** you explicitly tried +to update the `cbId` values on the meshes - in that case you actually want +to do to the reverse and copy the IDs from the deformed mesh to the history +mesh instead. + + + +### How does this happen? + +When a deformer is applied in the scene on a referenced mesh that had no +deformers then Maya will create a new shape node for the mesh that +does not have the original id. Then on scene save new ids get created for the +meshes lacking a `cbId` and thus the mesh then has a different `cbId` than +the mesh in the deformation history. + + + + diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index ab8cc25210..c9e779e999 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -7,7 +7,7 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError, + PublishXmlValidationError, OptionalPyblishPluginMixin ) @@ -42,8 +42,20 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - "Nodes found with mismatching IDs: {0}".format(invalid) + + # Use the short names + invalid = cmds.ls(invalid) + invalid.sort() + + # Construct a human-readable list + invalid = "\n".join("- {}".format(node) for node in invalid) + + raise PublishXmlValidationError( + plugin=ValidateRigOutSetNodeIds, + message=( + "Rig nodes have different IDs than their input " + "history: \n{0}".format(invalid) + ) ) @classmethod From c7a1ab2a65ef24e568d5661d314eccf9d0ddd3d7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 21:44:57 +0100 Subject: [PATCH 152/284] Add version check for Hou 20+ --- client/ayon_core/hosts/houdini/plugins/load/load_image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_image.py b/client/ayon_core/hosts/houdini/plugins/load/load_image.py index 72bb873eff..0429b1c3fe 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_image.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_image.py @@ -166,6 +166,10 @@ class ImageLoader(load.LoaderPlugin): dict: Parm to value mapping if colorspace data is defined. """ + # Using OCIO colorspace on COP2 File node is only supported in Hou 20+ + major, _, _ = hou.applicationVersion() + if major < 20: + return {} data = representation.get("data", {}).get("colorspaceData", {}) if not data: From 3cee75065734549542277bf0ee9585404343fe59 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 22:10:08 +0100 Subject: [PATCH 153/284] Fix instance node --- .../ayon_core/hosts/maya/plugins/publish/validate_step_size.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py index a3419a83a9..cad584d8f9 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py @@ -29,7 +29,7 @@ class ValidateStepSize(pyblish.api.InstancePlugin, @classmethod def get_invalid(cls, instance): - objset = instance.data['name'] + objset = instance.data['instance_node'] step = instance.data.get("step", 1.0) if step < cls.MIN or step > cls.MAX: From f95d172c55f1fba372d8b7b501894016b34fde23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 22:10:26 +0100 Subject: [PATCH 154/284] Improve validation message --- .../ayon_core/hosts/maya/plugins/publish/validate_step_size.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py index cad584d8f9..a276a5b644 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py @@ -47,4 +47,4 @@ class ValidateStepSize(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - "Invalid instances found: {0}".format(invalid)) + "Instance found with invalid step size: {0}".format(invalid)) From 0536998b660d9c3d00ba6c2a500613329687f15b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 23:04:03 +0100 Subject: [PATCH 155/284] Maya: load image plane set colorspace --- .../maya/plugins/load/load_image_plane.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py index 7d6f7e26cf..c5b85d2cd4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py @@ -145,6 +145,18 @@ class ImagePlaneLoader(load.LoaderPlugin): fileName=context["representation"]["data"]["path"], camera=camera ) + + # Set colorspace + colorspace = self.get_colorspace(context["representation"]) + if colorspace: + cmds.setAttr( + "{}.ignoreColorSpaceFileRules".format(image_plane_shape), + True + ) + cmds.setAttr("{}.colorSpace".format(image_plane_shape), + colorspace, type="string") + + # Set offset frame range start_frame = cmds.playbackOptions(query=True, min=True) end_frame = cmds.playbackOptions(query=True, max=True) @@ -216,6 +228,15 @@ class ImagePlaneLoader(load.LoaderPlugin): repre_entity["id"], type="string") + colorspace = self.get_colorspace(repre_entity) + if colorspace: + cmds.setAttr( + "{}.ignoreColorSpaceFileRules".format(image_plane_shape), + True + ) + cmds.setAttr("{}.colorSpace".format(image_plane_shape), + colorspace, type="string") + # Set frame range. start_frame = folder_entity["attrib"]["frameStart"] end_frame = folder_entity["attrib"]["frameEnd"] @@ -243,3 +264,12 @@ class ImagePlaneLoader(load.LoaderPlugin): deleteNamespaceContent=True) except RuntimeError: pass + + def get_colorspace(self, representation): + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return + + colorspace = data.get("colorspace") + return colorspace From 0e30dfb2a8d3a38c61f6d6ad668971305da77ed5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:29:53 +0100 Subject: [PATCH 156/284] Maya: Raise PublishValidationError in validators --- .../plugins/publish/validate_mesh_lamina_faces.py | 10 +++++++--- .../maya/plugins/publish/validate_mesh_ngons.py | 7 ++++--- .../plugins/publish/validate_mesh_single_uv_set.py | 5 +++-- .../plugins/publish/validate_mesh_uv_set_map1.py | 7 ++++--- .../plugins/publish/validate_node_no_ghosting.py | 12 +++++++----- .../plugins/publish/validate_shape_default_names.py | 7 ++++--- .../publish/validate_skinCluster_deformer_set.py | 12 +++++++++--- .../maya/plugins/publish/validate_unique_names.py | 8 +++++--- .../publish/validate_vray_referenced_aovs.py | 6 ++++-- .../maya/plugins/publish/validate_vrayproxy.py | 13 ++++++++----- 10 files changed, 55 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py index 8f80b689fd..5543505206 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py @@ -2,7 +2,11 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateMeshOrder, OptionalPyblishPluginMixin +from ayon_core.pipeline.publish import ( + ValidateMeshOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, @@ -36,5 +40,5 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with lamina faces: " - "{0}".format(invalid)) + raise PublishValidationError( + "Meshes found with lamina faces: {0}".format(invalid)) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index 5f107b7f7e..1bca4d5e9a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( ValidateContentsOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) @@ -49,5 +50,5 @@ class ValidateMeshNgons(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with n-gons" - "values: {0}".format(invalid)) + raise PublishValidationError( + "Meshes found with n-gons: {0}".format(invalid)) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py index 8dbd0ca264..21697cd903 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py @@ -6,7 +6,8 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) @@ -66,7 +67,7 @@ class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin, if allowed: self.log.warning(message) else: - raise ValueError(message) + raise PublishValidationError(message) @classmethod def repair(cls, instance): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py index c7f405b0cf..a139b65169 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py @@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) @@ -55,8 +56,8 @@ class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found without 'map1' " - "UV set: {0}".format(invalid)) + raise PublishValidationError( + "Meshes found without 'map1' UV set: {0}".format(invalid)) @classmethod def repair(cls, instance): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py index 73701f8d83..10cbbc9a88 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py @@ -5,10 +5,12 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - OptionalPyblishPluginMixin - + OptionalPyblishPluginMixin, + PublishValidationError ) -class ValidateNodeNoGhosting(pyblish.api.InstancePlugin. + + +class ValidateNodeNoGhosting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure nodes do not have ghosting enabled. @@ -55,5 +57,5 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin. invalid = self.get_invalid(instance) if invalid: - raise ValueError("Nodes with ghosting enabled found: " - "{0}".format(invalid)) + raise PublishValidationError( + "Nodes with ghosting enabled found: {0}".format(invalid)) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_default_names.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_default_names.py index 2f0811a73e..c4c4c909d3 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_default_names.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_default_names.py @@ -8,7 +8,8 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, RepairAction, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) @@ -84,8 +85,8 @@ class ValidateShapeDefaultNames(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Incorrectly named shapes " - "found: {0}".format(invalid)) + raise PublishValidationError( + "Incorrectly named shapes found: {0}".format(invalid)) @classmethod def repair(cls, instance): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py b/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py index 48d8e63553..a548e12f33 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py @@ -3,7 +3,11 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateContentsOrder,OptionalPyblishPluginMixin +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin, @@ -30,8 +34,10 @@ class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Invalid skinCluster relationships " - "found on meshes: {0}".format(invalid)) + raise PublishValidationError( + "Invalid skinCluster relationships found on meshes: {0}" + .format(invalid) + ) @classmethod def get_invalid(cls, instance): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py index 8ec704ddd1..72c3c7dc72 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py @@ -4,9 +4,11 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) + class ValidateUniqueNames(pyblish.api.Validator, OptionalPyblishPluginMixin): """transform names should be unique @@ -40,5 +42,5 @@ class ValidateUniqueNames(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - raise ValueError("Nodes found with none unique names. " - "values: {0}".format(invalid)) + raise PublishValidationError( + "Nodes found with non-unique names:\n{0}".format(invalid)) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py index 7c480a6bf7..9df5fb8488 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py @@ -6,9 +6,11 @@ from maya import cmds from ayon_core.pipeline.publish import ( RepairContextAction, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) + class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate whether the V-Ray Render Elements (AOVs) include references. @@ -60,7 +62,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, self.log.error(( "'Use referenced' not enabled in Vray Render Settings." )) - raise AssertionError("Invalid render settings") + raise PublishValidationError("Invalid render settings") @classmethod def repair(cls, context): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py index 29b8be411c..47006ca9de 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py @@ -1,7 +1,10 @@ import pyblish.api -from ayon_core.pipeline import KnownPublishError -from ayon_core.pipeline.publish import OptionalPyblishPluginMixin +from ayon_core.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError +) + class ValidateVrayProxy(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): @@ -17,18 +20,18 @@ class ValidateVrayProxy(pyblish.api.InstancePlugin, if not self.is_active(data): return if not data["setMembers"]: - raise KnownPublishError( + raise PublishValidationError( "'%s' is empty! This is a bug" % instance.name ) if data["animation"]: if data["frameEnd"] < data["frameStart"]: - raise KnownPublishError( + raise PublishValidationError( "End frame is smaller than start frame" ) if not data["vrmesh"] and not data["alembic"]: - raise KnownPublishError( + raise PublishValidationError( "Both vrmesh and alembic are off. Needs at least one to" " publish." ) From 64be88cff5129de55e9a487cf2cee3503e91e968 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:44:14 +0100 Subject: [PATCH 157/284] Add description to error message --- .../plugins/publish/validate_mesh_lamina_faces.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py index 5543505206..bfb4257f23 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py @@ -24,6 +24,16 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] optional = True + description = ( + "## Meshes with Lamina Faces\n" + "Detected meshes with lamina faces. Lamina faces are faces " + "that share all of their edges and thus are merged together on top of " + "each other.\n\n" + "### How to repair?\n" + "You can repair them by using Maya's modeling tool `Mesh > Cleanup..` " + "and select to cleanup matching polygons for lamina faces." + ) + @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) @@ -41,4 +51,5 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, if invalid: raise PublishValidationError( - "Meshes found with lamina faces: {0}".format(invalid)) + "Meshes found with lamina faces: {0}".format(invalid), + description=self.description) From 2bdb362a04260e33efbcb98a1b2066d64b0c2df2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:44:54 +0100 Subject: [PATCH 158/284] Simplify error message for readability --- .../ayon_core/hosts/maya/plugins/publish/validate_mesh_empty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_empty.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_empty.py index 934cbae327..c95e1ec816 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_empty.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_empty.py @@ -51,5 +51,5 @@ class ValidateMeshEmpty(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - "Meshes found in instance without any vertices: %s" % invalid + "Meshes found without any vertices: %s" % invalid ) From 7f25e9d5f1eb473370202de460960df73c897fba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:47:42 +0100 Subject: [PATCH 159/284] Add description to validation report --- .../maya/plugins/publish/validate_mesh_ngons.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index 1bca4d5e9a..dd43b70bd2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -28,6 +28,15 @@ class ValidateMeshNgons(pyblish.api.Validator, actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] optional = True + description = ( + "## Meshes with NGONs Faces\n" + "Detected meshes with NGON faces. NGONS are faces that " + "with more than four sides.\n\n" + "### How to repair?\n" + "You can repair them by usings Maya's modeling tool Mesh > Cleanup.. " + "and select to cleanup matching polygons for lamina faces." + ) + @staticmethod def get_invalid(instance): @@ -51,4 +60,5 @@ class ValidateMeshNgons(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - "Meshes found with n-gons: {0}".format(invalid)) + "Meshes found with n-gons: {0}".format(invalid), + description=self.description) From 9107440bf52db8d24f6952386f36a4896db3c0f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:48:08 +0100 Subject: [PATCH 160/284] Add description to validation report --- .../ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index dd43b70bd2..b6d3dc73fd 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -30,7 +30,7 @@ class ValidateMeshNgons(pyblish.api.Validator, description = ( "## Meshes with NGONs Faces\n" - "Detected meshes with NGON faces. NGONS are faces that " + "Detected meshes with NGON faces. **NGONS** are faces that " "with more than four sides.\n\n" "### How to repair?\n" "You can repair them by usings Maya's modeling tool Mesh > Cleanup.. " From 9f6b9ddd3d119b268eaddf526346ce838e3f1f89 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 09:50:56 +0100 Subject: [PATCH 161/284] Cosmetics/hound --- .../maya/plugins/publish/validate_mesh_shader_connections.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py index 8672ac13dd..70ede83f2d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py @@ -107,8 +107,9 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Shapes found with invalid shader " - "connections: {0}".format(invalid)) + raise PublishValidationError( + "Shapes found with invalid shader connections: " + "{0}".format(invalid)) @staticmethod def get_invalid(instance): From 248265d5eb2b01e0dc1f724383b0ae656d25dd24 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:11:47 +0100 Subject: [PATCH 162/284] Optimize logic --- .../maya/plugins/publish/validate_no_null_transforms.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py index a9dc1d5bef..48af387f95 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -26,15 +26,10 @@ def has_shape_children(node): return False # Check if there are any shapes at all - shapes = cmds.ls(allDescendents, shapes=True) + shapes = cmds.ls(allDescendents, shapes=True, noIntermediate=True) if not shapes: return False - # Check if all descendent shapes are intermediateObjects; - # if so we consider this node a null node and return False. - if all(cmds.getAttr('{0}.intermediateObject'.format(x)) for x in shapes): - return False - return True From 6b3808fcfa58798046be2f3c4a9a1254c403e1c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:12:10 +0100 Subject: [PATCH 163/284] Cosmetics/hound --- .../plugins/publish/validate_no_null_transforms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py index 48af387f95..38955fd777 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -19,14 +19,14 @@ def _as_report_list(values, prefix="- ", suffix="\n"): def has_shape_children(node): # Check if any descendants - allDescendents = cmds.listRelatives(node, - allDescendents=True, - fullPath=True) - if not allDescendents: + all_descendents = cmds.listRelatives(node, + allDescendents=True, + fullPath=True) + if not all_descendents: return False # Check if there are any shapes at all - shapes = cmds.ls(allDescendents, shapes=True, noIntermediate=True) + shapes = cmds.ls(all_descendents, shapes=True, noIntermediate=True) if not shapes: return False From d0da71c45cc2ec80a8b6fa8cc66db36393c81bbb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:14:39 +0100 Subject: [PATCH 164/284] Improve readability of the artist facing report --- .../maya/plugins/publish/validate_no_namespace.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py index 7ea2a79339..f546caff2c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py @@ -49,11 +49,17 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: + invalid_namespaces = {get_namespace(node) for node in invalid} raise PublishValidationError( - "Namespaces found:\n\n{0}".format( - _as_report_list(sorted(invalid)) + message="Namespaces found:\n\n{0}".format( + _as_report_list(sorted(invalid_namespaces)) ), - title="Namespaces in model" + title="Namespaces in model", + description=( + "## Namespaces found in model\n" + "It is not allowed to publish a model that contains " + "namespaces." + ) ) @classmethod From fe8613b55e14d529ad76d9cd8a1e25e13631f6af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:38:20 +0100 Subject: [PATCH 165/284] Improve description, remove docstring which was only about the `bake_attributes` to begin with --- server_addon/maya/server/settings/publishers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index fe5c10e93c..7e1cdbf167 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -315,16 +315,13 @@ class ExtractMayaSceneRawModel(BaseSettingsModel): class ExtractCameraAlembicModel(BaseSettingsModel): - """ - List of attributes that will be added to the baked alembic camera. Needs to be written in python list syntax. - """ enabled: bool = SettingsField(title="ExtractCameraAlembic") optional: bool = SettingsField(title="Optional") active: bool = SettingsField(title="Active") bake_attributes: str = SettingsField( "[]", title="Bake Attributes", widget="textarea", description="List of attributes that will be included in the alembic " - "export.", + "camera export. Needs to be written as a JSON list.", ) @validator("bake_attributes") From 08a6cbc57c33bf346bbc4364d9473bc9d312a4be Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 11:57:15 +0100 Subject: [PATCH 166/284] Ensure unique class name compared to `extract_yeti_cache.py` --- .../hosts/maya/plugins/publish/extract_unreal_yeticache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_yeticache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_yeticache.py index 9a264959d1..9a6b4ebaed 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_yeticache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_yeticache.py @@ -5,13 +5,13 @@ from maya import cmds from ayon_core.pipeline import publish -class ExtractYetiCache(publish.Extractor): +class ExtractUnrealYetiCache(publish.Extractor): """Producing Yeti cache files using scene time range. This will extract Yeti cache file sequence and fur settings. """ - label = "Extract Yeti Cache" + label = "Extract Yeti Cache (Unreal)" hosts = ["maya"] families = ["yeticacheUE"] From b57337ea7713a92d564bdab12e9885dc7c065a08 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 26 Mar 2024 12:58:19 +0200 Subject: [PATCH 167/284] Houdini Redshift allow disabling AOVs --- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index 67cc080ead..8437757c58 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -68,12 +68,15 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): files_by_aov = { "_": self.generate_expected_files(instance, beauty_product)} - + aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode() if aovs_rop: rop = aovs_rop - num_aovs = rop.evalParm("RS_aov") + num_aovs = 0 + if not rop.evalParm('RS_aovAllAOVsDisabled'): + num_aovs = rop.evalParm("RS_aov") + for index in range(num_aovs): i = index + 1 From e391f058081d4bb6ab83aff18bea7598f7344c3b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 14:47:42 +0100 Subject: [PATCH 168/284] Refactor frame data collection and sequence handling - Removed unnecessary line - Added check for files type before processing --- .../plugins/publish/collect_sequence_frame_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index bd5bac114d..de18050f41 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -43,7 +43,6 @@ class CollectSequenceFrameData( instance.data[key] = value self.log.debug(f"Collected Frame range data '{key}':{value} ") - def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") folder_attributes = instance.data["folderEntity"]["attrib"] @@ -56,6 +55,9 @@ class CollectSequenceFrameData( return files = first_repre["files"] + if not isinstance(files, list): + files = [files] + collections, _ = clique.assemble(files) if not collections: # No sequences detected and we can't retrieve From ec11da9861ea3d9670ed066b19aed1db5829c876 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Mar 2024 15:53:49 +0100 Subject: [PATCH 169/284] fix access to folder entity in look assigner tool --- .../ayon_core/hosts/maya/tools/mayalookassigner/widgets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py index 6cc5f156e3..f345b87e36 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py @@ -125,8 +125,9 @@ class AssetOutliner(QtWidgets.QWidget): folder_items = {} namespaces_by_folder_path = defaultdict(set) for item in items: - folder_id = item["folder"]["id"] - folder_path = item["folder"]["path"] + folder_entity = item["folder_entity"] + folder_id = folder_entity["id"] + folder_path = folder_entity["path"] namespaces_by_folder_path[folder_path].add(item.get("namespace")) if folder_path in folder_items: From 3526a1e52e60aef2892ad26892eac8c677ece6a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Mar 2024 16:02:44 +0100 Subject: [PATCH 170/284] use correct source for product type --- .../hosts/maya/plugins/load/load_redshift_proxy.py | 5 +---- client/ayon_core/hosts/maya/plugins/load/load_reference.py | 6 +----- .../ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py | 5 +---- .../hosts/maya/plugins/load/load_vdb_to_redshift.py | 5 +---- .../ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py | 5 +---- client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py | 5 +---- client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py | 5 +---- client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py | 5 +---- 8 files changed, 8 insertions(+), 33 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py b/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py index 63dae87243..0f91d9048a 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py @@ -32,10 +32,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, options=None): """Plugin entry point.""" - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "redshiftproxy" + product_type = context["product"]["productType"] folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( diff --git a/client/ayon_core/hosts/maya/plugins/load/load_reference.py b/client/ayon_core/hosts/maya/plugins/load/load_reference.py index fdd85eda43..de18b2b0ec 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_reference.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_reference.py @@ -117,11 +117,7 @@ class ReferenceLoader(plugin.ReferenceLoader): def process_reference(self, context, name, namespace, options): import maya.cmds as cmds - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "model" - + product_type = context["product"]["productType"] project_name = context["project"]["name"] # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py index f0fb89e5a4..3d984fdc79 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -25,10 +25,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.lib import unique_namespace - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "vdbcache" + product_type = context["product"]["productType"] # Check if the plugin for arnold is available on the pc try: diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py index cad0900590..3fa490f405 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -31,10 +31,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.lib import unique_namespace - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "vdbcache" + product_type = context["product"]["productType"] # Check if the plugin for redshift is available on the pc try: diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py index 88f62e81a4..7b87c21f38 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -94,10 +94,7 @@ class LoadVDBtoVRay(load.LoaderPlugin): "Path does not exist: %s" % path ) - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "vdbcache" + product_type = context["product"]["productType"] # Ensure V-ray is loaded with the vrayvolumegrid if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py index 59d8eadefa..895a4a4127 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py @@ -47,10 +47,7 @@ class VRayProxyLoader(load.LoaderPlugin): """ - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "vrayproxy" + product_type = context["product"]["productType"] # get all representations for this version filename = self._get_abc( diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py b/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py index 2f4ab1d080..36a25e2af1 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py @@ -26,10 +26,7 @@ class VRaySceneLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "vrayscene_layer" + product_type = context["product"]["productType"] folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( diff --git a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py index 8933c4d8a6..a5cd04b0f4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py @@ -56,10 +56,7 @@ class YetiCacheLoader(load.LoaderPlugin): """ - try: - product_type = context["representation"]["context"]["family"] - except ValueError: - product_type = "yeticache" + product_type = context["product"]["productType"] # Build namespace folder_name = context["folder"]["name"] From 40f2001b886c3d4eb11d871bd0eaf32dd8d7ade3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 16:15:47 +0100 Subject: [PATCH 171/284] Fusion: Prompt reset scene context on saving to another task --- client/ayon_core/hosts/fusion/api/lib.py | 127 +++++++++++++++++- client/ayon_core/hosts/fusion/api/pipeline.py | 46 ++++++- 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index e5bf4b5a44..cfb77a3652 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -5,6 +5,8 @@ import contextlib from ayon_core.lib import Logger +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.context_tools import get_current_project_folder self = sys.modules[__name__] @@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True, comp.SetAttrs(attrs) -def set_current_context_framerange(): +def set_current_context_framerange(folder_entity=None): """Set Comp's frame range based on current folder.""" - folder_entity = get_current_project_folder() + if folder_entity is None: + folder_entity = get_current_project_folder( + fields={"attrib.frameStart", + "attrib.frameEnd", + "attrib.handleStart", + "attrib.handleEnd"}) + folder_attributes = folder_entity["attrib"] start = folder_attributes["frameStart"] end = folder_attributes["frameEnd"] @@ -65,9 +73,24 @@ def set_current_context_framerange(): handle_end=handle_end) -def set_current_context_resolution(): +def set_current_context_fps(folder_entity=None): + """Set Comp's frame rate (FPS) to based on current asset""" + if folder_entity is None: + folder_entity = get_current_project_folder(fields={"attrib.fps"}) + + fps = float(folder_entity["attrib"].get("fps", 24.0)) + comp = get_current_comp() + comp.SetPrefs({ + "Comp.FrameFormat.Rate": fps, + }) + + +def set_current_context_resolution(folder_entity=None): """Set Comp's resolution width x height default based on current folder""" - folder_entity = get_current_project_folder() + if folder_entity is None: + folder_entity = get_current_project_folder( + fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}) + folder_attributes = folder_entity["attrib"] width = folder_attributes["resolutionWidth"] height = folder_attributes["resolutionHeight"] @@ -285,3 +308,99 @@ def comp_lock_and_undo_chunk( finally: comp.Unlock() comp.EndUndo(keep_undo) + + +def update_content_on_context_change(): + """Update all Creator instances to current asset""" + host = registered_host() + context = host.get_current_context() + + folder_path = context["folder_path"] + task = context["task_name"] + + create_context = CreateContext(host, reset=True) + + for instance in create_context.instances: + instance_folder_path = instance.get("folderPath") + if instance_folder_path and instance_folder_path != folder_path: + instance["folderPath"] = folder_path + instance_task = instance.get("task") + if instance_task and instance_task != task: + instance["task"] = task + + create_context.save_changes() + + +def prompt_reset_context(): + """Prompt the user what context settings to reset. + This prompt is used on saving to a different task to allow the scene to + get matched to the new context. + """ + # TODO: Cleanup this prototyped mess of imports and odd dialog + from ayon_core.tools.attribute_defs.dialog import ( + AttributeDefinitionsDialog + ) + from ayon_core.style import load_stylesheet + from ayon_core.lib import BoolDef, UILabelDef + from qtpy import QtWidgets, QtCore + + definitions = [ + UILabelDef( + label=( + "You are saving your scene into a different task." + "\n\n" + "Would you like to reset some settings for the " + "for the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset Comp FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset Comp start and end frame ranges", + default=True + ), + BoolDef( + "resolution", + label="Comp Resolution", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "instances", + label="Publish instances", + tooltip="Update all publish instance's folder and task to match " + "the new folder and task", + default=True + ), + ] + + dialog = AttributeDefinitionsDialog(definitions) + dialog.setWindowFlags( + dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setStyleSheet(load_stylesheet()) + if not dialog.exec_(): + return None + + options = dialog.get_values() + folder_entity = get_current_project_folder() + if options["frame_range"]: + set_current_context_framerange(folder_entity) + + if options["fps"]: + set_current_context_fps(folder_entity) + + if options["resolution"]: + set_current_context_resolution(folder_entity) + + if options["instances"]: + update_content_on_context_change() + + dialog.deleteLater() diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 50157cfae6..03773790e4 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -5,6 +5,7 @@ import os import sys import logging import contextlib +from pathlib import Path import pyblish.api from qtpy import QtCore @@ -28,8 +29,8 @@ from ayon_core.tools.utils import host_tools from .lib import ( get_current_comp, - comp_lock_and_undo_chunk, - validate_comp_prefs + validate_comp_prefs, + prompt_reset_context ) log = Logger.get_logger(__name__) @@ -41,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +# Track whether the workfile tool is about to save +ABOUT_TO_SAVE = False + class FusionLogHandler(logging.Handler): # Keep a reference to fusion's Print function (Remote Object) @@ -104,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # Register events register_event_callback("open", on_after_open) + register_event_callback("workfile.save.before", before_workfile_save) register_event_callback("save", on_save) register_event_callback("new", on_new) + register_event_callback("taskChanged", on_task_changed) # region workfile io api def has_unsaved_changes(self): @@ -169,6 +175,19 @@ def on_save(event): comp = event["sender"] validate_comp_prefs(comp) + # We are now starting the actual save directly + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = False + + +def on_task_changed(): + global ABOUT_TO_SAVE + print(f"Task changed: {ABOUT_TO_SAVE}") + # TODO: Only do this if not headless + if ABOUT_TO_SAVE: + # Let's prompt the user to update the context settings or not + prompt_reset_context() + def on_after_open(event): comp = event["sender"] @@ -202,6 +221,28 @@ def on_after_open(event): dialog.setStyleSheet(load_stylesheet()) +def before_workfile_save(event): + # Due to Fusion's external python process design we can't really + # detect whether the current Fusion environment matches the one the artists + # expects it to be. For example, our pipeline python process might + # have been shut down, and restarted - which will restart it to the + # environment Fusion started with; not necessarily where the artist + # is currently working. + # The `ABOUT_TO_SAVE` var is used to detect context changes when + # saving into another asset. If we keep it False it will be ignored + # as context change. As such, before we change tasks we will only + # consider it the current filepath is within the currently known + # AVALON_WORKDIR. This way we avoid false positives of thinking it's + # saving to another context and instead sometimes just have false negatives + # where we fail to show the "Update on task change" prompt. + comp = get_current_comp() + filepath = comp.GetAttrs()["COMPS_FileName"] + workdir = os.environ.get("AYON_WORKDIR") + if Path(workdir) in Path(filepath).parents: + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = True + + def ls(): """List containers from active Fusion scene @@ -338,7 +379,6 @@ class FusionEventHandler(QtCore.QObject): >>> handler = FusionEventHandler(parent=window) >>> handler.start() - """ ACTION_IDS = [ "Comp_Save", From 99e81bf45362275aadf6006d9fd6a701d6244f76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 17:11:31 +0100 Subject: [PATCH 172/284] Refactor validation logic for editorial asset names - Updated condition to check entity_type using get() method. --- .../ayon_core/plugins/publish/validate_editorial_asset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index 9b6794a0c4..33b4210ad5 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -117,6 +117,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): output[folder_path] = [ str(p["entity_name"]) for p in parents - if p["entity_type"].lower() != "project" + if p.get("entity_type") != "project" ] return output From 5a3ad328fba0005c6d579a565df8a3093909a2c3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 17:12:05 +0100 Subject: [PATCH 173/284] Refactor hierarchy data structure for better organization. - Refactored the hierarchy data structure to improve organization and readability. - Updated the way child entities are nested within parent entities. --- .../plugins/publish/collect_hierarchy.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 0751bf305b..2ae3cc67f3 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -18,14 +18,13 @@ class CollectHierarchy(pyblish.api.ContextPlugin): def process(self, context): project_name = context.data["projectName"] - temp_context = {} final_context = { project_name: { "entity_type": "project", - "children": temp_context + "children": {} }, } - + temp_context = {} for instance in context: self.log.debug("Processing instance: `{}` ...".format(instance)) @@ -68,21 +67,11 @@ class CollectHierarchy(pyblish.api.ContextPlugin): actual = {name: shot_data} for parent in reversed(instance.data["parents"]): - next_dict = {} - parent_name = parent["entity_name"] - next_dict[parent_name] = {} - next_dict[parent_name]["entity_type"] = "folder" - next_dict[parent_name]["folder_type"] = parent[ - "entity_type"].capitalize() - next_dict[parent_name]["children"] = actual - - for parent in reversed(instance.data["parents"]): - parent_name = parent["entity_name"] next_dict = { - parent_name: { + parent["entity_name"]: { "entity_type": "folder", - "folder_type": parent["entity_type"].capitalize(), - "children": actual + "folder_type": parent["folder_type"], + "children": actual, } } actual = next_dict @@ -93,6 +82,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not temp_context: return + final_context[project_name]["children"] = temp_context + # adding hierarchy context to context context.data["hierarchyContext"] = final_context self.log.debug("context.data[hierarchyContext] is: {}".format( From af6102afbd9e519395d6832afbd0512323a94388 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 17:13:11 +0100 Subject: [PATCH 174/284] Add folder type mapping and handle missing types, improve logging. - Added a dictionary to map folder types by name - Implemented handling for missing folder types with default "Folder" - Enhanced logging for missing folder types and active paths --- .../publish/extract_hierarchy_to_ayon.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index d43dcab28c..60c92aa8b1 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -115,6 +115,10 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): entity_hub = EntityHub(project_name) project = entity_hub.project_entity + folder_type_name_by_low_name = { + folder_type_item["name"].lower(): folder_type_item["name"] + for folder_type_item in project.get_folder_types() + } hierarchy_match_queue = collections.deque() hierarchy_match_queue.append((project, hierarchy_context)) @@ -167,8 +171,18 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): # TODO check if existing entity have 'folder' type child_entity = children_by_low_name.get(child_name.lower()) if child_entity is None: + folder_type = folder_type_name_by_low_name.get( + child_info["folder_type"].lower() + ) + if folder_type is None: + # TODO add validator for folder type validations + self.log.warning(( + "Couldn't find folder type '{}'" + ).format(child_info["folder_type"])) + folder_type = "Folder" + child_entity = entity_hub.add_new_folder( - child_info["entity_type"], + folder_type, parent_id=entity.id, name=child_name ) @@ -223,12 +237,11 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): # filter only the active publishing instances active_folder_paths = set() for instance in context: - if instance.data.get("publish") is not False: + if instance.data.get("publish", True) is not False: active_folder_paths.add(instance.data.get("folderPath")) active_folder_paths.discard(None) - self.log.debug("Active folder paths: {}".format(active_folder_paths)) if not active_folder_paths: return None @@ -271,10 +284,11 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): item_id = uuid.uuid4().hex new_item = copy.deepcopy(folder_info) - new_item["name"] = folder_name - new_item["children"] = [] new_children_context = new_item.pop("children", None) tasks = new_item.pop("tasks", {}) + + new_item["name"] = folder_name + new_item["children"] = [] task_items = [] for task_name, task_info in tasks.items(): task_info["name"] = task_name From cd0a3005d3d2cfc9418c4d32fa1fbac296b4d93b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 26 Mar 2024 17:27:31 +0100 Subject: [PATCH 175/284] Fix - differentiate between host main and main --- client/ayon_core/hosts/aftereffects/api/launch_script.py | 4 ++-- client/ayon_core/hosts/harmony/api/launch_script.py | 4 ++-- client/ayon_core/hosts/photoshop/api/launch_script.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/aftereffects/api/launch_script.py b/client/ayon_core/hosts/aftereffects/api/launch_script.py index ad4e779bd0..87926c022b 100644 --- a/client/ayon_core/hosts/aftereffects/api/launch_script.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_script.py @@ -8,7 +8,7 @@ workfile or others. import os import sys -from ayon_core.hosts.aftereffects.api.launch_logic import main +from ayon_core.hosts.aftereffects.api.launch_logic import main as host_main # Get current file to locate start point of sys.argv CURRENT_FILE = os.path.abspath(__file__) @@ -83,7 +83,7 @@ def main(argv): if launch_args: # Launch host implementation - main(*launch_args) + host_main(*launch_args) else: # Show message box on_invalid_args(after_script_idx is None) diff --git a/client/ayon_core/hosts/harmony/api/launch_script.py b/client/ayon_core/hosts/harmony/api/launch_script.py index 1f2c36b7e6..3c809e210f 100644 --- a/client/ayon_core/hosts/harmony/api/launch_script.py +++ b/client/ayon_core/hosts/harmony/api/launch_script.py @@ -8,7 +8,7 @@ workfile or others. import os import sys -from ayon_core.hosts.harmony.api.lib import main +from ayon_core.hosts.harmony.api.lib import main as host_main # Get current file to locate start point of sys.argv CURRENT_FILE = os.path.abspath(__file__) @@ -83,7 +83,7 @@ def main(argv): if launch_args: # Launch host implementation - main(*launch_args) + host_main(*launch_args) else: # Show message box on_invalid_args(after_script_idx is None) diff --git a/client/ayon_core/hosts/photoshop/api/launch_script.py b/client/ayon_core/hosts/photoshop/api/launch_script.py index c036b63c46..bb4de80086 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_script.py +++ b/client/ayon_core/hosts/photoshop/api/launch_script.py @@ -8,7 +8,7 @@ workfile or others. import os import sys -from ayon_core.hosts.photoshop.api.lib import main +from ayon_core.hosts.photoshop.api.lib import main as host_main # Get current file to locate start point of sys.argv CURRENT_FILE = os.path.abspath(__file__) @@ -83,7 +83,7 @@ def main(argv): if launch_args: # Launch host implementation - main(*launch_args) + host_main(*launch_args) else: # Show message box on_invalid_args(after_script_idx is None) From 59756f812b51c324b28e776d7bf586fb7d7d8539 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:34:54 +0100 Subject: [PATCH 176/284] Maya: Prompt reset scene context on saving to another task --- client/ayon_core/hosts/maya/api/lib.py | 106 ++++++++++++++++++-- client/ayon_core/hosts/maya/api/pipeline.py | 15 +++ 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 8ca898f621..7fa4700d6a 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2647,31 +2647,115 @@ def reset_scene_resolution(): set_scene_resolution(width, height, pixelAspect) -def set_context_settings(): +def set_context_settings( + fps=True, + resolution=True, + frame_range=True, + colorspace=True +): """Apply the project settings from the project definition - Settings can be overwritten by an folder if the folder.attrib contains + Settings can be overwritten by an asset if the asset.data contains any information regarding those settings. - Examples of settings: - fps - resolution - renderer + Args: + fps (bool): Whether to set the scene FPS. + resolution (bool): Whether to set the render resolution. + frame_range (bool): Whether to reset the time slide frame ranges. + colorspace (bool): Whether to reset the colorspace. Returns: None """ - # Set project fps - set_scene_fps(get_fps_for_current_context()) + if fps: + # Set project fps + set_scene_fps(get_fps_for_current_context()) - reset_scene_resolution() + if resolution: + reset_scene_resolution() # Set frame range. - reset_frame_range() + if frame_range: + reset_frame_range(fps=False) # Set colorspace - set_colorspace() + if colorspace: + set_colorspace() + + +def prompt_reset_context(): + """Prompt the user what context settings to reset. + This prompt is used on saving to a different task to allow the scene to + get matched to the new context. + """ + # TODO: Cleanup this prototyped mess of imports and odd dialog + from ayon_core.tools.attribute_defs.dialog import ( + AttributeDefinitionsDialog + ) + from ayon_core.style import load_stylesheet + from ayon_core.lib import BoolDef, UILabelDef + + definitions = [ + UILabelDef( + label=( + "You are saving your scene into a different task." + "\n\n" + "Would you like to reset some settings for the " + "for the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset Comp FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset Comp start and end frame ranges", + default=True + ), + BoolDef( + "resolution", + label="Resolution", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "colorspace", + label="Colorspace", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "instances", + label="Publish instances", + tooltip="Update all publish instance's folder and task to match " + "the new folder and task", + default=True + ), + ] + + dialog = AttributeDefinitionsDialog(definitions) + dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setStyleSheet(load_stylesheet()) + if not dialog.exec_(): + return None + + options = dialog.get_values() + with suspended_refresh(): + set_context_settings( + fps=options["fps"], + resolution=options["resolution"], + frame_range=options["frame_range"], + colorspace=options["colorspace"] + ) + if options["instances"]: + update_content_on_context_change() + + dialog.deleteLater() # Valid FPS diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index b3e401b91e..2be452a22a 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -67,6 +67,9 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" +# Track whether the workfile tool is about to save +ABOUT_TO_SAVE = False + class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "maya" @@ -581,6 +584,10 @@ def on_save(): for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) + # We are now starting the actual save directly + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = False + def on_open(): """On scene open let's assume the containers have changed.""" @@ -650,6 +657,11 @@ def on_task_changed(): lib.set_context_settings() lib.update_content_on_context_change() + global ABOUT_TO_SAVE + if not lib.IS_HEADLESS and ABOUT_TO_SAVE: + # Let's prompt the user to update the context settings or not + lib.prompt_reset_context() + def before_workfile_open(): if handle_workfile_locks(): @@ -664,6 +676,9 @@ def before_workfile_save(event): if workdir_path: create_workspace_mel(workdir_path, project_name) + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = True + def workfile_save_before_xgen(event): """Manage Xgen external files when switching context. From 62fc3e2cdd210009c381d56941335d583713e6b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:38:52 +0100 Subject: [PATCH 177/284] Fix tooltips --- client/ayon_core/hosts/maya/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 7fa4700d6a..4e9410af41 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2708,25 +2708,25 @@ def prompt_reset_context(): BoolDef( "fps", label="FPS", - tooltip="Reset Comp FPS", + tooltip="Reset workfile FPS", default=True ), BoolDef( "frame_range", label="Frame Range", - tooltip="Reset Comp start and end frame ranges", + tooltip="Reset workfile start and end frame ranges", default=True ), BoolDef( "resolution", label="Resolution", - tooltip="Reset Comp resolution", + tooltip="Reset workfile resolution", default=True ), BoolDef( "colorspace", label="Colorspace", - tooltip="Reset Comp resolution", + tooltip="Reset workfile resolution", default=True ), BoolDef( From 27f546dc35e44b778a0e2800ebc2a16a413e4ee8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:52:46 +0100 Subject: [PATCH 178/284] Remove "This is a bug" --- .../ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py index 47006ca9de..0288d4b865 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py @@ -21,7 +21,7 @@ class ValidateVrayProxy(pyblish.api.InstancePlugin, return if not data["setMembers"]: raise PublishValidationError( - "'%s' is empty! This is a bug" % instance.name + f"Instance '{instance.name}' is empty." ) if data["animation"]: From 243df68ea2cf72fa7472d5c45dfe473f4ece57ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 18:06:46 +0100 Subject: [PATCH 179/284] Houdini: Prompt reset scene context on saving to another task --- client/ayon_core/hosts/houdini/api/lib.py | 123 +++++++++++++++--- .../ayon_core/hosts/houdini/api/pipeline.py | 21 +++ 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 681052a44d..6a314d097d 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -526,7 +526,7 @@ def maintained_selection(): node.setSelected(on=True) -def reset_framerange(): +def reset_framerange(fps=True, frame_range=True): """Set frame range and FPS to current folder.""" project_name = get_current_project_name() @@ -535,29 +535,32 @@ def reset_framerange(): folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) folder_attributes = folder_entity["attrib"] - # Get FPS - fps = get_folder_fps(folder_entity) + # Set FPS + if fps: + fps = get_folder_fps(folder_entity) + print("Setting scene FPS to {}".format(int(fps))) + set_scene_fps(fps) - # Get Start and End Frames - frame_start = folder_attributes.get("frameStart") - frame_end = folder_attributes.get("frameEnd") + if frame_range: - if frame_start is None or frame_end is None: - log.warning("No edit information found for '{}'".format(folder_path)) - return + # Set Start and End Frames + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") - handle_start = folder_attributes.get("handleStart", 0) - handle_end = folder_attributes.get("handleEnd", 0) + if frame_start is None or frame_end is None: + log.warning("No edit information found for '%s'", folder_path) + return - frame_start -= int(handle_start) - frame_end += int(handle_end) + handle_start = folder_attributes.get("handleStart", 0) + handle_end = folder_attributes.get("handleEnd", 0) - # Set frame range and FPS - print("Setting scene FPS to {}".format(int(fps))) - set_scene_fps(fps) - hou.playbar.setFrameRange(frame_start, frame_end) - hou.playbar.setPlaybackRange(frame_start, frame_end) - hou.setFrame(frame_start) + frame_start -= int(handle_start) + frame_end += int(handle_end) + + # Set frame range and FPS + hou.playbar.setFrameRange(frame_start, frame_end) + hou.playbar.setPlaybackRange(frame_start, frame_end) + hou.setFrame(frame_start) def get_main_window(): @@ -1072,3 +1075,85 @@ def add_self_publish_button(node): template = node.parmTemplateGroup() template.insertBefore((0,), button_parm) node.setParmTemplateGroup(template) + + +def update_content_on_context_change(): + """Update all Creator instances to current asset""" + host = registered_host() + context = host.get_current_context() + + folder_path = context["folder_path"] + task = context["task_name"] + + create_context = CreateContext(host, reset=True) + + for instance in create_context.instances: + instance_folder_path = instance.get("folderPath") + if instance_folder_path and instance_folder_path != folder_path: + instance["folderPath"] = folder_path + instance_task = instance.get("task") + if instance_task and instance_task != task: + instance["task"] = task + + create_context.save_changes() + + +def prompt_reset_context(): + """Prompt the user what context settings to reset. + This prompt is used on saving to a different task to allow the scene to + get matched to the new context. + """ + # TODO: Cleanup this prototyped mess of imports and odd dialog + from ayon_core.tools.attribute_defs.dialog import ( + AttributeDefinitionsDialog + ) + from ayon_core.style import load_stylesheet + from ayon_core.lib import BoolDef, UILabelDef + + definitions = [ + UILabelDef( + label=( + "You are saving your scene into a different task." + "\n\n" + "Would you like to reset some settings for the " + "for the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset workfile FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset workfile start and end frame ranges", + default=True + ), + BoolDef( + "instances", + label="Publish instances", + tooltip="Update all publish instance's folder and task to match " + "the new folder and task", + default=True + ), + ] + + dialog = AttributeDefinitionsDialog(definitions) + dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setStyleSheet(load_stylesheet()) + if not dialog.exec_(): + return None + + options = dialog.get_values() + if options["fps"] or options["frame_range"]: + reset_framerange( + fps=options["fps"], + frame_range=options["frame_range"] + ) + + if options["instances"]: + update_content_on_context_change() + + dialog.deleteLater() \ No newline at end of file diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index d5144200cf..bad23f8db5 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -39,6 +39,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +# Track whether the workfile tool is about to save +ABOUT_TO_SAVE = False + class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "houdini" @@ -61,10 +64,12 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing callbacks ... ") # register_event_callback("init", on_init) self._register_callbacks() + register_event_callback("workfile.save.before", before_workfile_save) register_event_callback("before.save", before_save) register_event_callback("save", on_save) register_event_callback("open", on_open) register_event_callback("new", on_new) + register_event_callback("taskChanged", on_task_changed) self._has_been_setup = True @@ -287,6 +292,11 @@ def ls(): yield parse_container(container) +def before_workfile_save(event): + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = True + + def before_save(): return lib.validate_fps() @@ -302,6 +312,17 @@ def on_save(): for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) + # We are now starting the actual save directly + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = False + + +def on_task_changed(): + global ABOUT_TO_SAVE + if not IS_HEADLESS and ABOUT_TO_SAVE: + # Let's prompt the user to update the context settings or not + lib.prompt_reset_context() + def _show_outdated_content_popup(): # Get main window From b1ba751f3a15a6a2e6c1602fc6244c93cc3348fb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 18:11:06 +0100 Subject: [PATCH 180/284] Remove legacy unused id logic in Houdini - this was never used --- client/ayon_core/hosts/houdini/api/lib.py | 78 ------------------- .../ayon_core/hosts/houdini/api/pipeline.py | 4 - 2 files changed, 82 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 681052a44d..673183a15d 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -44,84 +44,6 @@ def get_folder_fps(folder_entity=None): return folder_entity["attrib"]["fps"] -def set_id(node, unique_id, overwrite=False): - exists = node.parm("id") - if not exists: - imprint(node, {"id": unique_id}) - - if not exists and overwrite: - node.setParm("id", unique_id) - - -def get_id(node): - """Get the `cbId` attribute of the given node. - - Args: - node (hou.Node): the name of the node to retrieve the attribute from - - Returns: - str: cbId attribute of the node. - - """ - - if node is not None: - return node.parm("id") - - -def generate_ids(nodes, folder_id=None): - """Returns new unique ids for the given nodes. - - Note: This does not assign the new ids, it only generates the values. - - To assign new ids using this method: - >>> nodes = ["a", "b", "c"] - >>> for node, id in generate_ids(nodes): - >>> set_id(node, id) - - To also override any existing values (and assign regenerated ids): - >>> nodes = ["a", "b", "c"] - >>> for node, id in generate_ids(nodes): - >>> set_id(node, id, overwrite=True) - - Args: - nodes (list): List of nodes. - folder_id (str): Folder id . Use current folder id if is ``None``. - - Returns: - list: A list of (node, id) tuples. - - """ - - if folder_id is None: - project_name = get_current_project_name() - folder_path = get_current_folder_path() - # Get folder id of current context folder - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path, fields={"id"} - ) - if not folder_entity: - raise ValueError("No current folder is set.") - - folder_id = folder_entity["id"] - - node_ids = [] - for node in nodes: - _, uid = str(uuid.uuid4()).rsplit("-", 1) - unique_id = "{}:{}".format(folder_id, uid) - node_ids.append((node, unique_id)) - - return node_ids - - -def get_id_required_nodes(): - - valid_types = ["geometry"] - nodes = {n for n in hou.node("/out").children() if - n.type().name() in valid_types} - - return list(nodes) - - def get_output_parameter(node): """Return the render output parameter of the given node diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index d5144200cf..3439bdea0a 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -298,10 +298,6 @@ def on_save(): # update houdini vars lib.update_houdini_vars_context_dialog() - nodes = lib.get_id_required_nodes() - for node, new_id in lib.generate_ids(nodes): - lib.set_id(node, new_id, overwrite=False) - def _show_outdated_content_popup(): # Get main window From c0cf9de455e4ec1153248415bee46aa5da9fd362 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 18:13:54 +0100 Subject: [PATCH 181/284] Fix correctly updating context data on saving in publisher --- client/ayon_core/hosts/houdini/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index d5144200cf..dd635cfd48 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -166,7 +166,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): if not op_ctx: op_ctx = self.create_context_node() - lib.imprint(op_ctx, data) + lib.imprint(op_ctx, data, update=True) def get_context_data(self): op_ctx = hou.node(CONTEXT_CONTAINER) From df1d0d4a864fce397ba02d2f009666e1e09ef2cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Mar 2024 18:32:55 +0100 Subject: [PATCH 182/284] fix unreal load plugins too --- .../hosts/unreal/plugins/load/load_alembic_animation.py | 2 +- client/ayon_core/hosts/unreal/plugins/load/load_layout.py | 2 +- .../ayon_core/hosts/unreal/plugins/load/load_layout_existing.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py index 02259b706c..64d684939c 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py @@ -72,7 +72,7 @@ class AnimationAlembicLoader(plugin.Loader): root = unreal_pipeline.AYON_ASSET_DIR folder_name = context["folder"]["name"] folder_path = context["folder"]["path"] - product_type = context["representation"]["context"]["family"] + product_type = context["product"]["productType"] suffix = "_CON" if folder_name: asset_name = "{}_{}".format(folder_name, name) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py index 6c667d3d2f..6c01925453 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py @@ -659,7 +659,7 @@ class LayoutLoader(plugin.Loader): "loader": str(self.__class__.__name__), "representation": context["representation"]["id"], "parent": context["representation"]["versionId"], - "family": context["representation"]["context"]["family"], + "family": context["product"]["productType"], "loaded_assets": loaded_assets } imprint( diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py index 700b6957a2..56e36f6185 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py @@ -393,7 +393,7 @@ class ExistingLayoutLoader(plugin.Loader): folder_name = context["folder"]["name"] folder_path = context["folder"]["path"] - product_type = context["representation"]["context"]["family"] + product_type = context["product"]["productType"] asset_name = f"{folder_name}_{name}" if folder_name else name container_name = f"{folder_name}_{name}_CON" From a94081e820f562dd7574607a4ce4e24bada666a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:39:00 +0100 Subject: [PATCH 183/284] Improve label and window title --- client/ayon_core/hosts/fusion/api/lib.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index cfb77a3652..4fdc8c6856 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -349,8 +349,7 @@ def prompt_reset_context(): label=( "You are saving your scene into a different task." "\n\n" - "Would you like to reset some settings for the " - "for the new context?\n" + "Would you like to update some settings to the new context?\n" ) ), BoolDef( @@ -384,7 +383,7 @@ def prompt_reset_context(): dialog.setWindowFlags( dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setWindowTitle("Saving to different context.") dialog.setStyleSheet(load_stylesheet()) if not dialog.exec_(): return None From eadfc1542ba65afc711b96c35c3e8a8be322c8ed Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 22:42:29 +0100 Subject: [PATCH 184/284] Update families data key to use the correct value from the data dictionary. This change ensures accurate information is passed for processing. --- client/ayon_core/hosts/hiero/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 1115eab9d3..4d228ac3a2 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -906,7 +906,7 @@ class PublishClip: "hierarchyData": hierarchy_formatting_data, "productName": self.product_name, "productType": self.product_type, - "families": [self.product_type, self.data["family"]] + "families": [self.product_type, self.data["productType"]] } def _convert_to_entity(self, src_type, template): From cfbcc32953c1dbf924f3dca6a48d0f830422f65e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:44:18 +0100 Subject: [PATCH 185/284] Improve artist report + add repair --- .../publish/validate_mesh_non_manifold.py | 132 ++++++++++++++++-- 1 file changed, 121 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py index 6dbad538ef..8eee2b754b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py @@ -1,14 +1,99 @@ -from maya import cmds +from maya import cmds, mel import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateMeshOrder, - PublishValidationError, + PublishXmlValidationError, + RepairAction, OptionalPyblishPluginMixin ) +def poly_cleanup(version=4, + meshes=None, + # Version 1 + all_meshes=False, + select_only=False, + history_on=True, + quads=False, + nsided=False, + concave=False, + holed=False, + nonplanar=False, + zeroGeom=False, + zeroGeomTolerance=1e-05, + zeroEdge=False, + zeroEdgeTolerance=1e-05, + zeroMap=False, + zeroMapTolerance=1e-05, + # Version 2 + shared_uvs=False, + non_manifold=False, + # Version 3 + lamina=False, + # Version 4 + invalid_components=False): + """Wrapper around `polyCleanupArgList` mel command""" + + # Get all inputs named as `dict` to easily do conversions and formatting + values = locals() + + # Convert booleans to 1 or 0 + for key in [ + "all_meshes", + "select_only", + "history_on", + "quads", + "nsided", + "concave", + "holed", + "nonplanar", + "zeroGeom", + "zeroEdge", + "zeroMap", + "shared_uvs", + "non_manifold", + "lamina", + "invalid_components", + ]: + values[key] = 1 if values[key] else 0 + + cmd = ( + 'polyCleanupArgList {version} {{ ' + '"{all_meshes}",' # 0: All selectable meshes + '"{select_only}",' # 1: Only perform a selection + '"{history_on}",' # 2: Keep construction history + '"{quads}",' # 3: Check for quads polys + '"{nsided}",' # 4: Check for n-sides polys + '"{concave}",' # 5: Check for concave polys + '"{holed}",' # 6: Check for holed polys + '"{nonplanar}",' # 7: Check for non-planar polys + '"{zeroGeom}",' # 8: Check for 0 area faces + '"{zeroGeomTolerance}",' # 9: Tolerance for face areas + '"{zeroEdge}",' # 10: Check for 0 length edges + '"{zeroEdgeTolerance}",' # 11: Tolerance for edge length + '"{zeroMap}",' # 12: Check for 0 uv face area + '"{zeroMapTolerance}",' # 13: Tolerance for uv face areas + '"{shared_uvs}",' # 14: Unshare uvs that are shared + # across vertices + '"{non_manifold}",' # 15: Check for nonmanifold polys + '"{lamina}",' # 16: Check for lamina polys + '"{invalid_components}"' # 17: Remove invalid components + ' }};'.format(**values) + ) + + mel.eval("source polyCleanupArgList") + if not all_meshes and meshes: + # Allow to specify meshes to run over by selecting them + cmds.select(meshes, replace=True) + mel.eval(cmd) + + +class CleanupMatchingPolygons(RepairAction): + label = "Cleanup matching polygons" + + def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: @@ -29,7 +114,8 @@ class ValidateMeshNonManifold(pyblish.api.Validator, hosts = ['maya'] families = ['model'] label = 'Mesh Non-Manifold Edges/Vertices' - actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, + CleanupMatchingPolygons] optional = True @staticmethod @@ -39,9 +125,11 @@ class ValidateMeshNonManifold(pyblish.api.Validator, invalid = [] for mesh in meshes: - if (cmds.polyInfo(mesh, nonManifoldVertices=True) or - cmds.polyInfo(mesh, nonManifoldEdges=True)): - invalid.append(mesh) + components = cmds.polyInfo(mesh, + nonManifoldVertices=True, + nonManifoldEdges=True) + if components: + invalid.extend(components) return invalid @@ -49,12 +137,34 @@ class ValidateMeshNonManifold(pyblish.api.Validator, """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return + invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - "Meshes found with non-manifold edges/vertices:\n\n{0}".format( - _as_report_list(sorted(invalid)) - ), - title="Non-Manifold Edges/Vertices" + # Report only the meshes instead of all component indices + invalid_meshes = { + component.split(".", 1)[0] for component in invalid + } + invalid_meshes = _as_report_list(sorted(invalid_meshes)) + + raise PublishXmlValidationError( + plugin=self, + message=( + "Meshes found with non-manifold " + "edges/vertices:\n\n{0}".format(invalid_meshes) + ) ) + + @classmethod + def repair(cls, instance): + invalid_components = cls.get_invalid(instance) + if not invalid_components: + cls.log.info("No invalid components found to cleanup.") + return + + invalid_meshes = { + component.split(".", 1)[0] for component in invalid_components + } + poly_cleanup(meshes=list(invalid_meshes), + select_only=True, + non_manifold=True) From b41d5fc0136e0fe0d74f9daef889b7fd35fcb4ae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:46:40 +0100 Subject: [PATCH 186/284] Add XML help --- .../help/validate_mesh_non_manifold.xml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml diff --git a/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml b/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml new file mode 100644 index 0000000000..5aec3009a7 --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml @@ -0,0 +1,33 @@ + + + +Non-Manifold Edges/Vertices +## Non-Manifold Edges/Vertices + +Meshes found with non-manifold edges or vertices. + +### How to repair? + +Run select invalid to select the invalid components. + +You can also try the _cleanup matching polygons_ action which will perform a +cleanup like Maya's `Mesh > Cleanup...` modeling tool. + +It is recommended to always select the invalid to see where the issue is +because if you run any repair on it you will need to double check the topology +is still like you wanted. + + + +### What is non-manifold topology? + +_Non-manifold topology_ polygons have a configuration that cannot be unfolded +into a continuous flat piece, for example: + +- Three or more faces share an edge +- Two or more faces share a single vertex but no edge. +- Adjacent faces have opposite normals + + + + From b2075054914ba4c0f4e51b730a74871be2fdb9f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:57:09 +0100 Subject: [PATCH 187/284] Maya: Validate Animation Out Set Related Node Ids improve validation report message - Ignore nodes without ids - that is validated elsewhere - Only check specific types instead of excluding only locators (so that e.g. also constraints or deformers are excluded in the check) - Improve report message --- ...ate_animation_out_set_related_node_ids.xml | 29 +++++++++++++++ ...date_animation_out_set_related_node_ids.py | 37 +++++++++---------- 2 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml diff --git a/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml b/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml new file mode 100644 index 0000000000..a855dd90a5 --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml @@ -0,0 +1,29 @@ + + + +Shape IDs mismatch original shape +## Shapes mismatch IDs with original shape + +Meshes are detected where the (deformed) mesh has a different `cbId` than +the same mesh in its deformation history. +Theses should normally be the same. + +### How to repair? + +By using the repair action the IDs from the shape in history will be +copied to the deformed shape. For **animation** instances using the +repair action usually is usually the correct fix. + + + +### How does this happen? + +When a deformer is applied in the scene on a referenced mesh that had no +deformers then Maya will create a new shape node for the mesh that +does not have the original id. Then on scene save new ids get created for the +meshes lacking a `cbId` and thus the mesh then has a different `cbId` than +the mesh in the deformation history. + + + + diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index 2502fd74b2..7ecd602662 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -6,7 +6,7 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError, + PublishXmlValidationError, OptionalPyblishPluginMixin, get_plugin_settings, apply_plugin_settings_automatically @@ -56,40 +56,39 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - # TODO: Message formatting can be improved - raise PublishValidationError("Nodes found with mismatching " - "IDs: {0}".format(invalid), - title="Invalid node ids") + + # Use the short names + invalid = cmds.ls(invalid) + invalid.sort() + + # Construct a human-readable list + invalid = "\n".join("- {}".format(node) for node in invalid) + + raise PublishXmlValidationError( + plugin=self, + message=( + "Nodes have different IDs than their input " + "history: \n{0}".format(invalid) + ) + ) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" invalid = [] - types_to_skip = ["locator"] + types = ["mesh", "nurbsCurve", "nurbsSurface"] # get asset id nodes = instance.data.get("out_hierarchy", instance[:]) - for node in nodes: + for node in cmds.ls(nodes, type=types, long=True): # We only check when the node is *not* referenced if cmds.referenceQuery(node, isNodeReferenced=True): continue - # Check if node is a shape as deformers only work on shapes - obj_type = cmds.objectType(node, isAType="shape") - if not obj_type: - continue - - # Skip specific types - if cmds.objectType(node) in types_to_skip: - continue - # Get the current id of the node node_id = lib.get_id(node) - if not node_id: - invalid.append(node) - continue history_id = lib.get_id_from_sibling(node) if history_id is not None and node_id != history_id: From d1cc880968da13e900649ee223df2cef99cbfc8c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 23:23:31 +0100 Subject: [PATCH 188/284] Refactor folder type to lowercase in ShotMetadataSolver. Adjusted parent_token_type to lowercase for consistency. --- client/ayon_core/hosts/traypublisher/api/editorial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/api/editorial.py b/client/ayon_core/hosts/traypublisher/api/editorial.py index 92a7b315f8..09a2ab17ac 100644 --- a/client/ayon_core/hosts/traypublisher/api/editorial.py +++ b/client/ayon_core/hosts/traypublisher/api/editorial.py @@ -186,7 +186,7 @@ class ShotMetadataSolver: # in case first parent is project then start parents from start if ( _index == 0 - and parent_token_type == "project" + and parent_token_type == "Project" ): project_parent = parents[0] parents = [project_parent] @@ -194,7 +194,7 @@ class ShotMetadataSolver: parents.append({ "entity_type": "folder", - "folder_type": parent_token_type, + "folder_type": parent_token_type.lower(), "entity_name": parent_name }) From 74d64f9dcbfdf7ce82993bf822acc0972f5636dc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 23:55:20 +0100 Subject: [PATCH 189/284] Raise PublishValidationError and improve error message --- .../publish/validate_shape_render_stats.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py index 2783a6dbe8..f9fadb4a3e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py @@ -6,6 +6,7 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, + PublishValidationError, OptionalPyblishPluginMixin ) @@ -20,7 +21,6 @@ class ValidateShapeRenderStats(pyblish.api.Validator, label = 'Shape Default Render Stats' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] - optional = True defaults = {'castsShadows': 1, 'receiveShadows': 1, @@ -37,14 +37,13 @@ class ValidateShapeRenderStats(pyblish.api.Validator, # It seems the "surfaceShape" and those derived from it have # `renderStat` attributes. shapes = cmds.ls(instance, long=True, type='surfaceShape') - invalid = [] + invalid = set() for shape in shapes: - _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) - for attr, default_value in _iteritems(): + for attr, default_value in cls.defaults.items(): if cmds.attributeQuery(attr, node=shape, exists=True): value = cmds.getAttr('{}.{}'.format(shape, attr)) if value != default_value: - invalid.append(shape) + invalid.add(shape) return invalid @@ -52,17 +51,36 @@ class ValidateShapeRenderStats(pyblish.api.Validator, if not self.is_active(instance.data): return invalid = self.get_invalid(instance) + if not invalid: + return - if invalid: - raise ValueError("Shapes with non-default renderStats " - "found: {0}".format(invalid)) + defaults_str = "\n".join( + "- {}: {}\n".format(key, value) + for key, value in self.defaults.items() + ) + description = ( + "## Shape Default Render Stats\n" + "Shapes are detected with non-default render stats.\n\n" + "To ensure a model's shapes behave like a shape would by default " + "we require the render stats to have not been altered in " + "the published models.\n\n" + "### How to repair?\n" + "You can reset the default values on the shapes by using the " + "repair action." + ) + + raise PublishValidationError( + "Shapes with non-default renderStats " + "found: {0}".format(", ".join(sorted(invalid))), + description=description, + detail="The expected default values " + "are:\n\n{}".format(defaults_str) + ) @classmethod def repair(cls, instance): for shape in cls.get_invalid(instance): - _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) - for attr, default_value in _iteritems(): - + for attr, default_value in cls.defaults.items(): if cmds.attributeQuery(attr, node=shape, exists=True): plug = '{0}.{1}'.format(shape, attr) value = cmds.getAttr(plug) From 6c9794c78926bbff169de1d38f30ef36223a83b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:03:51 +0100 Subject: [PATCH 190/284] Raise PublishValidationError --- .../plugins/publish/validate_yeti_renderscript_callbacks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py index 35b2443718..086cb7b1f5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py @@ -3,9 +3,11 @@ from maya import cmds import pyblish.api from ayon_core.pipeline.publish import ( ValidateContentsOrder, + PublishValidationError, OptionalPyblishPluginMixin ) + class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Check if the render script callbacks will be used during the rendering @@ -45,8 +47,8 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin, return invalid = self.get_invalid(instance) if invalid: - raise ValueError("Invalid render callbacks found for '%s'!" - % instance.name) + raise PublishValidationError( + f"Invalid render callbacks found for '{instance.name}'.") @classmethod def get_invalid(cls, instance): From b940cabfaf96b3e79d9211b398a5ba76599299c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:07:19 +0100 Subject: [PATCH 191/284] Change more validations to PublishValidationError --- .../maya/plugins/publish/validate_instance_subset.py | 6 +++--- .../hosts/maya/plugins/publish/validate_setdress_root.py | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instance_subset.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instance_subset.py index da3a194e58..df9ca0bf13 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instance_subset.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_instance_subset.py @@ -36,18 +36,18 @@ class ValidateSubsetName(pyblish.api.InstancePlugin): ) if not isinstance(product_name, six.string_types): - raise TypeError(( + raise PublishValidationError(( "Instance product name must be string, got: {0} ({1})" ).format(product_name, type(product_name))) # Ensure is not empty product if not product_name: - raise ValueError( + raise PublishValidationError( "Instance product name is empty: {0}".format(product_name) ) # Validate product characters if not validate_name(product_name): - raise ValueError(( + raise PublishValidationError(( "Instance product name contains invalid characters: {0}" ).format(product_name)) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_setdress_root.py b/client/ayon_core/hosts/maya/plugins/publish/validate_setdress_root.py index 906f6fbd1a..f88e33fdfb 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_setdress_root.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_setdress_root.py @@ -1,5 +1,8 @@ import pyblish.api -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateSetdressRoot(pyblish.api.InstancePlugin): @@ -20,4 +23,6 @@ class ValidateSetdressRoot(pyblish.api.InstancePlugin): root = cmds.ls(set_member, assemblies=True, long=True) if not root or root[0] not in set_member: - raise Exception("Setdress top root node is not being published.") + raise PublishValidationError( + "Setdress top root node is not being published." + ) From db81bb1ad46d3fe8cc5f1fade48aee246f24db05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:10:59 +0100 Subject: [PATCH 192/284] Change wording for clarity --- client/ayon_core/hosts/fusion/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index 4fdc8c6856..ba650cc73f 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -347,7 +347,7 @@ def prompt_reset_context(): definitions = [ UILabelDef( label=( - "You are saving your scene into a different task." + "You are saving your workfile into a different folder or task." "\n\n" "Would you like to update some settings to the new context?\n" ) From 5a413faf277ed2ca72457491c5fb76394d85ca35 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:14:02 +0100 Subject: [PATCH 193/284] Tweak dialog wording to match #259 --- client/ayon_core/hosts/maya/api/lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 4e9410af41..9f36193413 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2699,10 +2699,9 @@ def prompt_reset_context(): definitions = [ UILabelDef( label=( - "You are saving your scene into a different task." + "You are saving your workfile into a different folder or task." "\n\n" - "Would you like to reset some settings for the " - "for the new context?\n" + "Would you like to update some settings to the new context?\n" ) ), BoolDef( @@ -2739,7 +2738,7 @@ def prompt_reset_context(): ] dialog = AttributeDefinitionsDialog(definitions) - dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setWindowTitle("Saving to different context.") dialog.setStyleSheet(load_stylesheet()) if not dialog.exec_(): return None From 5aff2a6fc1b79c99f21bbbfe33b36a9abbaf603c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:14:53 +0100 Subject: [PATCH 194/284] Tweak dialog wording to match #259 --- client/ayon_core/hosts/houdini/api/lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 6a314d097d..e509e2c166 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -1113,10 +1113,9 @@ def prompt_reset_context(): definitions = [ UILabelDef( label=( - "You are saving your scene into a different task." + "You are saving your workfile into a different folder or task." "\n\n" - "Would you like to reset some settings for the " - "for the new context?\n" + "Would you like to update some settings to the new context?\n" ) ), BoolDef( @@ -1141,7 +1140,7 @@ def prompt_reset_context(): ] dialog = AttributeDefinitionsDialog(definitions) - dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setWindowTitle("Saving to different context.") dialog.setStyleSheet(load_stylesheet()) if not dialog.exec_(): return None From c152e459da51638ab0f11434ce7627cdea5735e8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:23:04 +0100 Subject: [PATCH 195/284] Change assert to `PublishValidationError` --- .../plugins/publish/validate_single_assembly.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_single_assembly.py b/client/ayon_core/hosts/maya/plugins/publish/validate_single_assembly.py index 1987f93e32..f5d73553d3 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_single_assembly.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_single_assembly.py @@ -1,5 +1,8 @@ import pyblish.api -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateSingleAssembly(pyblish.api.InstancePlugin): @@ -30,7 +33,11 @@ class ValidateSingleAssembly(pyblish.api.InstancePlugin): # ensure unique (somehow `maya.cmds.ls` doesn't manage that) assemblies = set(assemblies) - assert len(assemblies) > 0, ( - "One assembly required for: %s (currently empty?)" % instance) - assert len(assemblies) < 2, ( - 'Multiple assemblies found: %s' % assemblies) + if len(assemblies) == 0: + raise PublishValidationError( + "One assembly required for: %s (currently empty?)" % instance + ) + elif len(assemblies) > 1: + raise PublishValidationError( + 'Multiple assemblies found: %s' % assemblies + ) From 38d4a3231a8cecd43cb2044afe2161755ccbd7f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:23:46 +0100 Subject: [PATCH 196/284] Change assert to `PublishValidationError` --- .../publish/validate_unreal_mesh_triangulated.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index 101bd5bf04..6440c00eae 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -5,7 +5,8 @@ import pyblish.api from ayon_core.pipeline.publish import ( ValidateMeshOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) import ayon_core.hosts.maya.api.action @@ -26,8 +27,8 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin, invalid = [] meshes = cmds.ls(instance, type="mesh", long=True) for mesh in meshes: - faces = cmds.polyEvaluate(mesh, f=True) - tris = cmds.polyEvaluate(mesh, t=True) + faces = cmds.polyEvaluate(mesh, face=True) + tris = cmds.polyEvaluate(mesh, triangle=True) if faces != tris: invalid.append(mesh) @@ -37,5 +38,5 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return invalid = self.get_invalid(instance) - assert len(invalid) == 0, ( - "Found meshes without triangles") + if invalid: + raise PublishValidationError("Found meshes without triangles") From 11e0df6a2af38941ad61c4488bafe2b4736faecf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:24:15 +0100 Subject: [PATCH 197/284] Change assert to `PublishValidationError` --- .../maya/plugins/publish/validate_unreal_up_axis.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_up_axis.py index ef7296e628..f7acd41cea 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -6,7 +6,8 @@ import pyblish.api from ayon_core.pipeline.publish import ( ValidateContentsOrder, RepairAction, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError ) @@ -26,9 +27,10 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin, if not self.is_active(context.data): return - assert cmds.upAxis(q=True, axis=True) == "z", ( - "Invalid axis set as up axis" - ) + if cmds.upAxis(q=True, axis=True) != "z": + raise PublishValidationError( + "Invalid axis set as up axis" + ) @classmethod def repair(cls, instance): From 2b175b806ea51da2d371f7dda5212b0e8c891d37 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:25:08 +0100 Subject: [PATCH 198/284] Cosmetics --- .../hosts/maya/plugins/publish/validate_visible_only.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py b/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py index af6c9a64c6..1fdb476dba 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py @@ -34,8 +34,9 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: start, end = self.get_frame_range(instance) - raise PublishValidationError("No visible nodes found in " - "frame range {}-{}.".format(start, end)) + raise PublishValidationError( + f"No visible nodes found in frame range {start}-{end}." + ) @classmethod def get_invalid(cls, instance): From 5fd209970c1ea8000474f1063de89ee0eb5f7cf8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:26:36 +0100 Subject: [PATCH 199/284] Change assert to `KnownPublishError` --- .../publish/validate_vray_distributed_rendering.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py index b35508d635..b3978b8483 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py @@ -3,6 +3,7 @@ from maya import cmds from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( + KnownPublishError, PublishValidationError, RepairAction, ValidateContentsOrder, @@ -35,11 +36,14 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return if instance.data.get("renderer") != "vray": - # If not V-Ray ignore.. + # If not V-Ray, ignore return vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode") - assert vray_settings, "Please ensure a VRay Settings Node is present" + if not vray_settings: + raise KnownPublishError( + "Please ensure a VRay Settings Node is present" + ) renderlayer = instance.data['renderlayer'] @@ -51,8 +55,8 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin, # during batch mode we invalidate the instance if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer): raise PublishValidationError( - ("Renderlayer has distributed rendering enabled " - "but is not set to ignore in batch mode.")) + "Renderlayer has distributed rendering enabled " + "but is not set to ignore in batch mode.") @classmethod def repair(cls, instance): From 3d3ef75a0f3c511dc8e5a63f0c34c169edb9ea2e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:29:58 +0100 Subject: [PATCH 200/284] Add docstring as description --- .../maya/plugins/publish/validate_yeti_rig_cache_state.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py index d81534192a..84614fc0be 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py @@ -1,3 +1,5 @@ +import inspect + import pyblish.api import maya.cmds as cmds import ayon_core.hosts.maya.api.action @@ -8,7 +10,6 @@ from ayon_core.pipeline.publish import ( ) - class ValidateYetiRigCacheState(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the I/O attributes of the node @@ -32,7 +33,10 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin, return invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Nodes have incorrect I/O settings") + raise PublishValidationError( + "Nodes have incorrect I/O settings", + description=inspect.getdoc(self) + ) @classmethod def get_invalid(cls, instance): From 3d38ddef9fb5acafb699d41a768ce1f39fc8d65a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:32:31 +0100 Subject: [PATCH 201/284] Fix bullet point list formatting - In HTML mode the `-` doesn't get converted into nice bullet point list --- .../maya/plugins/publish/validate_transform_zero.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index 1cbdd05b0b..e003e9018f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -69,14 +69,13 @@ class ValidateTransformZero(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - - names = "
".join( - " - {}".format(node) for node in invalid + names = "\n".join( + "- {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", - message="The model publish allows no transformations. You must" - " freeze transformations to continue.

" - "Nodes found with transform values: " + message="The model publish allows no transformations. " + "You must **freeze transformations** to continue.\n\n" + "Nodes found with transform values:\n" "{0}".format(names)) From 6d741751945227e51a3f92c3336b96786ef271e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:41:08 +0100 Subject: [PATCH 202/284] Improve validation report --- .../publish/validate_transform_zero.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index e003e9018f..0814b4525b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -1,5 +1,6 @@ -from maya import cmds +import inspect +from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action @@ -57,7 +58,7 @@ class ValidateTransformZero(pyblish.api.Validator, if ('_LOC' in transform) or ('_loc' in transform): continue mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True) - if not all(abs(x-y) < cls._tolerance + if not all(abs(x - y) < cls._tolerance for x, y in zip(cls._identity, mat)): invalid.append(transform) @@ -69,13 +70,24 @@ class ValidateTransformZero(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - names = "\n".join( - "- {}".format(node) for node in invalid + names = "
".join( + " - {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", - message="The model publish allows no transformations. " - "You must **freeze transformations** to continue.\n\n" - "Nodes found with transform values:\n" + description=self.get_description(), + message="The model publish allows no transformations. You must" + " freeze transformations to continue.

" + "Nodes found with transform values:
" "{0}".format(names)) + + @staticmethod + def get_description(): + return inspect.cleandoc("""### Transform can't have any values + + The model publish allows no transformations. + + You must **freeze transformations** to continue. + + """) From d69ba8395c4898ddfdda157b5c436a3d10139db9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:50:40 +0100 Subject: [PATCH 203/284] Fix initial state for `CachedData.remapping` --- client/ayon_core/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 034c90d27b..efa3bbf968 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -23,7 +23,7 @@ log = Logger.get_logger(__name__) class CachedData: - remapping = None + remapping = {} has_compatible_ocio_package = None config_version_data = {} ocio_config_colorspaces = {} From f6cf5e06514ff347ce03c323dc5fcc6b655ffbae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 01:06:52 +0100 Subject: [PATCH 204/284] Remove unused import --- client/ayon_core/hosts/houdini/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 673183a15d..b395a06dc5 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -3,7 +3,6 @@ import sys import os import errno import re -import uuid import logging import json from contextlib import contextmanager From 0b36cbc5ec65e995ae6f82ee0044fce37968bada Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 09:15:15 +0100 Subject: [PATCH 205/284] Remove deprecated `AVALON_ACTIONS` --- client/ayon_core/modules/launcher_action.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py index 1faf6ef4b1..38e88d36ca 100644 --- a/client/ayon_core/modules/launcher_action.py +++ b/client/ayon_core/modules/launcher_action.py @@ -37,20 +37,6 @@ class LauncherAction(AYONAddon, ITrayAction): if path and os.path.exists(path): register_launcher_action_path(path) - paths_str = os.environ.get("AVALON_ACTIONS") or "" - if paths_str: - self.log.warning( - "WARNING: 'AVALON_ACTIONS' is deprecated. Support of this" - " environment variable will be removed in future versions." - " Please consider using 'OpenPypeModule' to define custom" - " action paths. Planned version to drop the support" - " is 3.17.2 or 3.18.0 ." - ) - - for path in paths_str.split(os.pathsep): - if path and os.path.exists(path): - register_launcher_action_path(path) - def on_action_trigger(self): """Implementation for ITrayAction interface. From 908e17430cb645ca6d2773b294cc02ea387063d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 11:46:07 +0100 Subject: [PATCH 206/284] fix typos in codebase --- client/ayon_core/addon/base.py | 4 ++-- client/ayon_core/hosts/blender/api/ops.py | 2 +- .../ayon_core/hosts/blender/plugins/load/load_blend.py | 2 +- .../blender/plugins/publish/validate_deadline_publish.py | 2 +- .../hosts/celaction/hooks/pre_celaction_setup.py | 2 +- .../celaction/plugins/publish/collect_render_path.py | 2 +- client/ayon_core/hosts/flame/api/lib.py | 4 ++-- client/ayon_core/hosts/flame/api/plugin.py | 2 +- client/ayon_core/hosts/flame/otio/utils.py | 2 +- .../hosts/flame/plugins/create/create_shot_clip.py | 4 ++-- .../flame/plugins/publish/collect_timeline_instances.py | 2 +- .../startup/openpype_babypublisher/modules/ftrack_lib.py | 2 +- client/ayon_core/hosts/harmony/api/lib.py | 2 +- client/ayon_core/hosts/hiero/api/lib.py | 8 ++++---- client/ayon_core/hosts/hiero/api/otio/hiero_import.py | 2 +- client/ayon_core/hosts/hiero/api/otio/utils.py | 2 +- .../startup/Python/StartupUI/otioimporter/OTIOImport.py | 2 +- .../hosts/hiero/plugins/create/create_shot_clip.py | 4 ++-- .../hosts/hiero/plugins/publish/precollect_instances.py | 8 ++++---- client/ayon_core/hosts/houdini/api/colorspace.py | 2 +- client/ayon_core/hosts/maya/api/lib.py | 6 +++--- .../hosts/maya/plugins/publish/collect_render.py | 2 +- .../hosts/maya/plugins/publish/validate_xgen.py | 2 +- client/ayon_core/hosts/nuke/api/lib.py | 4 ++-- client/ayon_core/hosts/nuke/api/plugin.py | 4 ++-- .../photoshop/plugins/load/load_image_from_sequence.py | 2 +- client/ayon_core/hosts/resolve/api/lib.py | 2 +- client/ayon_core/hosts/resolve/otio/utils.py | 2 +- .../hosts/resolve/plugins/create/create_shot_clip.py | 4 ++-- .../plugins/publish/validate_frame_ranges.py | 2 +- client/ayon_core/hosts/unreal/api/tools_ui.py | 2 +- client/ayon_core/lib/local_settings.py | 2 +- client/ayon_core/lib/path_templates.py | 2 +- .../plugins/publish/create_publish_royalrender_job.py | 2 +- client/ayon_core/pipeline/create/context.py | 2 +- client/ayon_core/pipeline/create/creator_plugins.py | 9 +++++---- client/ayon_core/pipeline/editorial.py | 2 +- client/ayon_core/pipeline/load/plugins.py | 2 +- .../pipeline/workfile/workfile_template_builder.py | 2 +- .../plugins/publish/extract_otio_audio_tracks.py | 2 +- client/ayon_core/tools/common_models/thumbnails.py | 2 +- client/ayon_core/tools/pyblish_pype/util.py | 2 +- server/settings/publish_plugins.py | 2 +- 43 files changed, 62 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 42b53c59e3..6bac25b8ac 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -1075,7 +1075,7 @@ class AddonsManager: """Print out report of time spent on addons initialization parts. Reporting is not automated must be implemented for each initialization - part separatelly. Reports must be stored to `_report` attribute. + part separately. Reports must be stored to `_report` attribute. Print is skipped if `_report` is empty. Attribute `_report` is dictionary where key is "label" describing @@ -1267,7 +1267,7 @@ class TrayAddonsManager(AddonsManager): def add_doubleclick_callback(self, addon, callback): """Register doubleclick callbacks on tray icon. - Currently there is no way how to determine which is launched. Name of + Currently, there is no way how to determine which is launched. Name of callback can be defined with `doubleclick_callback` attribute. Missing feature how to define default callback. diff --git a/client/ayon_core/hosts/blender/api/ops.py b/client/ayon_core/hosts/blender/api/ops.py index d71ee6faf5..c03ec98d0c 100644 --- a/client/ayon_core/hosts/blender/api/ops.py +++ b/client/ayon_core/hosts/blender/api/ops.py @@ -191,7 +191,7 @@ def _process_app_events() -> Optional[float]: class LaunchQtApp(bpy.types.Operator): - """A Base class for opertors to launch a Qt app.""" + """A Base class for operators to launch a Qt app.""" _app: QtWidgets.QApplication _window = Union[QtWidgets.QDialog, ModuleType] diff --git a/client/ayon_core/hosts/blender/plugins/load/load_blend.py b/client/ayon_core/hosts/blender/plugins/load/load_blend.py index e84dddc88f..1984193a30 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_blend.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_blend.py @@ -227,7 +227,7 @@ class BlendLoader(plugin.AssetLoader): obj.animation_data_create() obj.animation_data.action = actions[obj.name] - # Restore the old data, but reset memebers, as they don't exist anymore + # Restore the old data, but reset members, as they don't exist anymore # This avoids a crash, because the memory addresses of those members # are not valid anymore old_data["members"] = [] diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_deadline_publish.py b/client/ayon_core/hosts/blender/plugins/publish/validate_deadline_publish.py index b37db44cd4..a86e73ba81 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -32,7 +32,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, tree = bpy.context.scene.node_tree output_type = "CompositorNodeOutputFile" output_node = None - # Remove all output nodes that inlcude "AYON" in the name. + # Remove all output nodes that include "AYON" in the name. # There should be only one. for node in tree.nodes: if node.bl_idname == output_type and "AYON" in node.name: diff --git a/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py b/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py index 73b368e4e3..d94fff8f2b 100644 --- a/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py +++ b/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py @@ -118,7 +118,7 @@ class CelactionPrelaunchHook(PreLaunchHook): def workfile_path(self): workfile_path = self.data["last_workfile_path"] - # copy workfile from template if doesnt exist any on path + # copy workfile from template if doesn't exist any on path if not os.path.exists(workfile_path): # TODO add ability to set different template workfile path via # settings diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py index 52bb183663..1bb4d54831 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py @@ -38,7 +38,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): render_path = r_template_item["path"].format_strict(anatomy_data) self.log.debug("__ render_path: `{}`".format(render_path)) - # create dir if it doesnt exists + # create dir if it doesn't exists try: if not os.path.isdir(render_dir): os.makedirs(render_dir, exist_ok=True) diff --git a/client/ayon_core/hosts/flame/api/lib.py b/client/ayon_core/hosts/flame/api/lib.py index efa23fe01e..e1316658bf 100644 --- a/client/ayon_core/hosts/flame/api/lib.py +++ b/client/ayon_core/hosts/flame/api/lib.py @@ -615,7 +615,7 @@ def get_reformated_filename(filename, padded=True): filename (str): file name Returns: - type: string with reformated path + type: string with reformatted path Example: get_reformated_filename("plate.1001.exr") > plate.%04d.exr @@ -980,7 +980,7 @@ class MediaInfoFile(object): @property def file_pattern(self): - """Clips file patter + """Clips file pattern. Returns: str: file pattern. ex. file.[1-2].exr diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/client/ayon_core/hosts/flame/api/plugin.py index c57d021c69..35fe1b351d 100644 --- a/client/ayon_core/hosts/flame/api/plugin.py +++ b/client/ayon_core/hosts/flame/api/plugin.py @@ -1018,7 +1018,7 @@ class OpenClipSolver(flib.MediaInfoFile): self.feed_version_name)) else: self.log.debug("adding new track element ..") - # create new track as it doesnt exists yet + # create new track as it doesn't exist yet # set current version to feeds on tmp tmp_xml_feeds = tmp_xml_track.find('feeds') tmp_xml_feeds.set('currentVersion', self.feed_version_name) diff --git a/client/ayon_core/hosts/flame/otio/utils.py b/client/ayon_core/hosts/flame/otio/utils.py index 7ded8e55d8..a1206b6710 100644 --- a/client/ayon_core/hosts/flame/otio/utils.py +++ b/client/ayon_core/hosts/flame/otio/utils.py @@ -29,7 +29,7 @@ def get_reformated_filename(filename, padded=True): filename (str): file name Returns: - type: string with reformated path + type: string with reformatted path Example: get_reformated_filename("plate.1001.exr") > plate.%04d.exr diff --git a/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py index e8eb2b9fab..56f5319f21 100644 --- a/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py @@ -17,7 +17,7 @@ class CreateShotClip(opfapi.Creator): presets = deepcopy(self.presets) gui_inputs = self.get_gui_inputs() - # get key pares from presets and match it on ui inputs + # get key pairs from presets and match it on ui inputs for k, v in gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed @@ -236,7 +236,7 @@ class CreateShotClip(opfapi.Creator): "type": "QCheckBox", "label": "Source resolution", "target": "tag", - "toolTip": "Is resloution taken from timeline or source?", # noqa + "toolTip": "Is resolution taken from timeline or source?", # noqa "order": 4}, } }, diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py index 9d6560023c..c0dea0b891 100644 --- a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -37,7 +37,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): self.otio_timeline = context.data["otioTimeline"] self.fps = context.data["fps"] - # process all sellected + # process all selected for segment in selected_segments: # get openpype tag data marker_data = opfapi.get_segment_data_marker(segment) diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py index 0e84a5ef52..a66980493e 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py +++ b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py @@ -396,7 +396,7 @@ class FtrackEntityOperator: entity = session.query(query).first() - # if entity doesnt exist then create one + # if entity doesn't exist then create one if not entity: entity = self.create_ftrack_entity( session, diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index 3c833c7b69..f9980cb65e 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -568,7 +568,7 @@ def save_scene(): """Save the Harmony scene safely. The built-in (to Avalon) background zip and moving of the Harmony scene - folder, interfers with server/client communication by sending two requests + folder, interferes with server/client communication by sending two requests at the same time. This only happens when sending "scene.saveAll()". This method prevents this double request and safely saves the scene. diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index 8e08e8cbf3..ecb3460fb4 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -166,7 +166,7 @@ def get_current_track(sequence, name, audio=False): Creates new if none is found. Args: - sequence (hiero.core.Sequence): hiero sequene object + sequence (hiero.core.Sequence): hiero sequence object name (str): name of track we want to return audio (bool)[optional]: switch to AudioTrack @@ -846,8 +846,8 @@ def create_nuke_workfile_clips(nuke_workfiles, seq=None): [{ 'path': 'P:/Jakub_testy_pipeline/test_v01.nk', 'name': 'test', - 'handleStart': 15, # added asymetrically to handles - 'handleEnd': 10, # added asymetrically to handles + 'handleStart': 15, # added asymmetrically to handles + 'handleEnd': 10, # added asymmetrically to handles "clipIn": 16, "frameStart": 991, "frameEnd": 1023, @@ -1192,7 +1192,7 @@ def get_sequence_pattern_and_padding(file): Return: string: any matching sequence pattern - int: padding of sequnce numbering + int: padding of sequence numbering """ foundall = re.findall( r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) diff --git a/client/ayon_core/hosts/hiero/api/otio/hiero_import.py b/client/ayon_core/hosts/hiero/api/otio/hiero_import.py index 257c434011..f123b81ca6 100644 --- a/client/ayon_core/hosts/hiero/api/otio/hiero_import.py +++ b/client/ayon_core/hosts/hiero/api/otio/hiero_import.py @@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track): if isinstance(track, hiero.core.AudioTrack): kind = 'Audio' - # Gather TrackItems involved in trasition + # Gather TrackItems involved in transition item_in, item_out = get_neighboring_trackitems( otio_item, otio_track, diff --git a/client/ayon_core/hosts/hiero/api/otio/utils.py b/client/ayon_core/hosts/hiero/api/otio/utils.py index 4c5d46bd51..f7cb58f1e8 100644 --- a/client/ayon_core/hosts/hiero/api/otio/utils.py +++ b/client/ayon_core/hosts/hiero/api/otio/utils.py @@ -25,7 +25,7 @@ def get_reformated_path(path, padded=True): path (str): path url or simple file name Returns: - type: string with reformated path + type: string with reformatted path Example: get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index 17c044f3ec..8331c429df 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track): kind = "Audio" try: - # Gather TrackItems involved in trasition + # Gather TrackItems involved in transition item_in, item_out = get_neighboring_trackitems( otio_item, otio_track, diff --git a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py index 62e7041286..2985a81317 100644 --- a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py @@ -166,7 +166,7 @@ class CreateShotClip(phiero.Creator): "type": "QCheckBox", "label": "Source resolution", "target": "tag", - "toolTip": "Is resloution taken from timeline or source?", # noqa + "toolTip": "Is resolution taken from timeline or source?", # noqa "order": 4}, } }, @@ -211,7 +211,7 @@ class CreateShotClip(phiero.Creator): presets = deepcopy(self.presets) gui_inputs = deepcopy(self.gui_inputs) - # get key pares from presets and match it on ui inputs + # get key pairs from presets and match it on ui inputs for k, v in gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed diff --git a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py index d921f37934..26f6968884 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py @@ -43,7 +43,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): tracks_effect_items = self.collect_sub_track_items(all_tracks) context.data["tracksEffectItems"] = tracks_effect_items - # process all sellected timeline track items + # process all selected timeline track items for track_item in selected_timeline_items: data = {} clip_name = track_item.name() @@ -62,7 +62,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }: continue - # get clips subtracks and anotations + # get clips subtracks and annotations annotations = self.clip_annotations(source_clip) subtracks = self.clip_subtrack(track_item) self.log.debug("Annotations: {}".format(annotations)) @@ -439,10 +439,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for item in subTrackItems: if "TimeWarp" in item.name(): continue - # avoid all anotation + # avoid all annotation if isinstance(item, hiero.core.Annotation): continue - # # avoid all not anaibled + # avoid all disabled if not item.isEnabled(): continue subtracks.append(item) diff --git a/client/ayon_core/hosts/houdini/api/colorspace.py b/client/ayon_core/hosts/houdini/api/colorspace.py index 66581d6f20..6a92c77e49 100644 --- a/client/ayon_core/hosts/houdini/api/colorspace.py +++ b/client/ayon_core/hosts/houdini/api/colorspace.py @@ -59,7 +59,7 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): """Returns the colorspace attribute of the default (display, view) pair. - It's used for 'ociocolorspace' parm in OpenGL Node.""" + It's used for 'ociocolorspace' param in OpenGL Node.""" prefs = get_color_management_preferences() return get_display_view_colorspace_name( diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 8ca898f621..cad5b0405f 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -131,7 +131,7 @@ def get_main_window(): def suspended_refresh(suspend=True): """Suspend viewport refreshes - cmds.ogs(pause=True) is a toggle so we cant pass False. + cmds.ogs(pause=True) is a toggle so we can't pass False. """ if IS_HEADLESS: yield @@ -583,7 +583,7 @@ def pairwise(iterable): def collect_animation_defs(fps=False): - """Get the basic animation attribute defintions for the publisher. + """Get the basic animation attribute definitions for the publisher. Returns: OrderedDict @@ -3834,7 +3834,7 @@ def get_color_management_output_transform(): def image_info(file_path): # type: (str) -> dict - """Based on tha texture path, get its bit depth and format information. + """Based on the texture path, get its bit depth and format information. Take reference from makeTx.py in Arnold: ImageInfo(filename): Get Image Information for colorspace AiTextureGetFormat(filename): Get Texture Format diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py index 13eb8fd49e..c981c37123 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py @@ -314,7 +314,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin): if not extend_frames: instance.data["overrideExistingFrame"] = False - # Update the instace + # Update the instance instance.data.update(data) @staticmethod diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/validate_xgen.py index e2c006be9f..7e0f01c482 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_xgen.py @@ -34,7 +34,7 @@ class ValidateXgen(pyblish.api.InstancePlugin): " Node type found: {}".format(node_type) ) - # Cant have inactive modifiers in collection cause Xgen will try and + # Can't have inactive modifiers in collection cause Xgen will try and # look for them when loading. palette = instance.data["xgmPalette"].replace("|", "") inactive_modifiers = {} diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index 1bb0ff79e0..deeab47885 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -814,7 +814,7 @@ def on_script_load(): def check_inventory_versions(): """ - Actual version idetifier of Loaded containers + Actual version identifier of Loaded containers Any time this function is run it will check all nodes and filter only Loader nodes for its version. It will get all versions from database @@ -2381,7 +2381,7 @@ def launch_workfiles_app(): Context.workfiles_launched = True - # get all imortant settings + # get all important settings open_at_start = env_value_to_bool( env_key="AYON_WORKFILE_TOOL_ON_START", default=None) diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/client/ayon_core/hosts/nuke/api/plugin.py index 7f016d9c66..650b67dd2c 100644 --- a/client/ayon_core/hosts/nuke/api/plugin.py +++ b/client/ayon_core/hosts/nuke/api/plugin.py @@ -910,7 +910,7 @@ class ExporterReviewMov(ExporterReview): self._connect_to_above_nodes( node, product_name, "Reposition node... `{}`" ) - # append reformated tag + # append reformatted tag add_tags.append("reformated") # only create colorspace baking if toggled on @@ -1114,7 +1114,7 @@ def convert_to_valid_instaces(): transfer_data["active"] = ( node["publish"].value()) - # add idetifier + # add identifier transfer_data["creator_identifier"] = product_type_to_identifier( product_type ) diff --git a/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py b/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py index 25b22f53a4..73e8c3683c 100644 --- a/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -19,7 +19,7 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): This loader will be triggered multiple times, but selected name will match only to proper path. - Loader doesnt do containerization as there is currently no data model + Loader doesn't do containerization as there is currently no data model of 'frame of rendered files' (only rendered sequence), update would be difficult. """ diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index a60f3cd4ec..b9ad81c79d 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -925,7 +925,7 @@ def get_reformated_path(path, padded=False, first=False): path (str): path url or simple file name Returns: - type: string with reformated path + type: string with reformatted path Example: get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr diff --git a/client/ayon_core/hosts/resolve/otio/utils.py b/client/ayon_core/hosts/resolve/otio/utils.py index 7d8089e055..c03305ff23 100644 --- a/client/ayon_core/hosts/resolve/otio/utils.py +++ b/client/ayon_core/hosts/resolve/otio/utils.py @@ -25,7 +25,7 @@ def get_reformated_path(path, padded=True, first=False): path (str): path url or simple file name Returns: - type: string with reformated path + type: string with reformatted path Example: get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index 3a2a0345ea..cbc03da3b6 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -166,7 +166,7 @@ class CreateShotClip(plugin.Creator): "type": "QCheckBox", "label": "Source resolution", "target": "tag", - "toolTip": "Is resloution taken from timeline or source?", # noqa + "toolTip": "Is resolution taken from timeline or source?", # noqa "order": 4}, } }, @@ -207,7 +207,7 @@ class CreateShotClip(plugin.Creator): presets = None def process(self): - # get key pares from presets and match it on ui inputs + # get key pairs from presets and match it on ui inputs for k, v in self.gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index e5bf034d00..4f11571efe 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -20,7 +20,7 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, optional = True # published data might be sequence (.mov, .mp4) in that counting files - # doesnt make sense + # doesn't make sense check_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", "gif", "svg"] skip_timelines_check = [] # skip for specific task names (regex) diff --git a/client/ayon_core/hosts/unreal/api/tools_ui.py b/client/ayon_core/hosts/unreal/api/tools_ui.py index 084da9a0f0..efae5bb702 100644 --- a/client/ayon_core/hosts/unreal/api/tools_ui.py +++ b/client/ayon_core/hosts/unreal/api/tools_ui.py @@ -125,7 +125,7 @@ class WindowCache: @classmethod def _before_show(cls): - """Create QApplication if does not exists yet.""" + """Create QApplication if does not exist yet.""" if not cls._first_show: return diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 9eba3d1ed1..fd255c997f 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -524,7 +524,7 @@ def get_ayon_appdirs(*args): def get_local_site_id(): """Get local site identifier. - Identifier is created if does not exists yet. + Identifier is created if does not exist yet. """ # used for background syncing site_id = os.environ.get("AYON_SITE_ID") diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 09d11ea1de..a766dbd9c1 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -102,7 +102,7 @@ class StringTemplate(object): """ Figure out with whole formatting. Separate advanced keys (*Like '{project[name]}') from string which must - be formatted separatelly in case of missing or incomplete keys in data. + be formatted separately in case of missing or incomplete keys in data. Args: data (dict): Containing keys to be filled into template. diff --git a/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 5d177fec07..662913cadf 100644 --- a/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -198,7 +198,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, priority = self.priority or instance.data.get("priority", 50) - # rr requires absolut path or all jobs won't show up in rControl + # rr requires absolute path or all jobs won't show up in rrControl abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path) # command line set in E01__OpenPype__PublishJob.cfg, here only diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 8c6a7f1bb6..ca9896fb3f 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -529,7 +529,7 @@ class AttributeValues(object): Has dictionary like methods. Not all of them are allowed all the time. Args: - attr_defs(AbstractAttrDef): Defintions of value type and properties. + attr_defs(AbstractAttrDef): Definitions of value type and properties. values(dict): Values after possible conversion. origin_data(dict): Values loaded from host before conversion. """ diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 5505427d7e..e0b30763d0 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -347,7 +347,7 @@ class BaseCreator: Returns: str: Group label that can be used for grouping of instances in UI. - Group label can be overriden by instance itself. + Group label can be overridden by instance itself. """ if self._cached_group_label is None: @@ -607,18 +607,19 @@ class Creator(BaseCreator): """ # GUI Purposes - # - default_variants may not be used if `get_default_variants` is overriden + # - default_variants may not be used if `get_default_variants` + # is overridden default_variants = [] # Default variant used in 'get_default_variant' _default_variant = None # Short description of product type - # - may not be used if `get_description` is overriden + # - may not be used if `get_description` is overridden description = None # Detailed description of product type for artists - # - may not be used if `get_detail_description` is overriden + # - may not be used if `get_detail_description` is overridden detailed_description = None # It does make sense to change context on creation diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 564d78ea6f..84bffbe1ec 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -64,7 +64,7 @@ def convert_to_padded_path(path, padding): padding (int): number of padding Returns: - type: string with reformated path + type: string with reformatted path Example: convert_to_padded_path("plate.%d.exr") > plate.%04d.exr diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 91f839ebf3..aa2542d936 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -116,7 +116,7 @@ class LoaderPlugin(list): def is_compatible_loader(cls, context): """Return whether a loader is compatible with a context. - On override make sure it is overriden as class or static method. + On override make sure it is overridden as class or static method. This checks the product type and the representation for the given loader plugin. diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 1d7b5ed5a7..5e63ba444a 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1865,7 +1865,7 @@ class PlaceholderCreateMixin(object): self.log.debug("Clean up of placeholder is not implemented.") def _before_instance_create(self, placeholder): - """Can be overriden. Is called before instance is created.""" + """Can be overridden. Is called before instance is created.""" pass diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index a19b5b9090..98723beffa 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -80,7 +80,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # create duration duration = (timeline_out_h - timeline_in_h) + 1 - # ffmpeg generate new file only if doesnt exists already + # ffmpeg generate new file only if doesn't exists already if not recycling_file: # convert to seconds start_sec = float(timeline_in_h / fps) diff --git a/client/ayon_core/tools/common_models/thumbnails.py b/client/ayon_core/tools/common_models/thumbnails.py index 138cee4ea2..1c3aadc49f 100644 --- a/client/ayon_core/tools/common_models/thumbnails.py +++ b/client/ayon_core/tools/common_models/thumbnails.py @@ -112,7 +112,7 @@ class ThumbnailsCache: """ thumbnails_dir = self.get_thumbnails_dir() - # Skip if thumbnails dir does not exists yet + # Skip if thumbnails dir does not exist yet if not os.path.exists(thumbnails_dir): return diff --git a/client/ayon_core/tools/pyblish_pype/util.py b/client/ayon_core/tools/pyblish_pype/util.py index 8126637060..0c3a7a8ba6 100644 --- a/client/ayon_core/tools/pyblish_pype/util.py +++ b/client/ayon_core/tools/pyblish_pype/util.py @@ -39,7 +39,7 @@ def defer(delay, func): This aids in keeping the GUI responsive, but complicates logic when producing tests. To combat this, the environment variable ensures - that every operation is synchonous. + that every operation is synchronous. Arguments: delay (float): Delay multiplier; default 1, 0 means no delay diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 9b5f3ae571..f9ac1059ac 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -424,7 +424,7 @@ class ExtractReviewOutputDefModel(BaseSettingsModel): title="Scale pixel aspect", description=( "Rescale input when it's pixel aspect ratio is not 1." - " Usefull for anamorph reviews." + " Useful for anamorphic reviews." ) ) bg_color: ColorRGBA_uint8 = SettingsField( From 8e740caf2c9609b8874c06809084d93f7ac8864b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 11:48:45 +0100 Subject: [PATCH 207/284] No need to define `apply_settings` since it gets auto-applied --- .../maya/plugins/create/create_unreal_skeletalmesh.py | 7 ------- .../hosts/maya/plugins/create/create_unreal_staticmesh.py | 5 ----- 2 files changed, 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py index 9ded28b812..a32e94971e 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -20,13 +20,6 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator): # Defined in settings joint_hints = set() - def apply_settings(self, project_settings): - """Apply project settings to creator""" - settings = ( - project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"] - ) - self.joint_hints = set(settings.get("joint_hints", [])) - def get_dynamic_data( self, project_name, diff --git a/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py b/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py index 1991f92915..76c33f00cc 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -15,11 +15,6 @@ class CreateUnrealStaticMesh(plugin.MayaCreator): # Defined in settings collision_prefixes = [] - def apply_settings(self, project_settings): - """Apply project settings to creator""" - settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"] - self.collision_prefixes = settings["collision_prefixes"] - def get_dynamic_data( self, project_name, From f2056e8b34f2f8fc31bb62e4416fdb1e048b0246 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 11:53:34 +0100 Subject: [PATCH 208/284] Make Creator class name compared to existing `CreateYetiCache` in `create_yeti_cache.py` --- .../hosts/maya/plugins/create/create_unreal_yeticache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/create/create_unreal_yeticache.py b/client/ayon_core/hosts/maya/plugins/create/create_unreal_yeticache.py index 1eac8a5ea9..dea64b40fb 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_unreal_yeticache.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_unreal_yeticache.py @@ -5,7 +5,7 @@ from ayon_core.hosts.maya.api import ( from ayon_core.lib import NumberDef -class CreateYetiCache(plugin.MayaCreator): +class CreateUnrealYetiCache(plugin.MayaCreator): """Output for procedural plugin nodes of Yeti """ identifier = "io.openpype.creators.maya.unrealyeticache" From 499dd39f850ad1cef970f65a65b888bc569127a3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:05:59 +0100 Subject: [PATCH 209/284] removed unused imports --- client/ayon_core/cli_commands.py | 1 - .../hosts/aftereffects/plugins/publish/collect_render.py | 5 +---- .../hosts/blender/plugins/publish/extract_camera_abc.py | 1 - .../ayon_core/hosts/blender/plugins/publish/extract_fbx.py | 1 - client/ayon_core/hosts/fusion/api/pipeline.py | 1 - .../hosts/harmony/plugins/publish/collect_audio.py | 2 +- client/ayon_core/hosts/hiero/api/events.py | 4 +++- .../hiero/api/startup/Python/Startup/SpreadsheetExport.py | 4 +++- .../hosts/hiero/plugins/publish/collect_clip_effects.py | 2 +- client/ayon_core/hosts/houdini/api/pipeline.py | 1 - client/ayon_core/hosts/max/plugins/load/load_model_obj.py | 1 - .../hosts/max/plugins/publish/validate_camera_contents.py | 2 +- .../hosts/maya/plugins/load/load_arnold_standin.py | 1 - client/ayon_core/hosts/maya/plugins/load/load_gpucache.py | 2 -- client/ayon_core/hosts/maya/plugins/load/load_image.py | 2 -- .../plugins/publish/extract_unreal_skeletalmesh_abc.py | 1 - .../hosts/maya/plugins/publish/extract_workfile_xgen.py | 1 - .../ayon_core/hosts/maya/plugins/publish/extract_xgen.py | 1 - .../maya/plugins/publish/validate_node_no_ghosting.py | 6 ++---- client/ayon_core/hosts/nuke/api/pipeline.py | 2 -- client/ayon_core/hosts/nuke/api/plugin.py | 7 ------- .../ayon_core/hosts/nuke/plugins/publish/extract_camera.py | 1 - .../hosts/nuke/plugins/publish/validate_rendered_frames.py | 2 +- client/ayon_core/hosts/photoshop/api/launch_logic.py | 2 +- .../ayon_core/hosts/photoshop/plugins/publish/closePS.py | 2 -- .../unreal/plugins/publish/collect_render_instances.py | 3 +-- client/ayon_core/modules/clockify/clockify_api.py | 2 -- .../deadline/plugins/publish/submit_maya_deadline.py | 1 - .../deadline/repository/custom/plugins/Ayon/Ayon.py | 1 - .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 1 - client/ayon_core/modules/royalrender/lib.py | 1 - .../plugins/publish/submit_jobs_to_royalrender.py | 1 - client/ayon_core/pipeline/create/product_name.py | 2 -- client/ayon_core/pipeline/farm/pyblish_functions.pyi | 2 +- client/ayon_core/pipeline/load/plugins.py | 1 - client/ayon_core/pipeline/publish/publish_plugins.py | 1 - .../tools/publisher/widgets/create_context_widgets.py | 2 +- client/ayon_core/tools/publisher/widgets/folders_dialog.py | 2 +- client/ayon_core/tools/publisher/widgets/publish_frame.py | 4 ---- client/ayon_core/tools/publisher/widgets/tasks_model.py | 2 +- client/ayon_core/tools/pyblish_pype/util.py | 2 -- client/ayon_core/tools/utils/lib.py | 1 - client/ayon_core/tools/utils/models.py | 2 +- server_addon/create_ayon_addons.py | 1 - server_addon/nuke/server/settings/main.py | 1 - server_addon/tvpaint/server/settings/main.py | 1 - 46 files changed, 20 insertions(+), 69 deletions(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index bc0a22382c..fa90571462 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -3,7 +3,6 @@ import os import sys import json -import warnings class Commands: diff --git a/client/ayon_core/hosts/aftereffects/plugins/publish/collect_render.py b/client/ayon_core/hosts/aftereffects/plugins/publish/collect_render.py index afd58ca758..4134e9d593 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/aftereffects/plugins/publish/collect_render.py @@ -1,14 +1,11 @@ import os -import re import tempfile -import attr +import attr import pyblish.api -from ayon_core.settings import get_project_settings from ayon_core.pipeline import publish from ayon_core.pipeline.publish import RenderInstance - from ayon_core.hosts.aftereffects.api import get_stub diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py index cc783e552c..c60c92dee1 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py @@ -4,7 +4,6 @@ import bpy from ayon_core.pipeline import publish from ayon_core.hosts.blender.api import plugin -from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py index 7ebda2c4cd..e6367dbc0d 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py @@ -4,7 +4,6 @@ import bpy from ayon_core.pipeline import publish from ayon_core.hosts.blender.api import plugin -from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 50157cfae6..dfac0640b0 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -28,7 +28,6 @@ from ayon_core.tools.utils import host_tools from .lib import ( get_current_comp, - comp_lock_and_undo_chunk, validate_comp_prefs ) diff --git a/client/ayon_core/hosts/harmony/plugins/publish/collect_audio.py b/client/ayon_core/hosts/harmony/plugins/publish/collect_audio.py index 40b4107a62..cc959a23b9 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/collect_audio.py +++ b/client/ayon_core/hosts/harmony/plugins/publish/collect_audio.py @@ -1,8 +1,8 @@ import os -import pyblish.api import pyblish.api + class CollectAudio(pyblish.api.InstancePlugin): """ Collect relative path for audio file to instance. diff --git a/client/ayon_core/hosts/hiero/api/events.py b/client/ayon_core/hosts/hiero/api/events.py index 0e509747d5..304605e24e 100644 --- a/client/ayon_core/hosts/hiero/api/events.py +++ b/client/ayon_core/hosts/hiero/api/events.py @@ -1,10 +1,12 @@ import os + import hiero.core.events + from ayon_core.lib import Logger, register_event_callback + from .lib import ( sync_avalon_data_to_workfile, launch_workfiles_app, - selection_changed_timeline, before_project_save, ) from .tags import add_tags_to_workfile diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py b/client/ayon_core/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py index 9c919e7cb4..6a8057ec1e 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py @@ -3,9 +3,11 @@ # Note: This only prints the text data that is visible in the active Spreadsheet View. # If you've filtered text, only the visible text will be printed to the CSV file # Usage: Copy to ~/.hiero/Python/StartupUI +import os +import csv + import hiero.core.events import hiero.ui -import os, csv try: from PySide.QtGui import * from PySide.QtCore import * diff --git a/client/ayon_core/hosts/hiero/plugins/publish/collect_clip_effects.py b/client/ayon_core/hosts/hiero/plugins/publish/collect_clip_effects.py index 32b4864022..bfc63f2551 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -1,5 +1,5 @@ -from itertools import product import re + import pyblish.api diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index d5144200cf..5131344483 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Houdini integration.""" import os -import sys import logging import hou # noqa diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py index 4f8a22af07..2330dbfc24 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py @@ -7,7 +7,6 @@ from ayon_core.hosts.max.api.lib import ( maintained_selection, object_transform_set ) -from ayon_core.hosts.max.api.lib import maintained_selection from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py b/client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py index 0473fd4a8a..334e7dcec9 100644 --- a/client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py +++ b/client/ayon_core/hosts/max/plugins/publish/validate_camera_contents.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import pyblish.api + from ayon_core.pipeline import PublishValidationError -from pymxs import runtime as rt class ValidateCameraContent(pyblish.api.InstancePlugin): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py b/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py index 920ad762b3..7170c30422 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py @@ -12,7 +12,6 @@ from ayon_core.hosts.maya.api.lib import ( unique_namespace, get_attribute_input, maintained_selection, - convert_to_maya_fps ) from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.plugin import get_load_color_for_product_type diff --git a/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py b/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py index 494bc7cfc6..9689282ae9 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py @@ -1,5 +1,3 @@ -import os - import maya.cmds as cmds from ayon_core.hosts.maya.api.pipeline import containerise diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image.py b/client/ayon_core/hosts/maya/plugins/load/load_image.py index 4976c46d7f..3641655d49 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image.py @@ -1,10 +1,8 @@ -import os import copy from ayon_core.lib import EnumDef from ayon_core.pipeline import ( load, - get_representation_context, get_current_host_name, ) from ayon_core.pipeline.load.utils import get_representation_path_from_context diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py index 8b88bfb9f8..1a389f3d33 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Create Unreal Skeletal Mesh data to be extracted as FBX.""" import os -from contextlib import contextmanager from maya import cmds # noqa diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py index d305b8dc6c..d799486184 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -7,7 +7,6 @@ from maya import cmds import pyblish.api from ayon_core.hosts.maya.api.lib import extract_alembic from ayon_core.pipeline import publish -from ayon_core.lib import StringTemplate class ExtractWorkfileXgen(publish.Extractor): diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py index 73668da28d..b672089a63 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py @@ -9,7 +9,6 @@ from ayon_core.pipeline import publish from ayon_core.hosts.maya.api.lib import ( maintained_selection, attribute_values, write_xgen_file, delete_after ) -from ayon_core.lib import StringTemplate class ExtractXgen(publish.Extractor): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py index 73701f8d83..843ee0d9d5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py @@ -3,11 +3,9 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - OptionalPyblishPluginMixin +from ayon_core.pipeline.publish import ValidateContentsOrder + -) class ValidateNodeNoGhosting(pyblish.api.InstancePlugin. OptionalPyblishPluginMixin): """Ensure nodes do not have ghosting enabled. diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index 2255276c56..0d44aba2f9 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -30,13 +30,11 @@ from ayon_core.tools.utils import host_tools from ayon_core.hosts.nuke import NUKE_ROOT_DIR from ayon_core.tools.workfile_template_build import open_template_ui -from .command import viewer_update_and_undo_stop from .lib import ( Context, ROOT_DATA_KNOB, INSTANCE_DATA_KNOB, get_main_window, - add_publish_knob, WorkfileSettings, # TODO: remove this once workfile builder will be removed process_workfile_builder, diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/client/ayon_core/hosts/nuke/api/plugin.py index 7f016d9c66..a5f240e42a 100644 --- a/client/ayon_core/hosts/nuke/api/plugin.py +++ b/client/ayon_core/hosts/nuke/api/plugin.py @@ -6,7 +6,6 @@ import six import random import string from collections import OrderedDict, defaultdict -from abc import abstractmethod from ayon_core.settings import get_current_project_settings from ayon_core.lib import ( @@ -14,7 +13,6 @@ from ayon_core.lib import ( EnumDef ) from ayon_core.pipeline import ( - LegacyCreator, LoaderPlugin, CreatorError, Creator as NewCreator, @@ -34,18 +32,13 @@ from ayon_core.lib.transcoding import ( from .lib import ( INSTANCE_DATA_KNOB, Knobby, - check_product_name_exists, maintained_selection, get_avalon_knob_data, - set_avalon_knob_data, - add_publish_knob, - get_nuke_imageio_settings, set_node_knobs_from_settings, set_node_data, get_node_data, get_view_process_node, get_viewer_config_from_string, - deprecated, get_filenames_without_hash, link_knobs ) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py index 1f5a8c73e1..a1a5acb63b 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py @@ -1,6 +1,5 @@ import os import math -from pprint import pformat import nuke diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py index 852267f68c..76ac7e97ad 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -1,6 +1,6 @@ -import os import pyblish.api import clique + from ayon_core.pipeline import PublishXmlValidationError from ayon_core.pipeline.publish import get_errored_instances_from_context diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index d0823646d7..c388f93044 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -11,7 +11,7 @@ from wsrpc_aiohttp import ( import ayon_api from qtpy import QtCore -from ayon_core.lib import Logger, StringTemplate +from ayon_core.lib import Logger from ayon_core.pipeline import ( registered_host, Anatomy, diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/closePS.py b/client/ayon_core/hosts/photoshop/plugins/publish/closePS.py index 6f86d98580..68c3b5b249 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/closePS.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/closePS.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Close PS after publish. For Webpublishing only.""" -import os - import pyblish.api from ayon_core.hosts.photoshop import api as photoshop diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py index ea53f221ea..ce2a03155b 100644 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py @@ -1,12 +1,11 @@ -import os from pathlib import Path import unreal +import pyblish.api from ayon_core.pipeline import get_current_project_name from ayon_core.pipeline import Anatomy from ayon_core.hosts.unreal.api import pipeline -import pyblish.api class CollectRenderInstances(pyblish.api.InstancePlugin): diff --git a/client/ayon_core/modules/clockify/clockify_api.py b/client/ayon_core/modules/clockify/clockify_api.py index f8c9c537ee..2e1d8f008f 100644 --- a/client/ayon_core/modules/clockify/clockify_api.py +++ b/client/ayon_core/modules/clockify/clockify_api.py @@ -1,6 +1,4 @@ import os -import re -import time import json import datetime import requests diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index 0e871eb90e..a31a11ffb1 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -651,7 +651,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return job_info, attr.asdict(plugin_info) def _get_arnold_render_payload(self, data): - from maya import cmds # Job Info job_info = copy.deepcopy(self.job_info) job_info.Name = self._job_info_label("Render") diff --git a/client/ayon_core/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/client/ayon_core/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index de0a2c6d7a..bb7f932013 100644 --- a/client/ayon_core/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/client/ayon_core/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -7,7 +7,6 @@ from Deadline.Plugins import PluginType, DeadlinePlugin from Deadline.Scripting import ( StringUtils, FileUtils, - DirectoryUtils, RepositoryUtils ) diff --git a/client/ayon_core/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/client/ayon_core/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 1565b2c496..8df96b425e 100644 --- a/client/ayon_core/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/client/ayon_core/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -12,7 +12,6 @@ from Deadline.Scripting import ( RepositoryUtils, FileUtils, DirectoryUtils, - ProcessUtils, ) __version__ = "1.0.1" VERSION_REGEX = re.compile( diff --git a/client/ayon_core/modules/royalrender/lib.py b/client/ayon_core/modules/royalrender/lib.py index d552e7fb19..5392803710 100644 --- a/client/ayon_core/modules/royalrender/lib.py +++ b/client/ayon_core/modules/royalrender/lib.py @@ -2,7 +2,6 @@ """Submitting render job to RoyalRender.""" import os import json -import platform import re import tempfile import uuid diff --git a/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index 54de943428..09c1dc4a54 100644 --- a/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Submit jobs to RoyalRender.""" import tempfile -import platform import pyblish.api from ayon_core.modules.royalrender.api import ( diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 74e268fbb3..fecda867e5 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,5 +1,3 @@ -import ayon_api - from ayon_core.settings import get_project_settings from ayon_core.lib import filter_profiles, prepare_template_data diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.pyi b/client/ayon_core/pipeline/farm/pyblish_functions.pyi index 16c11aa480..fe0ae57da0 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.pyi +++ b/client/ayon_core/pipeline/farm/pyblish_functions.pyi @@ -1,6 +1,6 @@ import pyblish.api from ayon_core.pipeline import Anatomy -from typing import Tuple, Union, List +from typing import Tuple, List class TimeData: diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 91f839ebf3..bced89e3a1 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -2,7 +2,6 @@ import os import logging from ayon_core.settings import get_project_settings -from ayon_core.pipeline import schema from ayon_core.pipeline.plugin_discover import ( discover, register_plugin, diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 2386558091..6b1984d92b 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -2,7 +2,6 @@ import inspect from abc import ABCMeta import pyblish.api from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin -from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS from ayon_core.lib import BoolDef from .lib import ( diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 61223bbe75..235a778d0f 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -1,4 +1,4 @@ -from qtpy import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets, QtCore from ayon_core.lib.events import QueuedEventSystem from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index 03336e10a6..8dce7aba3a 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -1,4 +1,4 @@ -from qtpy import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets from ayon_core.lib.events import QueuedEventSystem from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index d423f97047..ee65c69c19 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -1,7 +1,3 @@ -import os -import json -import time - from qtpy import QtWidgets, QtCore from .widgets import ( diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index e36de80fcf..78b1f23b17 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -1,4 +1,4 @@ -from qtpy import QtWidgets, QtCore, QtGui +from qtpy import QtCore, QtGui from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import get_qt_icon diff --git a/client/ayon_core/tools/pyblish_pype/util.py b/client/ayon_core/tools/pyblish_pype/util.py index 8126637060..ddbb7ddad2 100644 --- a/client/ayon_core/tools/pyblish_pype/util.py +++ b/client/ayon_core/tools/pyblish_pype/util.py @@ -7,8 +7,6 @@ from __future__ import ( import os import sys -import numbers -import copy import collections from qtpy import QtCore diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index 4b7ca5425e..d56b370d75 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -7,7 +7,6 @@ from qtpy import QtWidgets, QtCore, QtGui import qtawesome from ayon_core.style import ( - get_default_entity_icon_color, get_objected_colors, get_app_icon_path, ) diff --git a/client/ayon_core/tools/utils/models.py b/client/ayon_core/tools/utils/models.py index 92bed16e98..9b32cc5710 100644 --- a/client/ayon_core/tools/utils/models.py +++ b/client/ayon_core/tools/utils/models.py @@ -2,7 +2,7 @@ import re import logging import qtpy -from qtpy import QtCore, QtGui +from qtpy import QtCore log = logging.getLogger(__name__) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 9553980f5d..dd765b381e 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -1,7 +1,6 @@ import os import sys import re -import json import shutil import argparse import zipfile diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py index 2b269f1fce..936686d6ce 100644 --- a/server_addon/nuke/server/settings/main.py +++ b/server_addon/nuke/server/settings/main.py @@ -1,7 +1,6 @@ from ayon_server.settings import ( BaseSettingsModel, SettingsField, - ensure_unique_names ) from .general import ( diff --git a/server_addon/tvpaint/server/settings/main.py b/server_addon/tvpaint/server/settings/main.py index c6b6c9ab12..f20e9ecc9c 100644 --- a/server_addon/tvpaint/server/settings/main.py +++ b/server_addon/tvpaint/server/settings/main.py @@ -1,7 +1,6 @@ from ayon_server.settings import ( BaseSettingsModel, SettingsField, - ensure_unique_names, ) from .imageio import TVPaintImageIOModel From 1e4dff2735b36b7e0302b120e0f96bab75d5da7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:07:09 +0100 Subject: [PATCH 210/284] add missing functions and classes in '__all__ ' --- client/ayon_core/__init__.py | 12 ++++++++++++ .../hosts/aftereffects/api/__init__.py | 1 + client/ayon_core/modules/base.py | 15 +++++++++++++++ .../ayon_core/scripts/slates/slate_base/api.py | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/client/ayon_core/__init__.py b/client/ayon_core/__init__.py index 7d95587e8a..c1b93405f3 100644 --- a/client/ayon_core/__init__.py +++ b/client/ayon_core/__init__.py @@ -14,3 +14,15 @@ AYON_SERVER_ENABLED = True # Indicate if AYON entities should be used instead of OpenPype entities USE_AYON_ENTITIES = True # ------------------------- + + +__all__ = ( + "__version__", + + # Deprecated + "AYON_CORE_ROOT", + "PACKAGE_DIR", + "PLUGINS_DIR", + "AYON_SERVER_ENABLED", + "USE_AYON_ENTITIES", +) \ No newline at end of file diff --git a/client/ayon_core/hosts/aftereffects/api/__init__.py b/client/ayon_core/hosts/aftereffects/api/__init__.py index 4c4a8cce2f..b1d83c5ad9 100644 --- a/client/ayon_core/hosts/aftereffects/api/__init__.py +++ b/client/ayon_core/hosts/aftereffects/api/__init__.py @@ -31,6 +31,7 @@ __all__ = [ "get_stub", # pipeline + "AfterEffectsHost", "ls", "containerise", diff --git a/client/ayon_core/modules/base.py b/client/ayon_core/modules/base.py index 8a78edf961..3f2a7d4ea5 100644 --- a/client/ayon_core/modules/base.py +++ b/client/ayon_core/modules/base.py @@ -1,3 +1,5 @@ +# Backwards compatibility support +# - TODO should be removed before release 1.0.0 from ayon_core.addon import ( AYONAddon, AddonsManager, @@ -12,3 +14,16 @@ from ayon_core.addon.base import ( ModulesManager = AddonsManager TrayModulesManager = TrayAddonsManager load_modules = load_addons + + +__all__ = ( + "AYONAddon", + "AddonsManager", + "TrayAddonsManager", + "load_addons", + "OpenPypeModule", + "OpenPypeAddOn", + "ModulesManager", + "TrayModulesManager", + "load_modules", +) diff --git a/client/ayon_core/scripts/slates/slate_base/api.py b/client/ayon_core/scripts/slates/slate_base/api.py index cd64c68134..d1b4b22979 100644 --- a/client/ayon_core/scripts/slates/slate_base/api.py +++ b/client/ayon_core/scripts/slates/slate_base/api.py @@ -13,3 +13,21 @@ from .items import ( ) from .lib import create_slates from .example import example + + +__all__ = ( + "FontFactory", + "BaseObj", + "load_default_style", + "MainFrame", + "Layer", + "BaseItem", + "ItemImage", + "ItemRectangle", + "ItemPlaceHolder", + "ItemText", + "ItemTable", + "TableField", + "create_slates", + "example", +) From 0300c69806dade675712e7acea12ffaa7162de40 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:08:10 +0100 Subject: [PATCH 211/284] use 'get_project_settings' in substance painter --- .../ayon_core/hosts/substancepainter/api/pipeline.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/client/ayon_core/hosts/substancepainter/api/pipeline.py index c75cc3135a..cc24e41702 100644 --- a/client/ayon_core/hosts/substancepainter/api/pipeline.py +++ b/client/ayon_core/hosts/substancepainter/api/pipeline.py @@ -12,17 +12,15 @@ import substance_painter.project import pyblish.api from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from ayon_core.settings import ( - get_current_project_settings, - get_project_settings, -) +from ayon_core.settings import get_project_settings from ayon_core.pipeline.template_data import get_template_data_with_names from ayon_core.pipeline import ( + get_current_project_name, register_creator_plugin_path, register_loader_plugin_path, AVALON_CONTAINER_ID, - Anatomy + Anatomy, ) from ayon_core.lib import ( StringTemplate, @@ -76,7 +74,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing menu ... ") self._install_menu() - project_settings = get_current_project_settings() + project_settings = get_project_settings(get_current_project_name()) self._install_shelves(project_settings) self._has_been_setup = True From defe61496b8625db46d96ecd7ed6d5787544b570 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:08:48 +0100 Subject: [PATCH 212/284] added missing import to validate knobs --- client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py index 281e172788..8bcde9609d 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py @@ -1,3 +1,5 @@ +import json + import nuke import six import pyblish.api From d0ba1123373deefab264b40754e71b87176fd038 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:09:33 +0100 Subject: [PATCH 213/284] revert 'parm' in comment --- client/ayon_core/hosts/houdini/api/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/colorspace.py b/client/ayon_core/hosts/houdini/api/colorspace.py index 6a92c77e49..66581d6f20 100644 --- a/client/ayon_core/hosts/houdini/api/colorspace.py +++ b/client/ayon_core/hosts/houdini/api/colorspace.py @@ -59,7 +59,7 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): """Returns the colorspace attribute of the default (display, view) pair. - It's used for 'ociocolorspace' param in OpenGL Node.""" + It's used for 'ociocolorspace' parm in OpenGL Node.""" prefs = get_color_management_preferences() return get_display_view_colorspace_name( From 7eeb7ccaa30a92662e10702fb1987df4ee9695b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:12:51 +0100 Subject: [PATCH 214/284] add new line at the end of file --- client/ayon_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/__init__.py b/client/ayon_core/__init__.py index c1b93405f3..ce5a28601c 100644 --- a/client/ayon_core/__init__.py +++ b/client/ayon_core/__init__.py @@ -25,4 +25,4 @@ __all__ = ( "PLUGINS_DIR", "AYON_SERVER_ENABLED", "USE_AYON_ENTITIES", -) \ No newline at end of file +) From 005271b28629e6cf5abe2c7c785661c2bd12beb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:26:32 +0100 Subject: [PATCH 215/284] renamed 'get_reformated_filename' to 'get_reformatted_filename' --- client/ayon_core/hosts/flame/api/__init__.py | 4 ++-- client/ayon_core/hosts/flame/api/lib.py | 4 ++-- client/ayon_core/hosts/flame/otio/flame_export.py | 2 +- client/ayon_core/hosts/flame/otio/utils.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/flame/api/__init__.py b/client/ayon_core/hosts/flame/api/__init__.py index e2c5ee154a..8fcf0c92b0 100644 --- a/client/ayon_core/hosts/flame/api/__init__.py +++ b/client/ayon_core/hosts/flame/api/__init__.py @@ -23,7 +23,7 @@ from .lib import ( reset_segment_selection, get_segment_attributes, get_clips_in_reels, - get_reformated_filename, + get_reformatted_filename, get_frame_from_filename, get_padding_from_filename, maintained_object_duplication, @@ -101,7 +101,7 @@ __all__ = [ "reset_segment_selection", "get_segment_attributes", "get_clips_in_reels", - "get_reformated_filename", + "get_reformatted_filename", "get_frame_from_filename", "get_padding_from_filename", "maintained_object_duplication", diff --git a/client/ayon_core/hosts/flame/api/lib.py b/client/ayon_core/hosts/flame/api/lib.py index e1316658bf..8bfe6348ea 100644 --- a/client/ayon_core/hosts/flame/api/lib.py +++ b/client/ayon_core/hosts/flame/api/lib.py @@ -607,7 +607,7 @@ def get_clips_in_reels(project): return output_clips -def get_reformated_filename(filename, padded=True): +def get_reformatted_filename(filename, padded=True): """ Return fixed python expression path @@ -618,7 +618,7 @@ def get_reformated_filename(filename, padded=True): type: string with reformatted path Example: - get_reformated_filename("plate.1001.exr") > plate.%04d.exr + get_reformatted_filename("plate.1001.exr") > plate.%04d.exr """ found = FRAME_PATTERN.search(filename) diff --git a/client/ayon_core/hosts/flame/otio/flame_export.py b/client/ayon_core/hosts/flame/otio/flame_export.py index e5ea4dcf5e..cb038f9e9a 100644 --- a/client/ayon_core/hosts/flame/otio/flame_export.py +++ b/client/ayon_core/hosts/flame/otio/flame_export.py @@ -256,7 +256,7 @@ def create_otio_reference(clip_data, fps=None): if not otio_ex_ref_item: dirname, file_name = os.path.split(path) - file_name = utils.get_reformated_filename(file_name, padded=False) + file_name = utils.get_reformatted_filename(file_name, padded=False) reformated_path = os.path.join(dirname, file_name) # in case old OTIO or video file create `ExternalReference` otio_ex_ref_item = otio.schema.ExternalReference( diff --git a/client/ayon_core/hosts/flame/otio/utils.py b/client/ayon_core/hosts/flame/otio/utils.py index a1206b6710..5a28263fc2 100644 --- a/client/ayon_core/hosts/flame/otio/utils.py +++ b/client/ayon_core/hosts/flame/otio/utils.py @@ -21,7 +21,7 @@ def frames_to_seconds(frames, framerate): return otio.opentime.to_seconds(rt) -def get_reformated_filename(filename, padded=True): +def get_reformatted_filename(filename, padded=True): """ Return fixed python expression path @@ -32,7 +32,7 @@ def get_reformated_filename(filename, padded=True): type: string with reformatted path Example: - get_reformated_filename("plate.1001.exr") > plate.%04d.exr + get_reformatted_filename("plate.1001.exr") > plate.%04d.exr """ found = FRAME_PATTERN.search(filename) From c69955c7b500173c7ff1970c02d4ce384e7d0459 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:28:26 +0100 Subject: [PATCH 216/284] use correct variable name 'version_attributes' --- client/ayon_core/hosts/houdini/plugins/load/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/actions.py b/client/ayon_core/hosts/houdini/plugins/load/actions.py index c277005919..fbd89ab9c2 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/actions.py +++ b/client/ayon_core/hosts/houdini/plugins/load/actions.py @@ -76,8 +76,8 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): return # Include handles - start -= version_data.get("handleStart", 0) - end += version_data.get("handleEnd", 0) + start -= version_attributes.get("handleStart", 0) + end += version_attributes.get("handleEnd", 0) hou.playbar.setFrameRange(start, end) hou.playbar.setPlaybackRange(start, end) From 52f4af67418dee20fc59fa4d9e13246b3c6a0717 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:28:52 +0100 Subject: [PATCH 217/284] use correct constant name 'NODE_TAB_NAME' --- client/ayon_core/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index deeab47885..a9c5aac659 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -921,7 +921,7 @@ def writes_version_sync(): for each in nuke.allNodes(filter="Write"): # check if the node is avalon tracked - if _NODE_TAB_NAME not in each.knobs(): + if NODE_TAB_NAME not in each.knobs(): continue avalon_knob_data = read_avalon_data(each) From 2c08de530471c88336730e289366e04418865e3d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:32:01 +0100 Subject: [PATCH 218/284] use full variable name in tvpaint --- client/ayon_core/hosts/tvpaint/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py index 8d91afc74e..e3a24c79a2 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py @@ -599,7 +599,7 @@ class CreateRenderPass(TVPaintCreator): if filtered_layers: self.log.info(( "Changing group of " - f"{','.join([l['name'] for l in filtered_layers])}" + f"{','.join([layer['name'] for layer in filtered_layers])}" f" to {group_id}" )) george_lines = [ From 71b4bb4527c6b6a348d471bec98ee8ab1b964685 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:33:25 +0100 Subject: [PATCH 219/284] removed unused variables --- client/ayon_core/hosts/hiero/api/plugin.py | 1 - .../api/startup/Python/StartupUI/otioimporter/OTIOImport.py | 6 ------ .../hosts/hiero/plugins/publish/precollect_instances.py | 2 -- .../hiero/plugins/publish_old_workflow/precollect_retime.py | 4 ++-- .../ayon_core/hosts/maya/plugins/publish/collect_render.py | 1 - client/ayon_core/hosts/unreal/lib.py | 2 -- server_addon/create_ayon_addons.py | 1 - 7 files changed, 2 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 4878368716..a6264add33 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -449,7 +449,6 @@ class ClipLoader: repr = self.context["representation"] repr_cntx = repr["context"] folder_path = self.context["folder"]["path"] - folder_name = self.context["folder"]["name"] product_name = self.context["product"]["name"] representation = repr["name"] self.data["clip_name"] = self.clip_name_template.format(**repr_cntx) diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index 8331c429df..5a84bbdfaf 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -254,12 +254,6 @@ def add_markers(otio_item, hiero_item, tagsbin): if _tag is None: _tag = hiero.core.Tag(marker_color_map[marker.color]) - start = marker.marked_range.start_time.value - end = ( - marker.marked_range.start_time.value + - marker.marked_range.duration.value - ) - tag = hiero_item.addTag(_tag) tag.setName(marker.name or marker_color_map[marker_color]) diff --git a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py index 26f6968884..d4d75d14ec 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py @@ -378,8 +378,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # collect all subtrack items sub_track_items = {} for track in tracks: - items = track.items() - effet_items = track.subTrackItems() # skip if no clips on track > need track with effect only diff --git a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py b/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py index 297ffa8001..d0f28840b5 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py +++ b/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py @@ -36,8 +36,8 @@ class PrecollectRetime(api.InstancePlugin): speed = track_item.playbackSpeed() # calculate available material before retime - available_in = int(track_item.handleInLength() * speed) - available_out = int(track_item.handleOutLength() * speed) + # available_in = int(track_item.handleInLength() * speed) + # available_out = int(track_item.handleOutLength() * speed) self.log.debug(( "_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n " diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py index c981c37123..ff959afabc 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py @@ -78,7 +78,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin): layer = instance.data["transientData"]["layer"] objset = instance.data.get("instance_node") filepath = context.data["currentFile"].replace("\\", "/") - workspace = context.data["workspaceDir"] # check if layer is renderable if not layer.isRenderable(): diff --git a/client/ayon_core/hosts/unreal/lib.py b/client/ayon_core/hosts/unreal/lib.py index fe9e239ed5..cbb589ad1e 100644 --- a/client/ayon_core/hosts/unreal/lib.py +++ b/client/ayon_core/hosts/unreal/lib.py @@ -216,10 +216,8 @@ def create_unreal_project(project_name: str, since 3.16.0 """ - env = env or os.environ preset = get_project_settings(project_name)["unreal"]["project_setup"] - ue_id = ".".join(ue_version.split(".")[:2]) # get unreal engine identifier # ------------------------------------------------------------------------- # FIXME (antirotor): As of 4.26 this is problem with UE4 built from diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 9553980f5d..8464a2f127 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -220,7 +220,6 @@ def main( addons=None, ): current_dir = Path(os.path.dirname(os.path.abspath(__file__))) - root_dir = current_dir.parent create_zip = not skip_zip if output_dir: From 7a005dbb8a1ffea70829fb7fa110f2f23d74c1bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:33:40 +0100 Subject: [PATCH 220/284] fix typo in 'effet_items' > 'effect_items' --- .../hosts/hiero/plugins/publish/precollect_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py index d4d75d14ec..e10f2c7e13 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py @@ -378,10 +378,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # collect all subtrack items sub_track_items = {} for track in tracks: - effet_items = track.subTrackItems() + effect_items = track.subTrackItems() # skip if no clips on track > need track with effect only - if not effet_items: + if not effect_items: continue # skip all disabled tracks @@ -389,7 +389,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): continue track_index = track.trackIndex() - _sub_track_items = phiero.flatten(effet_items) + _sub_track_items = phiero.flatten(effect_items) _sub_track_items = list(_sub_track_items) # continue only if any subtrack items are collected From 89c310e9c9b77efceafffe7bafba43fba59cb13d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:34:31 +0100 Subject: [PATCH 221/284] removed unnecessary f-strings --- .../hosts/harmony/plugins/create/create_farm_render.py | 4 ++-- .../ayon_core/hosts/harmony/plugins/publish/collect_scene.py | 4 ++-- .../hosts/max/plugins/publish/validate_renderpasses.py | 2 +- client/ayon_core/hosts/unreal/plugins/create/create_render.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/harmony/plugins/create/create_farm_render.py b/client/ayon_core/hosts/harmony/plugins/create/create_farm_render.py index 16c403de6a..3039d56ead 100644 --- a/client/ayon_core/hosts/harmony/plugins/create/create_farm_render.py +++ b/client/ayon_core/hosts/harmony/plugins/create/create_farm_render.py @@ -21,12 +21,12 @@ class CreateFarmRender(plugin.Creator): path = "render/{0}/{0}.".format(node.split("/")[-1]) harmony.send( { - "function": f"PypeHarmony.Creators.CreateRender.create", + "function": "PypeHarmony.Creators.CreateRender.create", "args": [node, path] }) harmony.send( { - "function": f"PypeHarmony.color", + "function": "PypeHarmony.color", "args": [[0.9, 0.75, 0.3, 1.0]] } ) diff --git a/client/ayon_core/hosts/harmony/plugins/publish/collect_scene.py b/client/ayon_core/hosts/harmony/plugins/publish/collect_scene.py index a60e44b69b..bc2ccca1be 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/collect_scene.py +++ b/client/ayon_core/hosts/harmony/plugins/publish/collect_scene.py @@ -17,7 +17,7 @@ class CollectScene(pyblish.api.ContextPlugin): """Plugin entry point.""" result = harmony.send( { - f"function": "PypeHarmony.getSceneSettings", + "function": "PypeHarmony.getSceneSettings", "args": []} )["result"] @@ -62,7 +62,7 @@ class CollectScene(pyblish.api.ContextPlugin): result = harmony.send( { - f"function": "PypeHarmony.getVersion", + "function": "PypeHarmony.getVersion", "args": []} )["result"] context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1]) diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py b/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py index ba948747b9..394d3119c4 100644 --- a/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py +++ b/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py @@ -140,7 +140,7 @@ class ValidateRenderPasses(OptionalPyblishPluginMixin, invalid = [] if instance.name not in file_name: cls.log.error("The renderpass filename should contain the instance name.") - invalid.append((f"Invalid instance name", + invalid.append(("Invalid instance name", file_name)) if renderpass is not None: if not file_name.rstrip(".").endswith(renderpass): diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_render.py b/client/ayon_core/hosts/unreal/plugins/create/create_render.py index cbec84c543..5a96d9809c 100644 --- a/client/ayon_core/hosts/unreal/plugins/create/create_render.py +++ b/client/ayon_core/hosts/unreal/plugins/create/create_render.py @@ -50,7 +50,7 @@ class CreateRender(UnrealAssetCreator): # If the option to create a new level sequence is selected, # create a new level sequence and a master level. - root = f"/Game/Ayon/Sequences" + root = "/Game/Ayon/Sequences" # Create a new folder for the sequence in root sequence_dir_name = create_folder(root, product_name) @@ -166,7 +166,7 @@ class CreateRender(UnrealAssetCreator): master_lvl = levels[0].get_asset().get_path_name() except IndexError: raise RuntimeError( - f"Could not find the hierarchy for the selected sequence.") + "Could not find the hierarchy for the selected sequence.") # If the selected asset is the master sequence, we get its data # and then we create the instance for the master sequence. From e309c34202f79c7aed1dc341eb1958d478890aa5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:34:47 +0100 Subject: [PATCH 222/284] remove unnecessary f-strings in unreal lib --- client/ayon_core/hosts/unreal/lib.py | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hosts/unreal/lib.py b/client/ayon_core/hosts/unreal/lib.py index cbb589ad1e..37122b2096 100644 --- a/client/ayon_core/hosts/unreal/lib.py +++ b/client/ayon_core/hosts/unreal/lib.py @@ -236,10 +236,12 @@ def create_unreal_project(project_name: str, project_file = pr_dir / f"{unreal_project_name}.uproject" print("--- Generating a new project ...") - commandlet_cmd = [f'{ue_editor_exe.as_posix()}', - f'{cmdlet_project.as_posix()}', - f'-run=AyonGenerateProject', - f'{project_file.resolve().as_posix()}'] + commandlet_cmd = [ + ue_editor_exe.as_posix(), + cmdlet_project.as_posix(), + "-run=AyonGenerateProject", + project_file.resolve().as_posix() + ] if dev_mode or preset["dev_mode"]: commandlet_cmd.append('-GenerateCode') @@ -266,7 +268,7 @@ def create_unreal_project(project_name: str, pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() - print(f'--- Engine ID has been written into the project file') + print("--- Engine ID has been written into the project file") if dev_mode or preset["dev_mode"]: u_build_tool = get_path_to_ubt(engine_path, ue_version) @@ -280,17 +282,25 @@ def create_unreal_project(project_name: str, # we need to test this out arch = "Mac" - command1 = [u_build_tool.as_posix(), "-projectfiles", - f"-project={project_file}", "-progress"] + command1 = [ + u_build_tool.as_posix(), + "-projectfiles", + f"-project={project_file}", + "-progress" + ] subprocess.run(command1) - command2 = [u_build_tool.as_posix(), - f"-ModuleWithSuffix={unreal_project_name},3555", arch, - "Development", "-TargetType=Editor", - f'-Project={project_file}', - f'{project_file}', - "-IgnoreJunk"] + command2 = [ + u_build_tool.as_posix(), + f"-ModuleWithSuffix={unreal_project_name},3555", + arch, + "Development", + "-TargetType=Editor", + f"-Project={project_file}", + project_file, + "-IgnoreJunk" + ] subprocess.run(command2) From d60a7be65cce914959f0b6fb0318961f6767f3fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:35:11 +0100 Subject: [PATCH 223/284] change formatting in hiero import functionality --- .../hosts/hiero/api/otio/hiero_import.py | 36 ++++------ .../StartupUI/otioimporter/OTIOImport.py | 67 ++++++------------- 2 files changed, 34 insertions(+), 69 deletions(-) diff --git a/client/ayon_core/hosts/hiero/api/otio/hiero_import.py b/client/ayon_core/hosts/hiero/api/otio/hiero_import.py index f123b81ca6..29ff7f7325 100644 --- a/client/ayon_core/hosts/hiero/api/otio/hiero_import.py +++ b/client/ayon_core/hosts/hiero/api/otio/hiero_import.py @@ -101,7 +101,7 @@ def apply_transition(otio_track, otio_item, track): if transition_type == 'dissolve': transition_func = getattr( hiero.core.Transition, - 'create{kind}DissolveTransition'.format(kind=kind) + "create{kind}DissolveTransition".format(kind=kind) ) try: @@ -109,7 +109,7 @@ def apply_transition(otio_track, otio_item, track): item_in, item_out, otio_item.in_offset.value, - otio_item.out_offset.value + otio_item.out_offset.value, ) # Catch error raised if transition is bigger than TrackItem source @@ -134,7 +134,7 @@ def apply_transition(otio_track, otio_item, track): transition = transition_func( item_out, - otio_item.out_offset.value + otio_item.out_offset.value, ) elif transition_type == 'fade_out': @@ -183,9 +183,7 @@ def prep_url(url_in): def create_offline_mediasource(otio_clip, path=None): global _otio_old - hiero_rate = hiero.core.TimeBase( - otio_clip.source_range.start_time.rate - ) + hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate) try: legal_media_refs = ( @@ -212,7 +210,7 @@ def create_offline_mediasource(otio_clip, path=None): source_range.start_time.value, source_range.duration.value, hiero_rate, - source_range.start_time.value + source_range.start_time.value, ) return media @@ -385,7 +383,8 @@ def create_trackitem(playhead, track, otio_clip, clip): # Only reverse effect can be applied here if abs(time_scalar) == 1.: trackitem.setPlaybackSpeed( - trackitem.playbackSpeed() * time_scalar) + trackitem.playbackSpeed() * time_scalar + ) elif isinstance(effect, otio.schema.FreezeFrame): # For freeze frame, playback speed must be set after range @@ -397,28 +396,21 @@ def create_trackitem(playhead, track, otio_clip, clip): source_in = source_range.end_time_inclusive().value timeline_in = playhead + source_out - timeline_out = ( - timeline_in + - source_range.duration.value - ) - 1 + timeline_out = (timeline_in + source_range.duration.value) - 1 else: # Normal playback speed source_in = source_range.start_time.value source_out = source_range.end_time_inclusive().value timeline_in = playhead - timeline_out = ( - timeline_in + - source_range.duration.value - ) - 1 + timeline_out = (timeline_in + source_range.duration.value) - 1 # Set source and timeline in/out points trackitem.setTimes( timeline_in, timeline_out, source_in, - source_out - + source_out, ) # Apply playback speed for freeze frames @@ -435,7 +427,8 @@ def create_trackitem(playhead, track, otio_clip, clip): def build_sequence( - otio_timeline, project=None, sequence=None, track_kind=None): + otio_timeline, project=None, sequence=None, track_kind=None +): if project is None: if sequence: project = sequence.project() @@ -509,10 +502,7 @@ def build_sequence( # Create TrackItem trackitem = create_trackitem( - playhead, - track, - otio_clip, - clip + playhead, track, otio_clip, clip ) # Add markers diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index 5a84bbdfaf..01fd047123 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -101,14 +101,14 @@ def apply_transition(otio_track, otio_item, track): if transition_type == "dissolve": transition_func = getattr( hiero.core.Transition, - 'create{kind}DissolveTransition'.format(kind=kind) + "create{kind}DissolveTransition".format(kind=kind) ) transition = transition_func( item_in, item_out, otio_item.in_offset.value, - otio_item.out_offset.value + otio_item.out_offset.value, ) elif transition_type == "fade_in": @@ -116,20 +116,14 @@ def apply_transition(otio_track, otio_item, track): hiero.core.Transition, 'create{kind}FadeInTransition'.format(kind=kind) ) - transition = transition_func( - item_out, - otio_item.out_offset.value - ) + transition = transition_func(item_out, otio_item.out_offset.value) elif transition_type == "fade_out": transition_func = getattr( hiero.core.Transition, 'create{kind}FadeOutTransition'.format(kind=kind) ) - transition = transition_func( - item_in, - otio_item.in_offset.value - ) + transition = transition_func(item_in, otio_item.in_offset.value) else: # Unknown transition @@ -138,11 +132,10 @@ def apply_transition(otio_track, otio_item, track): # Apply transition to track track.addTransition(transition) - except Exception, e: + except Exception as e: sys.stderr.write( 'Unable to apply transition "{t}": "{e}"\n'.format( - t=otio_item, - e=e + t=otio_item, e=e ) ) @@ -153,18 +146,13 @@ def prep_url(url_in): if url.startswith("file://localhost/"): return url.replace("file://localhost/", "") - url = '{url}'.format( - sep=url.startswith(os.sep) and "" or os.sep, - url=url.startswith(os.sep) and url[1:] or url - ) + url = "{url}".format(url=url.startswith(os.sep) and url[1:] or url) return url def create_offline_mediasource(otio_clip, path=None): - hiero_rate = hiero.core.TimeBase( - otio_clip.source_range.start_time.rate - ) + hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate) if isinstance(otio_clip.media_reference, otio.schema.ExternalReference): source_range = otio_clip.available_range() @@ -180,7 +168,7 @@ def create_offline_mediasource(otio_clip, path=None): source_range.start_time.value, source_range.duration.value, hiero_rate, - source_range.start_time.value + source_range.start_time.value, ) return media @@ -203,7 +191,7 @@ marker_color_map = { "MAGENTA": "Magenta", "BLACK": "Blue", "WHITE": "Green", - "MINT": "Cyan" + "MINT": "Cyan", } @@ -269,12 +257,12 @@ def create_track(otio_track, tracknum, track_kind): # Create a Track if otio_track.kind == otio.schema.TrackKind.Video: track = hiero.core.VideoTrack( - otio_track.name or 'Video{n}'.format(n=tracknum) + otio_track.name or "Video{n}".format(n=tracknum) ) else: track = hiero.core.AudioTrack( - otio_track.name or 'Audio{n}'.format(n=tracknum) + otio_track.name or "Audio{n}".format(n=tracknum) ) return track @@ -309,34 +297,25 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin): for effect in otio_clip.effects: if isinstance(effect, otio.schema.LinearTimeWarp): trackitem.setPlaybackSpeed( - trackitem.playbackSpeed() * - effect.time_scalar + trackitem.playbackSpeed() * effect.time_scalar ) # If reverse playback speed swap source in and out if trackitem.playbackSpeed() < 0: source_out = source_range.start_time.value source_in = ( - source_range.start_time.value + - source_range.duration.value + source_range.start_time.value + source_range.duration.value ) - 1 timeline_in = playhead + source_out - timeline_out = ( - timeline_in + - source_range.duration.value - ) - 1 + timeline_out = (timeline_in + source_range.duration.value) - 1 else: # Normal playback speed source_in = source_range.start_time.value source_out = ( - source_range.start_time.value + - source_range.duration.value + source_range.start_time.value + source_range.duration.value ) - 1 timeline_in = playhead - timeline_out = ( - timeline_in + - source_range.duration.value - ) - 1 + timeline_out = (timeline_in + source_range.duration.value) - 1 # Set source and timeline in/out points trackitem.setSourceIn(source_in) @@ -351,7 +330,8 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin): def build_sequence( - otio_timeline, project=None, sequence=None, track_kind=None): + otio_timeline, project=None, sequence=None, track_kind=None +): if project is None: if sequence: @@ -408,8 +388,7 @@ def build_sequence( if isinstance(otio_clip, otio.schema.Stack): bar = hiero.ui.mainWindow().statusBar() bar.showMessage( - "Nested sequences are created separately.", - timeout=3000 + "Nested sequences are created separately.", timeout=3000 ) build_sequence(otio_clip, project, otio_track.kind) @@ -422,11 +401,7 @@ def build_sequence( # Create TrackItem trackitem = create_trackitem( - playhead, - track, - otio_clip, - clip, - tagsbin + playhead, track, otio_clip, clip, tagsbin ) # Add trackitem to track From 0c2a54c5679d65d875b3f851dd5619477591cc05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:35:39 +0100 Subject: [PATCH 224/284] fix invalid formatting --- client/ayon_core/hosts/houdini/plugins/load/load_alembic.py | 2 +- .../houdini/plugins/publish/validate_cop_output_node.py | 6 ++++-- .../maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py index a77d06d409..37657cbdff 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py @@ -59,7 +59,7 @@ class AbcLoader(load.LoaderPlugin): normal_node.setInput(0, unpack) - null = container.createNode("null", node_name="OUT".format(name)) + null = container.createNode("null", node_name="OUT") null.setInput(0, normal_node) # Ensure display flag is on the Alembic input node and not on the OUT diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_cop_output_node.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_cop_output_node.py index 95414ae7f1..fdf03d5cba 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_cop_output_node.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_cop_output_node.py @@ -71,6 +71,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin): # the isinstance check above should be stricter than this category if output_node.type().category().name() != "Cop2": raise PublishValidationError( - ("Output node %s is not of category Cop2. " - "This is a bug...").format(output_node.path()), + ( + "Output node {} is not of category Cop2." + " This is a bug..." + ).format(output_node.path()), title=cls.label) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py index edbb5f845e..6292afcf41 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py @@ -74,7 +74,7 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor): renamed_to_extract.append("|".join(node_path)) with renamed(original_parent, parent_node): - self.log.debug("Extracting: {}".format(renamed_to_extract, path)) + self.log.debug("Extracting: {}".format(renamed_to_extract)) fbx_exporter.export(renamed_to_extract, path) if "representations" not in instance.data: From a817a0a4d288b68ff52bfe42bed4ff075d318932 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:36:07 +0100 Subject: [PATCH 225/284] use isinstance for type check --- client/ayon_core/hosts/hiero/api/tags.py | 2 +- .../deadline/plugins/publish/submit_maya_deadline.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/hiero/api/tags.py b/client/ayon_core/hosts/hiero/api/tags.py index 32620aa2f5..5abfee75d0 100644 --- a/client/ayon_core/hosts/hiero/api/tags.py +++ b/client/ayon_core/hosts/hiero/api/tags.py @@ -89,7 +89,7 @@ def update_tag(tag, data): # set all data metadata to tag metadata for _k, _v in data_mtd.items(): value = str(_v) - if type(_v) == dict: + if isinstance(_v, dict): value = json.dumps(_v) # set the value diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index 0e871eb90e..dcc589ffd4 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -856,10 +856,10 @@ def _format_tiles( """ # Math used requires integers for correct output - as such # we ensure our inputs are correct. - assert type(tiles_x) is int, "tiles_x must be an integer" - assert type(tiles_y) is int, "tiles_y must be an integer" - assert type(width) is int, "width must be an integer" - assert type(height) is int, "height must be an integer" + assert isinstance(tiles_x, int), "tiles_x must be an integer" + assert isinstance(tiles_y, int), "tiles_y must be an integer" + assert isinstance(width, int), "width must be an integer" + assert isinstance(height, int), "height must be an integer" out = {"JobInfo": {}, "PluginInfo": {}} cfg = OrderedDict() From b3aba193251e96806b206173700d81ba77e8fb49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:37:04 +0100 Subject: [PATCH 226/284] use "not in" expression --- .../api/startup/Python/StartupUI/PimpMySpreadsheet.py | 2 +- client/ayon_core/hosts/maya/api/lib.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py index b8dfb07b47..fcfa24310e 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py @@ -641,7 +641,7 @@ def _setStatus(self, status): global gStatusTags # Get a valid Tag object from the Global list of statuses - if not status in gStatusTags.keys(): + if status not in gStatusTags.keys(): print("Status requested was not a valid Status string.") return diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index cad5b0405f..9c57b885f1 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2152,9 +2152,13 @@ def get_related_sets(node): sets = cmds.ls(sets) # Ignore `avalon.container` - sets = [s for s in sets if - not cmds.attributeQuery("id", node=s, exists=True) or - not cmds.getAttr("%s.id" % s) in ignored] + sets = [ + s for s in sets + if ( + not cmds.attributeQuery("id", node=s, exists=True) + or cmds.getAttr("%s.id" % s) not in ignored + ) + ] # Exclude deformer sets (`type=2` for `maya.cmds.listSets`) deformer_sets = cmds.listSets(object=node, From 85a9c0559cc896336fca09b38dbba25941a21501 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:37:17 +0100 Subject: [PATCH 227/284] sorter is not lamda function --- client/ayon_core/hosts/maya/tools/mayalookassigner/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py index f5dad25ff0..b0807be6a6 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py @@ -29,7 +29,8 @@ class AssetModel(models.TreeModel): self.beginResetModel() # Add the items sorted by label - sorter = lambda x: x["label"] + def sorter(x): + return x["label"] for item in sorted(items, key=sorter): From d7afb03a5373fd1b8237ae561f9825811c077a8d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:41:52 +0100 Subject: [PATCH 228/284] fix george script in tvpaint auto create plugin --- .../ayon_core/hosts/tvpaint/plugins/create/create_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py index 8d91afc74e..09533c9057 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py @@ -760,7 +760,9 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): grg_lines: list[str] = [] for group_id, group_name in new_group_name_by_id.items(): group: dict[str, Any] = groups_by_id[group_id] - grg_line: str = "tv_layercolor \"setcolor\" {} {} {} {} {}".format( + grg_line: str = ( + "tv_layercolor \"setcolor\" {} {} {} {} {} \"{}\"" + ).format( group["clip_id"], group_id, group["red"], From 3d4aa23c5ed2eb66ddd0a512a3d79fc194500d51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 12:49:57 +0100 Subject: [PATCH 229/284] use correct variable in maya legacy convertor plugin --- client/ayon_core/hosts/maya/plugins/create/convert_legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py index 685602ef0b..81cf9613b4 100644 --- a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py @@ -83,7 +83,7 @@ class MayaLegacyConvertor(ProductConvertorPlugin, ).format(product_type)) continue - creator_id = product_type_to_id[family] + creator_id = product_type_to_id[product_type] creator = self.create_context.creators[creator_id] data["creator_identifier"] = creator_id From 54406c0489d6973040936942f6ca49d77d453b50 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:53:09 +0100 Subject: [PATCH 230/284] forgotten formatting change Co-authored-by: Petr Kalis --- .../api/startup/Python/StartupUI/otioimporter/OTIOImport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index 01fd047123..cd775e8eea 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -121,7 +121,7 @@ def apply_transition(otio_track, otio_item, track): elif transition_type == "fade_out": transition_func = getattr( hiero.core.Transition, - 'create{kind}FadeOutTransition'.format(kind=kind) + "create{kind}FadeOutTransition".format(kind=kind) ) transition = transition_func(item_in, otio_item.in_offset.value) From e78b8043ab9bf2e4e44d1e545e0ed64b9745c3c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 13:15:07 +0100 Subject: [PATCH 231/284] Update client/ayon_core/hosts/maya/plugins/load/load_image_plane.py --- client/ayon_core/hosts/maya/plugins/load/load_image_plane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py index c5b85d2cd4..b298d5b892 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py @@ -142,7 +142,7 @@ class ImagePlaneLoader(load.LoaderPlugin): with namespaced(namespace): # Create inside the namespace image_plane_transform, image_plane_shape = cmds.imagePlane( - fileName=context["representation"]["data"]["path"], + fileName=self.filepath_from_context(context), camera=camera ) From 93b6ded5df3e661340d9c209ca2543e6f50dd500 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 13:36:25 +0100 Subject: [PATCH 232/284] Refactor task field to use None instead of empty string Changed the task field in base_instance_data dictionary from an empty string to None for better clarity and consistency. --- .../hosts/traypublisher/plugins/create/create_editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py index a9ee343dfb..843729786c 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py @@ -675,7 +675,7 @@ or updating already created. Publishing will create OTIO file. base_instance_data = { "shotName": shot_name, "variant": variant_name, - "task": "", + "task": None, "newAssetPublishing": True, "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, From 8b1ff955669976a6c47c7e6994e6ee634c375a10 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 13:36:41 +0100 Subject: [PATCH 233/284] Refactor folder path extraction logic for validation plugin Adjust how folder paths are processed for better accuracy. --- .../ayon_core/plugins/publish/validate_editorial_asset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index 33b4210ad5..eba816d275 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -35,7 +35,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): existing_folder_paths = { folder_entity["path"]: ( - folder_entity["path"].lstrip("/").rsplit("/")[0] + folder_entity["path"].lstrip("/").rsplit("/")[:-1] ) for folder_entity in folder_entities } From 9ebb59cc358741c354c304275b7ed853bd06e423 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 13:54:16 +0100 Subject: [PATCH 234/284] Refactor folder path handling for validation plugin Improved folder path processing for better accuracy in validation. --- .../ayon_core/plugins/publish/validate_editorial_asset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index eba816d275..ad47d00006 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -35,7 +35,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): existing_folder_paths = { folder_entity["path"]: ( - folder_entity["path"].lstrip("/").rsplit("/")[:-1] + folder_entity["path"].lstrip("/").split("/")[:-1] ) for folder_entity in folder_entities } From 87205c5f8e8f29e40180b5010d1f3bbf161c65ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:59:04 +0100 Subject: [PATCH 235/284] use f-string Co-authored-by: Roy Nieterau --- client/ayon_core/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 9c57b885f1..acfac760e6 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2156,7 +2156,7 @@ def get_related_sets(node): s for s in sets if ( not cmds.attributeQuery("id", node=s, exists=True) - or cmds.getAttr("%s.id" % s) not in ignored + or cmds.getAttr(f"{s}.id") not in ignored ) ] From 1ba2b97e9f26d51b264651afd98dfb5069ef22f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:00:00 +0100 Subject: [PATCH 236/284] simplify url strip Co-authored-by: Roy Nieterau --- .../api/startup/Python/StartupUI/otioimporter/OTIOImport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index cd775e8eea..d2fe608d99 100644 --- a/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/client/ayon_core/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -146,7 +146,8 @@ def prep_url(url_in): if url.startswith("file://localhost/"): return url.replace("file://localhost/", "") - url = "{url}".format(url=url.startswith(os.sep) and url[1:] or url) + if url.startswith(os.sep): + url = url[1:] return url From 4134b8947bebef737ebe484399dd39ff7a2ca1f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 14:02:22 +0100 Subject: [PATCH 237/284] remove commented out variables --- .../hiero/plugins/publish_old_workflow/precollect_retime.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py b/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py index d0f28840b5..8503a0b6a7 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py +++ b/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py @@ -35,10 +35,6 @@ class PrecollectRetime(api.InstancePlugin): source_out = int(track_item.sourceOut()) speed = track_item.playbackSpeed() - # calculate available material before retime - # available_in = int(track_item.handleInLength() * speed) - # available_out = int(track_item.handleOutLength() * speed) - self.log.debug(( "_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n " "source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n " From 37e7b31feb4f336a3e78c4c0eca9a80654b7fa77 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 14:19:32 +0100 Subject: [PATCH 238/284] Refactor parent folder handling in validation plugin - Added comments to clarify the list of parents for context and folders. --- .../ayon_core/plugins/publish/validate_editorial_asset_name.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index ad47d00006..f1a2eb38bb 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -23,7 +23,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): ] def process(self, context): - + # list of parents for current context, converted from folderPath folder_and_parents = self.get_parents(context) self.log.debug("__ folder_and_parents: {}".format(folder_and_parents)) @@ -35,6 +35,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): existing_folder_paths = { folder_entity["path"]: ( + # list of parents for current folder folder_entity["path"].lstrip("/").split("/")[:-1] ) for folder_entity in folder_entities From 4ce342d356dcd86c06c0964fbebf266a12055dbc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:21:49 +0100 Subject: [PATCH 239/284] check against lowered value --- client/ayon_core/hosts/traypublisher/api/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/api/editorial.py b/client/ayon_core/hosts/traypublisher/api/editorial.py index 09a2ab17ac..c71dae336c 100644 --- a/client/ayon_core/hosts/traypublisher/api/editorial.py +++ b/client/ayon_core/hosts/traypublisher/api/editorial.py @@ -186,7 +186,7 @@ class ShotMetadataSolver: # in case first parent is project then start parents from start if ( _index == 0 - and parent_token_type == "Project" + and parent_token_type.lower() == "project" ): project_parent = parents[0] parents = [project_parent] From d073f1afdf4c4f8aa0946d220a3d286d56be725c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 14:33:19 +0100 Subject: [PATCH 240/284] removed validate editorial asset name --- .../publish/validate_editorial_asset_name.py | 123 ------------------ 1 file changed, 123 deletions(-) delete mode 100644 client/ayon_core/plugins/publish/validate_editorial_asset_name.py diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py deleted file mode 100644 index f1a2eb38bb..0000000000 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ /dev/null @@ -1,123 +0,0 @@ -from pprint import pformat - -import ayon_api -import pyblish.api - -from ayon_core.pipeline import KnownPublishError - - -class ValidateEditorialAssetName(pyblish.api.ContextPlugin): - """ Validating if editorial's folder names are not already created in db. - - Checking variations of names with different size of caps or with - or without underscores. - """ - - order = pyblish.api.ValidatorOrder - label = "Validate Editorial Folder Name" - hosts = [ - "hiero", - "resolve", - "flame", - "traypublisher" - ] - - def process(self, context): - # list of parents for current context, converted from folderPath - folder_and_parents = self.get_parents(context) - self.log.debug("__ folder_and_parents: {}".format(folder_and_parents)) - - project_name = context.data["projectName"] - folder_entities = list(ayon_api.get_folders( - project_name, fields={"path"} - )) - self.log.debug("__ folder_entities: {}".format(folder_entities)) - - existing_folder_paths = { - folder_entity["path"]: ( - # list of parents for current folder - folder_entity["path"].lstrip("/").split("/")[:-1] - ) - for folder_entity in folder_entities - } - - self.log.debug("__ project_entities: {}".format( - pformat(existing_folder_paths))) - - folders_missing_name = {} - folders_wrong_parent = {} - for folder_path in folder_and_parents.keys(): - if folder_path not in existing_folder_paths.keys(): - # add to some nonexistent list for next layer of check - folders_missing_name[folder_path] = ( - folder_and_parents[folder_path] - ) - continue - - existing_parents = existing_folder_paths[folder_path] - if folder_and_parents[folder_path] != existing_parents: - # add to some nonexistent list for next layer of check - folders_wrong_parent[folder_path] = { - "required": folder_and_parents[folder_path], - "already_in_db": existing_folder_paths[folder_path] - } - continue - - self.log.debug("correct folder: {}".format(folder_path)) - - if folders_missing_name: - wrong_names = {} - self.log.debug( - ">> folders_missing_name: {}".format(folders_missing_name)) - - # This will create set of folder paths - folder_paths = { - folder_path.lower().replace("_", "") - for folder_path in existing_folder_paths - } - - for folder_path in folders_missing_name: - _folder_path = folder_path.lower().replace("_", "") - if _folder_path in folder_paths: - wrong_names[folder_path].update( - { - "required_name": folder_path, - "used_variants_in_db": [ - p - for p in existing_folder_paths - if p.lower().replace("_", "") == _folder_path - ] - } - ) - - if wrong_names: - self.log.debug( - ">> wrong_names: {}".format(wrong_names)) - raise Exception( - "Some already existing folder name variants `{}`".format( - wrong_names)) - - if folders_wrong_parent: - self.log.debug( - ">> folders_wrong_parent: {}".format(folders_wrong_parent)) - raise KnownPublishError( - "Wrong parents on folders `{}`".format(folders_wrong_parent)) - - def get_parents(self, context): - output = {} - for instance in context: - folder_path = instance.data["folderPath"] - families = instance.data.get("families", []) + [ - instance.data["family"] - ] - # filter out non-shot families - if "shot" not in families: - continue - - parents = instance.data["parents"] - - output[folder_path] = [ - str(p["entity_name"]) for p in parents - if p.get("entity_type") != "project" - ] - return output From e6e722c05a2613472f1917d59ac1a0df4035be98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 14:37:54 +0100 Subject: [PATCH 241/284] use 'get_current_project_settings' --- client/ayon_core/hosts/substancepainter/api/pipeline.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/client/ayon_core/hosts/substancepainter/api/pipeline.py index cc24e41702..23d629533c 100644 --- a/client/ayon_core/hosts/substancepainter/api/pipeline.py +++ b/client/ayon_core/hosts/substancepainter/api/pipeline.py @@ -12,11 +12,10 @@ import substance_painter.project import pyblish.api from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from ayon_core.settings import get_project_settings +from ayon_core.settings import get_current_project_settings from ayon_core.pipeline.template_data import get_template_data_with_names from ayon_core.pipeline import ( - get_current_project_name, register_creator_plugin_path, register_loader_plugin_path, AVALON_CONTAINER_ID, @@ -74,7 +73,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing menu ... ") self._install_menu() - project_settings = get_project_settings(get_current_project_name()) + project_settings = get_current_project_settings() self._install_shelves(project_settings) self._has_been_setup = True From 6717db4482c43f87b153f98567db20a183849b28 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 15:30:16 +0100 Subject: [PATCH 242/284] Update ColorRGBA import and add regex validation for output name field. - Import statement changed to only import ColorRGBA_uint8. - Added regex validation for the 'name' field in ExtractOIIOTranscodeOutputModel. --- server/settings/publish_plugins.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index f9ac1059ac..e61bf6986b 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -9,7 +9,7 @@ from ayon_server.settings import ( task_types_enum, ) -from ayon_server.types import ColorRGB_uint8, ColorRGBA_uint8 +from ayon_server.types import ColorRGBA_uint8 class ValidateBaseModel(BaseSettingsModel): @@ -221,7 +221,12 @@ class OIIOToolArgumentsModel(BaseSettingsModel): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): _layout = "expanded" - name: str = SettingsField("", title="Name") + name: str = SettingsField( + "", + title="Name", + description="Output name (no space)", + regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$", + ) extension: str = SettingsField("", title="Extension") transcoding_type: str = SettingsField( "colorspace", From 5ccd7f0143f5673cf063e11e943c3244b7b14036 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 15:36:42 +0100 Subject: [PATCH 243/284] Fix removal of `get_id_required_nodes` --- client/ayon_core/hosts/houdini/api/pipeline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index b9446933ac..787d0a01a1 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -307,10 +307,6 @@ def on_save(): # update houdini vars lib.update_houdini_vars_context_dialog() - nodes = lib.get_id_required_nodes() - for node, new_id in lib.generate_ids(nodes): - lib.set_id(node, new_id, overwrite=False) - # We are now starting the actual save directly global ABOUT_TO_SAVE ABOUT_TO_SAVE = False From 9669f62a44f8f33f5ae70e62986db807e0d5881e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Mar 2024 16:06:52 +0100 Subject: [PATCH 244/284] removed pysync from vendor --- .../ayon_core/vendor/python/common/pysync.py | 216 ------------------ 1 file changed, 216 deletions(-) delete mode 100644 client/ayon_core/vendor/python/common/pysync.py diff --git a/client/ayon_core/vendor/python/common/pysync.py b/client/ayon_core/vendor/python/common/pysync.py deleted file mode 100644 index 14a6dda34c..0000000000 --- a/client/ayon_core/vendor/python/common/pysync.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/local/bin/python3 -# https://github.com/snullp/pySync/blob/master/pySync.py - -import sys -import shutil -import os -import time -import configparser -from os.path import ( - getsize, - getmtime, - isfile, - isdir, - join, - abspath, - expanduser, - realpath -) -import logging - -log = logging.getLogger(__name__) - -ignoreFiles = ("Thumbs.db", ".DS_Store") - -# this feature is not yet implemented -ignorePaths = [] - -if os.name == 'nt': - # msvcrt can't function correctly in IDLE - if 'idlelib.run' in sys.modules: - print("Please don't run this script in IDLE.") - sys.exit(0) - import msvcrt - - def flush_input(str, set=None): - if not set: - while msvcrt.kbhit(): - ch = msvcrt.getch() - if ch == '\xff': - print("msvcrt is broken, this is weird.") - sys.exit(0) - return input(str) - else: - return set -else: - import select - - def flush_input(str, set=None): - if not set: - while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0: - os.read(sys.stdin.fileno(), 4096) - return input(str) - else: - return set - - -def compare(fa, fb, options_input=[]): - if isfile(fa) == isfile(fb): - if isdir(fa): - walktree(fa, fb, options_input) - elif isfile(fa): - if getsize(fa) != getsize(fb) \ - or int(getmtime(fa)) != int(getmtime(fb)): - log.info(str((fa, ': size=', getsize(fa), 'mtime=', - time.asctime(time.localtime(getmtime(fa)))))) - log.info(str((fb, ': size=', getsize(fb), 'mtime=', - time.asctime(time.localtime(getmtime(fb)))))) - if getmtime(fa) > getmtime(fb): - act = '>' - else: - act = '<' - - set = [i for i in options_input if i in [">", "<"]][0] - - s = flush_input('What to do?(>,<,r,n)[' + act + ']', set=set) - if len(s) > 0: - act = s[0] - if act == '>': - shutil.copy2(fa, fb) - elif act == '<': - shutil.copy2(fb, fa) - elif act == 'r': - if isdir(fa): - shutil.rmtree(fa) - elif isfile(fa): - os.remove(fa) - else: - log.info(str(('Remove: Skipping', fa))) - if isdir(fb): - shutil.rmtree(fb) - elif isfile(fb): - os.remove(fb) - else: - log.info(str(('Remove: Skipping', fb))) - - else: - log.debug(str(('Compare: Skipping non-dir and non-file', fa))) - else: - log.error(str(('Error:', fa, ',', fb, 'have different file type'))) - - -def copy(fa, fb, options_input=[]): - set = [i for i in options_input if i in ["y"]][0] - s = flush_input('Copy ' + fa + ' to another side?(r,y,n)[y]', set=set) - if len(s) > 0: - act = s[0] - else: - act = 'y' - if act == 'y': - if isdir(fa): - shutil.copytree(fa, fb) - elif isfile(fa): - shutil.copy2(fa, fb) - else: - log.debug(str(('Copy: Skipping ', fa))) - elif act == 'r': - if isdir(fa): - shutil.rmtree(fa) - elif isfile(fa): - os.remove(fa) - else: - log.debug(str(('Remove: Skipping ', fa))) - - -stoentry = [] -tarentry = [] - - -def walktree(source, target, options_input=[]): - srclist = os.listdir(source) - tarlist = os.listdir(target) - if '!sync' in srclist: - return - if '!sync' in tarlist: - return - # files in source dir... - for f in srclist: - if f in ignoreFiles: - continue - spath = join(source, f) - tpath = join(target, f) - if spath in ignorePaths: - continue - if spath in stoentry: - # just in case target also have this one - if f in tarlist: - del tarlist[tarlist.index(f)] - continue - - # if also exists in target dir - if f in tarlist: - del tarlist[tarlist.index(f)] - compare(spath, tpath, options_input) - - # exists in source dir only - else: - copy(spath, tpath, options_input) - - # exists in target dir only - set = [i for i in options_input if i in ["<"]] - - for f in tarlist: - if f in ignoreFiles: - continue - spath = join(source, f) - tpath = join(target, f) - if tpath in ignorePaths: - continue - if tpath in tarentry: - continue - if set: - copy(tpath, spath, options_input) - else: - print("REMOVING: {}".format(f)) - if os.path.isdir(tpath): - shutil.rmtree(tpath) - else: - os.remove(tpath) - print("REMOVING: {}".format(f)) - - -if __name__ == '__main__': - stoconf = configparser.RawConfigParser() - tarconf = configparser.RawConfigParser() - stoconf.read("pySync.ini") - tarconf.read(expanduser("~/.pysync")) - stoname = stoconf.sections()[0] - tarname = tarconf.sections()[0] - - # calculate storage's base folder - if stoconf.has_option(stoname, 'BASE'): - stobase = abspath(stoconf.get(stoname, 'BASE')) - stoconf.remove_option(stoname, 'BASE') - else: - stobase = os.getcwd() - - # same, for target's base folder - if tarconf.has_option(tarname, 'BASE'): - tarbase = abspath(tarconf.get(tarname, 'BASE')) - tarconf.remove_option(tarname, 'BASE') - else: - tarbase = expanduser('~/') - - print("Syncing between", stoname, "and", tarname) - sto_content = {x: realpath(join(stobase, stoconf.get(stoname, x))) - for x in stoconf.options(stoname)} - tar_content = {x: realpath(join(tarbase, tarconf.get(tarname, x))) - for x in tarconf.options(tarname)} - stoentry = [sto_content[x] for x in sto_content] - tarentry = [tar_content[x] for x in tar_content] - - for folder in sto_content: - if folder in tar_content: - print('Processing', folder) - walktree(sto_content[folder], tar_content[folder], options_input) - print("Done.") From 350c40d77a39bcbeff9987c848c5bc2754adf76e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 14:09:05 +0100 Subject: [PATCH 245/284] Do not error with confusing message if shadingEngine has no material for whatever reason --- .../plugins/publish/validate_look_shading_group.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py index e70a805de4..070974aef5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py @@ -47,10 +47,18 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin, shape, destination=True, type="shadingEngine" ) or [] for shading_engine in shading_engines: - name = ( - cmds.listConnections(shading_engine + ".surfaceShader")[0] - + "SG" + materials = cmds.listConnections( + shading_engine + ".surfaceShader", + source=True, destination=False ) + if not materials: + cls.log.warning( + "Shading engine '{}' has no material connected to its " + ".surfaceShader attribute.".format(shading_engine)) + continue + + material = materials[0] # there should only ever be one input + name = material + "SG" if shading_engine != name: invalid.append(shading_engine) From 5939e00fe7e894631156a192d8b338bbc51c0cb2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:15:00 +0100 Subject: [PATCH 246/284] remove pype ascii art --- client/ayon_core/lib/terminal.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/ayon_core/lib/terminal.py b/client/ayon_core/lib/terminal.py index a22f2358aa..10fcc79a27 100644 --- a/client/ayon_core/lib/terminal.py +++ b/client/ayon_core/lib/terminal.py @@ -1,15 +1,5 @@ # -*- coding: utf-8 -*- """Package helping with colorizing and formatting terminal output.""" -# :: -# //. ... .. ///. //. -# ///\\\ \\\ \\ ///\\\ /// -# /// \\ \\\ \\ /// \\ /// // -# \\\ // \\\ // \\\ // \\\// ./ -# \\\// \\\// \\\// \\\' // -# \\\ \\\ \\\ \\\// -# ''' ''' ''' ''' -# ..---===[[ PyP3 Setup ]]===---... -# import re import time import threading From ca06fb8ef0bd630d6094004490e25cd1f4f48b04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:15:14 +0100 Subject: [PATCH 247/284] remove legacy_io.py --- client/ayon_core/pipeline/legacy_io.py | 36 -------------------------- 1 file changed, 36 deletions(-) delete mode 100644 client/ayon_core/pipeline/legacy_io.py diff --git a/client/ayon_core/pipeline/legacy_io.py b/client/ayon_core/pipeline/legacy_io.py deleted file mode 100644 index d5b555845b..0000000000 --- a/client/ayon_core/pipeline/legacy_io.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from ayon_core.pipeline import get_current_project_name - -Session = {} - -log = logging.getLogger(__name__) -log.warning( - "DEPRECATION WARNING: 'legacy_io' is deprecated and will be removed in" - " future versions of ayon-core addon." - "\nReading from Session won't give you updated information and changing" - " values won't affect global state of a process." -) - - -def session_data_from_environment(context_keys=False): - return {} - - -def is_installed(): - return False - - -def install(): - pass - - -def uninstall(): - pass - - -def active_project(*args, **kwargs): - return get_current_project_name() - - -def current_project(*args, **kwargs): - return get_current_project_name() From 38d974c55e67cc70524d9ed72ec98bfdb9ecd5df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:17:04 +0100 Subject: [PATCH 248/284] removed unused 'debug_host' function --- client/ayon_core/pipeline/context_tools.py | 41 ---------------------- 1 file changed, 41 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 84a17be8f2..9bb62dab79 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -281,47 +281,6 @@ def deregister_host(): _registered_host["_"] = None -def debug_host(): - """A debug host, useful to debugging features that depend on a host""" - - host = types.ModuleType("debugHost") - - def ls(): - containers = [ - { - "representation": "ee-ft-a-uuid1", - "schema": "openpype:container-1.0", - "name": "Bruce01", - "objectName": "Bruce01_node", - "namespace": "_bruce01_", - "version": 3, - }, - { - "representation": "aa-bc-s-uuid2", - "schema": "openpype:container-1.0", - "name": "Bruce02", - "objectName": "Bruce01_node", - "namespace": "_bruce02_", - "version": 2, - } - ] - - for container in containers: - yield container - - host.__dict__.update({ - "ls": ls, - "open_file": lambda fname: None, - "save_file": lambda fname: None, - "current_file": lambda: os.path.expanduser("~/temp.txt"), - "has_unsaved_changes": lambda: False, - "work_root": lambda: os.path.expanduser("~/temp"), - "file_extensions": lambda: ["txt"], - }) - - return host - - def get_current_host_name(): """Current host name. From a056d65d888c1743e54195ff4a253347578632a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:17:48 +0100 Subject: [PATCH 249/284] removed unused 'get_workdir_from_session' and 'get_custom_workfile_template_from_session' --- client/ayon_core/pipeline/context_tools.py | 75 ---------------------- 1 file changed, 75 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 9bb62dab79..db5f849ae5 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -474,81 +474,6 @@ def get_current_context_template_data(settings=None): ) -def get_workdir_from_session(session=None, template_key=None): - """Template data for template fill from session keys. - - Args: - session (Union[Dict[str, str], None]): The Session to use. If not - provided use the currently active global Session. - template_key (str): Prepared template key from which workdir is - calculated. - - Returns: - str: Workdir path. - """ - - if session is not None: - project_name = session["AYON_PROJECT_NAME"] - host_name = session["AYON_HOST_NAME"] - else: - project_name = get_current_project_name() - host_name = get_current_host_name() - template_data = get_template_data_from_session(session) - - if not template_key: - task_type = template_data["task"]["type"] - template_key = get_workfile_template_key( - project_name, - task_type, - host_name, - ) - - anatomy = Anatomy(project_name) - template_obj = anatomy.get_template_item("work", template_key, "directory") - path = template_obj.format_strict(template_data) - if path: - path = os.path.normpath(path) - return path - - -def get_custom_workfile_template_from_session( - session=None, project_settings=None -): - """Filter and fill workfile template profiles by current context. - - This function cab be used only inside host where context is set. - - Args: - session (Optional[Dict[str, str]]): Session from which are taken - data. - project_settings(Optional[Dict[str, Any]]): Project settings. - - Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - """ - - if session is not None: - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] - host_name = session["AYON_HOST_NAME"] - else: - context = get_current_context() - project_name = context["project_name"] - folder_path = context["folder_path"] - task_name = context["task_name"] - host_name = get_current_host_name() - - return get_custom_workfile_template_by_string_context( - project_name, - folder_path, - task_name, - host_name, - project_settings=project_settings - ) - - def get_current_context_custom_workfile_template(project_settings=None): """Filter and fill workfile template profiles by current context. From 98746c30a2455e881ea0eef686b54fc75e28f40c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:18:30 +0100 Subject: [PATCH 250/284] fill or dix docstrings and readme files --- client/ayon_core/pipeline/context_tools.py | 24 +++++++++++++++++----- client/ayon_core/pipeline/create/README.md | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index db5f849ae5..ca409fadf2 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -97,8 +97,8 @@ def install_host(host): """Install `host` into the running Python session. Args: - host (module): A Python module containing the Avalon - avalon host-interface. + host (HostBase): A host interface object. + """ global _is_installed @@ -154,6 +154,13 @@ def install_host(host): def install_ayon_plugins(project_name=None, host_name=None): + """Install AYON core plugins and make sure the core is initialized. + + Args: + project_name (Optional[str]): Name of project to install plugins for. + host_name (Optional[str]): Name of host to install plugins for. + + """ # Make sure global AYON connection has set site id and version # - this is necessary if 'install_host' is not called initialize_ayon_connection() @@ -223,6 +230,12 @@ def install_ayon_plugins(project_name=None, host_name=None): def install_openpype_plugins(project_name=None, host_name=None): + """Install AYON core plugins and make sure the core is initialized. + + Deprecated: + Use `install_ayon_plugins` instead. + + """ install_ayon_plugins(project_name, host_name) @@ -306,7 +319,8 @@ def get_global_context(): Use 'get_current_context' to make sure you'll get current host integration context info. - Example: + Example:: + { "project_name": "Commercial", "folder_path": "Bunny", @@ -477,10 +491,10 @@ def get_current_context_template_data(settings=None): def get_current_context_custom_workfile_template(project_settings=None): """Filter and fill workfile template profiles by current context. - This function can be used only inside host where context is set. + This function can be used only inside host where current context is set. Args: - project_settings(Optional[Dict[str, Any]]): Project settings. + project_settings (Optional[dict[str, Any]]): Project settings Returns: str: Path to template or None if none of profiles match current diff --git a/client/ayon_core/pipeline/create/README.md b/client/ayon_core/pipeline/create/README.md index bbfd1bfa0f..09d3a22222 100644 --- a/client/ayon_core/pipeline/create/README.md +++ b/client/ayon_core/pipeline/create/README.md @@ -8,7 +8,7 @@ Discovers Creator plugins to be able create new instances and convert existing i Publish plugins are loaded because they can also define attributes definitions. These are less product type specific To be able define attributes Publish plugin must inherit from `AYONPyblishPluginMixin` and must override `get_attribute_defs` class method which must return list of attribute definitions. Values of publish plugin definitions are stored per plugin name under `publish_attributes`. Also can override `convert_attribute_values` class method which gives ability to modify values on instance before are used in CreatedInstance. Method `convert_attribute_values` can be also used without `get_attribute_defs` to modify values when changing compatibility (remove metadata from instance because are irrelevant). -Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. +Possible attribute definitions can be found in `ayon_core/lib/attribute_definitions.py`. Except creating and removing instances are all changes not automatically propagated to host context (scene/workfile/...) to propagate changes call `save_changes` which trigger update of all instances in context using Creators implementation. From fce92456c102870c33055501f59a50307db4be8f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 14:18:46 +0100 Subject: [PATCH 251/284] remove deprecated widgets --- client/ayon_core/widgets/__init__.py | 0 client/ayon_core/widgets/password_dialog.py | 33 --------------------- 2 files changed, 33 deletions(-) delete mode 100644 client/ayon_core/widgets/__init__.py delete mode 100644 client/ayon_core/widgets/password_dialog.py diff --git a/client/ayon_core/widgets/__init__.py b/client/ayon_core/widgets/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/widgets/password_dialog.py b/client/ayon_core/widgets/password_dialog.py deleted file mode 100644 index a4c50128ff..0000000000 --- a/client/ayon_core/widgets/password_dialog.py +++ /dev/null @@ -1,33 +0,0 @@ -# TODO remove - kept for kitsu addon which imported it -from qtpy import QtWidgets, QtCore, QtGui - - -class PressHoverButton(QtWidgets.QPushButton): - """ - Deprecated: - Use `openpype.tools.utils.PressHoverButton` instead. - """ - _mouse_pressed = False - _mouse_hovered = False - change_state = QtCore.Signal(bool) - - def mousePressEvent(self, event): - self._mouse_pressed = True - self._mouse_hovered = True - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self._mouse_pressed = False - self._mouse_hovered = False - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mouseReleaseEvent(event) - - def mouseMoveEvent(self, event): - mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) - under_mouse = self.rect().contains(mouse_pos) - if under_mouse != self._mouse_hovered: - self._mouse_hovered = under_mouse - self.change_state.emit(self._mouse_hovered) - - super(PressHoverButton, self).mouseMoveEvent(event) From 4a95f97f195b0b3f7caddc06b1a558644a373b58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 14:41:34 +0100 Subject: [PATCH 252/284] Support SelectInvalidAction in Maya for ContextPlugin --- client/ayon_core/hosts/maya/api/action.py | 32 ++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/action.py b/client/ayon_core/hosts/maya/api/action.py index baf558036e..d845ac6066 100644 --- a/client/ayon_core/hosts/maya/api/action.py +++ b/client/ayon_core/hosts/maya/api/action.py @@ -4,7 +4,10 @@ from __future__ import absolute_import import pyblish.api import ayon_api -from ayon_core.pipeline.publish import get_errored_instances_from_context +from ayon_core.pipeline.publish import ( + get_errored_instances_from_context, + get_errored_plugins_from_context +) class GenerateUUIDsOnInvalidAction(pyblish.api.Action): @@ -112,20 +115,25 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Maya") - errored_instances = get_errored_instances_from_context(context, - plugin=plugin) - # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in errored_instances: - invalid_nodes = plugin.get_invalid(instance) - if invalid_nodes: - if isinstance(invalid_nodes, (list, tuple)): - invalid.extend(invalid_nodes) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") + if issubclass(plugin, pyblish.api.ContextPlugin): + errored_plugins = get_errored_plugins_from_context(context) + if plugin in errored_plugins: + invalid = plugin.get_invalid(context) + else: + errored_instances = get_errored_instances_from_context( + context, plugin=plugin + ) + for instance in errored_instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") # Ensure unique (process each node only once) invalid = list(set(invalid)) From 1099c391d356ea4eb563bd6290acf18a2d8adac4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 14:56:07 +0100 Subject: [PATCH 253/284] Use explicit plug-ins from pyblish api instead of legacy plug-ins Use explicit pyblish api orders, instead of order of legacy plug-ins --- .../plugins/publish/validate_mesh_no_negative_scale.py | 2 +- .../celaction/plugins/publish/collect_celaction_cli_kwargs.py | 4 ++-- .../hosts/maya/plugins/publish/validate_color_sets.py | 2 +- .../hosts/maya/plugins/publish/validate_mesh_ngons.py | 2 +- .../maya/plugins/publish/validate_mesh_no_negative_scale.py | 2 +- .../hosts/maya/plugins/publish/validate_mesh_non_manifold.py | 2 +- .../maya/plugins/publish/validate_mesh_normals_unlocked.py | 2 +- .../hosts/maya/plugins/publish/validate_no_animation.py | 2 +- .../hosts/maya/plugins/publish/validate_shape_render_stats.py | 2 +- .../hosts/maya/plugins/publish/validate_shape_zero.py | 2 +- .../hosts/maya/plugins/publish/validate_transform_zero.py | 2 +- .../hosts/maya/plugins/publish/validate_unique_names.py | 2 +- .../plugins/publish/validate_yeti_rig_input_in_instance.py | 2 +- .../hosts/nuke/plugins/publish/extract_script_save.py | 4 ++-- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 3 ++- 15 files changed, 18 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 63b7dc7530..fb16bb7f8d 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import ( import ayon_core.hosts.blender.api.action -class ValidateMeshNoNegativeScale(pyblish.api.Validator, +class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale.""" diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py index 54dea15dff..1820569918 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py @@ -3,11 +3,11 @@ import sys from pprint import pformat -class CollectCelactionCliKwargs(pyblish.api.Collector): +class CollectCelactionCliKwargs(pyblish.api.ContextPlugin): """ Collects all keyword arguments passed from the terminal """ label = "Collect Celaction Cli Kwargs" - order = pyblish.api.Collector.order - 0.1 + order = pyblish.api.CollectorOrder - 0.1 def process(self, context): args = list(sys.argv[1:]) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py b/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py index e69717fad0..f70b46f89e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateColorSets(pyblish.api.Validator, +class ValidateColorSets(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index b6d3dc73fd..d1d7e49fa4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateMeshNgons(pyblish.api.Validator, +class ValidateMeshNgons(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have ngons diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py index ff1dca87cf..bf1489f92e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py @@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNoNegativeScale(pyblish.api.Validator, +class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale. diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py index 6dbad538ef..3ca6742d98 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py @@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNonManifold(pyblish.api.Validator, +class ValidateMeshNonManifold(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have non-manifold edges or vertices diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py index 1790a94580..76b716d01f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py @@ -18,7 +18,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNormalsUnlocked(pyblish.api.Validator, +class ValidateMeshNormalsUnlocked(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py index 6e0719628f..bf45c0e974 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py @@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateNoAnimation(pyblish.api.Validator, +class ValidateNoAnimation(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure no keyframes on nodes in the Instance. diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py index 2783a6dbe8..31fb084439 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateShapeRenderStats(pyblish.api.Validator, +class ValidateShapeRenderStats(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure all render stats are set to the default values.""" diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py index 4f4826776c..6c89258085 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py @@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateShapeZero(pyblish.api.Validator, +class ValidateShapeZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Shape components may not have any "tweak" values diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index 1cbdd05b0b..8d1dca56d3 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateTransformZero(pyblish.api.Validator, +class ValidateTransformZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Transforms can't have any values diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py index 72c3c7dc72..0066d70531 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py @@ -9,7 +9,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateUniqueNames(pyblish.api.Validator, +class ValidateUniqueNames(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """transform names should be unique diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py index aa229875fe..77e189e37b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator, +class ValidateYetiRigInputShapesInInstance(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate if all input nodes are part of the instance's hierarchy""" diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py index e44e5686b6..d325684a7c 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py @@ -2,10 +2,10 @@ import nuke import pyblish.api -class ExtractScriptSave(pyblish.api.Extractor): +class ExtractScriptSave(pyblish.api.InstancePlugin): """Save current Nuke workfile script""" label = 'Script Save' - order = pyblish.api.Extractor.order - 0.1 + order = pyblish.api.ExtractorOrder - 0.1 hosts = ['nuke'] def process(self, instance): diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py b/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py index ab30e3dc10..fe5e148b7b 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -25,8 +25,9 @@ from ayon_core.hosts.tvpaint.lib import ( ) -class ExtractSequence(pyblish.api.Extractor): +class ExtractSequence(pyblish.api.InstancePlugin): label = "Extract Sequence" + order = pyblish.api.ExtractorOrder hosts = ["tvpaint"] families = ["review", "render"] From 27793cef1f93f87e75bd1652929f8c4da5deccc0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 16:06:57 +0100 Subject: [PATCH 254/284] added helper object to handle selection in launcher --- client/ayon_core/pipeline/actions.py | 237 +++++++++++++++++- .../tools/launcher/models/actions.py | 62 ++--- 2 files changed, 248 insertions(+), 51 deletions(-) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 8e0ce7e583..d6e589c0ef 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -1,4 +1,7 @@ import logging + +import ayon_api + from ayon_core.pipeline.plugin_discover import ( discover, register_plugin, @@ -10,6 +13,224 @@ from ayon_core.pipeline.plugin_discover import ( from .load.utils import get_representation_path_from_context +class LauncherActionSelection: + """Object helper to pass selection to actions. + + Object support backwards compatibility for 'session' from OpenPype where + environment variable keys were used to define selection. + + Args: + project_name (str): Selected project name. + folder_id (str): Selected folder id. + task_id (str): Selected task id. + folder_path (Optional[str]): Selected folder path. + task_name (Optional[str]): Selected task name. + project_entity (Optional[dict[str, Any]]): Project entity. + folder_entity (Optional[dict[str, Any]]): Folder entity. + task_entity (Optional[dict[str, Any]]): Task entity. + + """ + def __init__( + self, + project_name, + folder_id, + task_id, + folder_path=None, + task_name=None, + project_entity=None, + folder_entity=None, + task_entity=None + ): + self._project_name = project_name + self._folder_id = folder_id + self._task_id = task_id + + self._folder_path = folder_path + self._task_name = task_name + + self._project_entity = project_entity + self._folder_entity = folder_entity + self._task_entity = task_entity + + def __getitem__(self, key): + if key in {"project_name", "AYON_PROJECT_NAME", "AVALON_PROJECT"}: + return self.project_name + if key == {"folder_path", "AYON_FOLDER_PATH", "AVALON_ASSET"}: + return self.folder_path + if key == {"task_name", "AYON_TASK_NAME", "AVALON_TASK"}: + return self.task_name + if key == "folder_id": + return self.folder_id + if key == "task_id": + return self.task_id + if key == "project_entity": + return self.project_entity + if key == "folder_entity": + return self.folder_entity + if key == "task_entity": + return self.task_entity + raise KeyError(f"Key: {key} not found") + + def __contains__(self, key): + # Fake missing keys check for backwards compatibility + if key in { + "AYON_PROJECT_NAME", + "AVALON_PROJECT", + "project_entity", + }: + return self._project_name is not None + if key in { + "AYON_FOLDER_PATH", + "folder_id", + "folder_path", + "folder_entity", + "AVALON_ASSET", + }: + return self._folder_id is not None + if key in { + "AYON_TASK_NAME", + "task_id", + "task_name", + "task_entity", + "AVALON_TASK", + }: + return self._task_id is not None + return False + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def get_project_name(self): + """Selected project name. + + Returns: + Union[str, None]: Selected project name. + + """ + return self._project_name + + def get_folder_id(self): + """Selected folder id. + + Returns: + Union[str, None]: Selected folder id. + + """ + return self._folder_id + + def get_folder_path(self): + """Selected folder path. + + Returns: + Union[str, None]: Selected folder path. + + """ + if self._folder_id is None: + return None + if self._folder_path is None: + self._folder_path = self.folder_entity["path"] + return self._folder_path + + def get_task_id(self): + """Selected task id. + + Returns: + Union[str, None]: Selected task id. + + """ + return self._task_id + + def get_task_name(self): + """Selected task name. + + Returns: + Union[str, None]: Selected task name. + + """ + if self._task_id is None: + return None + if self._task_name is None: + self._task_name = self.task_entity["name"] + return self._task_name + + def get_project_entity(self): + """Project entity for the selection. + + Returns: + Union[dict[str, Any], None]: Project entity. + + """ + if self._project_name is None: + return None + if self._project_entity is None: + self._project_entity = ayon_api.get_project(self._project_name) + return self._project_entity + + def get_folder_entity(self): + """Folder entity for the selection. + + Returns: + Union[dict[str, Any], None]: Folder entity. + + """ + if self._project_name is None or self._folder_id is None: + return None + if self._folder_entity is None: + self._folder_entity = ayon_api.get_folder_by_id( + self._project_name, self._folder_id + ) + return self._folder_entity + + def get_task_entity(self): + """Task entity for the selection. + + Returns: + Union[dict[str, Any], None]: Task entity. + + """ + if ( + self._project_name is None + or self._task_id is None + ): + return None + if self._task_entity is None: + self._task_entity = ayon_api.get_task_by_id( + self._project_name, self._task_id + ) + return self._task_entity + + @property + def is_project_selected(self): + """Return whether a project is selected. + + Returns: + bool: Whether a project is selected. + + """ + return self._project_name is not None + + @property + def is_folder_selected(self): + return self._folder_id is not None + + @property + def is_task_selected(self): + return self._task_id is not None + + project_name = property(get_project_name) + folder_id = property(get_folder_id) + task_id = property(get_task_id) + folder_path = property(get_folder_path) + task_name = property(get_task_name) + + project_entity = property(get_project_entity) + folder_entity = property(get_folder_entity) + task_entity = property(get_task_entity) + + class LauncherAction(object): """A custom action available""" name = None @@ -21,17 +242,23 @@ class LauncherAction(object): log = logging.getLogger("LauncherAction") log.propagate = True - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the class is compatible with the Session. Args: - session (dict[str, Union[str, None]]): Session data with - AYON_PROJECT_NAME, AYON_FOLDER_PATH and AYON_TASK_NAME. - """ + selection (LauncherActionSelection): Data with selection. + """ return True - def process(self, session, **kwargs): + def process(self, selection, **kwargs): + """Process the action. + + Args: + selection (LauncherActionSelection): Data with selection. + **kwargs: Additional arguments. + + """ pass diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 97943e6ad7..6da34151b6 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -5,6 +5,7 @@ from ayon_core.lib import Logger, AYONSettingsRegistry from ayon_core.pipeline.actions import ( discover_launcher_actions, LauncherAction, + LauncherActionSelection, ) from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch @@ -69,11 +70,6 @@ class ApplicationAction(LauncherAction): project_entities = {} _log = None - required_session_keys = ( - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME" - ) @property def log(self): @@ -81,18 +77,16 @@ class ApplicationAction(LauncherAction): self._log = Logger.get_logger(self.__class__.__name__) return self._log - def is_compatible(self, session): - for key in self.required_session_keys: - if not session.get(key): - return False + def is_compatible(self, selection): + if not selection.is_task_selected: + return False - project_name = session["AYON_PROJECT_NAME"] - project_entity = self.project_entities[project_name] + project_entity = self.project_entities[selection.project_name] apps = project_entity["attrib"].get("applications") if not apps or self.application.full_name not in apps: return False - project_settings = self.project_settings[project_name] + project_settings = self.project_settings[selection.project_name] only_available = project_settings["applications"]["only_available"] if only_available and not self.application.find_executable(): return False @@ -112,7 +106,7 @@ class ApplicationAction(LauncherAction): dialog.setDetailedText(details) dialog.exec_() - def process(self, session, **kwargs): + def process(self, selection, **kwargs): """Process the full Application action""" from ayon_core.lib import ( @@ -120,14 +114,11 @@ class ApplicationAction(LauncherAction): ApplicationLaunchFailed, ) - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] try: self.application.launch( - project_name=project_name, - folder_path=folder_path, - task_name=task_name, + project_name=selection.project_name, + folder_path=selection.folder_path, + task_name=selection.task_name, **self.data ) @@ -335,11 +326,11 @@ class ActionsModel: """ not_open_workfile_actions = self._get_no_last_workfile_for_context( project_name, folder_id, task_id) - session = self._prepare_session(project_name, folder_id, task_id) + selection = self._prepare_selection(project_name, folder_id, task_id) output = [] action_items = self._get_action_items(project_name) for identifier, action in self._get_action_objects().items(): - if not action.is_compatible(session): + if not action.is_compatible(selection): continue action_item = action_items[identifier] @@ -374,7 +365,7 @@ class ActionsModel: ) def trigger_action(self, project_name, folder_id, task_id, identifier): - session = self._prepare_session(project_name, folder_id, task_id) + selection = self._prepare_selection(project_name, folder_id, task_id) failed = False error_message = None action_label = identifier @@ -403,7 +394,7 @@ class ActionsModel: ) action.data["start_last_workfile"] = start_last_workfile - action.process(session) + action.process(selection) except Exception as exc: self.log.warning("Action trigger failed.", exc_info=True) failed = True @@ -440,29 +431,8 @@ class ActionsModel: .get(task_id, {}) ) - def _prepare_session(self, project_name, folder_id, task_id): - folder_path = None - if folder_id: - folder = self._controller.get_folder_entity( - project_name, folder_id) - if folder: - folder_path = folder["path"] - - task_name = None - if task_id: - task = self._controller.get_task_entity(project_name, task_id) - if task: - task_name = task["name"] - - return { - "AYON_PROJECT_NAME": project_name, - "AYON_FOLDER_PATH": folder_path, - "AYON_TASK_NAME": task_name, - # Deprecated - kept for backwards compatibility - "AVALON_PROJECT": project_name, - "AVALON_ASSET": folder_path, - "AVALON_TASK": task_name, - } + def _prepare_selection(self, project_name, folder_id, task_id): + return LauncherActionSelection(project_name, folder_id, task_id) def _get_discovered_action_classes(self): if self._discovered_actions is None: From 93b9541268e0885052126ab647441db56be19a14 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 16:07:13 +0100 Subject: [PATCH 255/284] use new options of selection object --- .../launcher_actions/ClockifyStart.py | 14 ++++----- .../clockify/launcher_actions/ClockifySync.py | 12 ++++---- .../plugins/actions/open_file_explorer.py | 29 +++++++++---------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py index 61c5eac2f5..8381c7d73e 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py @@ -11,19 +11,17 @@ class ClockifyStart(LauncherAction): order = 500 clockify_api = ClockifyAPI() - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the action is compatible with the session""" - if "AYON_TASK_NAME" in session: - return True - return False + return selection.is_task_selected - def process(self, session, **kwargs): + def process(self, selection, **kwargs): self.clockify_api.set_api() user_id = self.clockify_api.user_id workspace_id = self.clockify_api.workspace_id - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] + project_name = selection.project_name + folder_path = selection.folder_path + task_name = selection.task_name description = "/".join([folder_path.lstrip("/"), task_name]) # fetch folder entity diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py index 72187c6d28..5388f47c98 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py @@ -19,15 +19,18 @@ class ClockifySync(LauncherAction): order = 500 clockify_api = ClockifyAPI() - def is_compatible(self, session): + def is_compatible(self, selection): """Check if there's some projects to sync""" + if selection.is_project_selected: + return True + try: next(ayon_api.get_projects()) return True except StopIteration: return False - def process(self, session, **kwargs): + def process(self, selection, **kwargs): self.clockify_api.set_api() workspace_id = self.clockify_api.workspace_id user_id = self.clockify_api.user_id @@ -37,10 +40,9 @@ class ClockifySync(LauncherAction): raise ClockifyPermissionsCheckFailed( "Current CLockify user is missing permissions for this action!" ) - project_name = session.get("AYON_PROJECT_NAME") or "" - if project_name.strip(): - projects_to_sync = [ayon_api.get_project(project_name)] + if selection.is_project_selected: + projects_to_sync = [selection.project_entity] else: projects_to_sync = ayon_api.get_projects() diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 69375a7859..6a456c75c1 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -18,18 +18,14 @@ class OpenTaskPath(LauncherAction): icon = "folder-open" order = 500 - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the action is compatible with the session""" - return bool(session.get("AYON_FOLDER_PATH")) + return selection.is_folder_selected - def process(self, session, **kwargs): + def process(self, selection, **kwargs): from qtpy import QtCore, QtWidgets - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session.get("AYON_TASK_NAME", None) - - path = self._get_workdir(project_name, folder_path, task_name) + path = self._get_workdir(selection) if not path: return @@ -60,16 +56,17 @@ class OpenTaskPath(LauncherAction): path = path.split(field, 1)[0] return path - def _get_workdir(self, project_name, folder_path, task_name): - project_entity = ayon_api.get_project(project_name) - folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name + def _get_workdir(self, selection): + data = get_template_data( + selection.project_entity, + selection.folder_entity, + selection.task_entity ) - data = get_template_data(project_entity, folder_entity, task_entity) - - anatomy = Anatomy(project_name) + anatomy = Anatomy( + selection.project_name, + project_entity=selection.project_entity + ) workdir = anatomy.get_template_item( "work", "default", "folder" ).format(data) From b6269b176516426a53bb227d8a390464623ec5a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:34:10 +0100 Subject: [PATCH 256/284] Add missing docstrings. Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/actions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index d6e589c0ef..52d68800d1 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -214,10 +214,22 @@ class LauncherActionSelection: @property def is_folder_selected(self): + """Return whether a folder is selected. + + Returns: + bool: Whether a folder is selected. + + """ return self._folder_id is not None @property def is_task_selected(self): + """Return whether a task is selected. + + Returns: + bool: Whether a task is selected. + + """ return self._task_id is not None project_name = property(get_project_name) From 24d6280ae68ebf1c3168635f472ed76fab21e882 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 16:36:21 +0100 Subject: [PATCH 257/284] Houdini: Fix creating instances from tab menu --- client/ayon_core/hosts/houdini/api/creator_node_shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py index 6e48cb375b..72c157f187 100644 --- a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py +++ b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py @@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs): pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() - project_name = context.get_current_project_name(), + project_name = context.get_current_project_name() folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() folder_entity = ayon_api.get_folder_by_path( From 66f3b4cbf6573d87f0dccd0a2f427aaf6f6cfef0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 16:39:42 +0100 Subject: [PATCH 258/284] keep dict like access only for backwards compatible keys --- client/ayon_core/pipeline/actions.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 52d68800d1..9ccc82e4a9 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -53,22 +53,12 @@ class LauncherActionSelection: self._task_entity = task_entity def __getitem__(self, key): - if key in {"project_name", "AYON_PROJECT_NAME", "AVALON_PROJECT"}: + if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}: return self.project_name - if key == {"folder_path", "AYON_FOLDER_PATH", "AVALON_ASSET"}: + if key == {"AYON_FOLDER_PATH", "AVALON_ASSET"}: return self.folder_path - if key == {"task_name", "AYON_TASK_NAME", "AVALON_TASK"}: + if key == {"AYON_TASK_NAME", "AVALON_TASK"}: return self.task_name - if key == "folder_id": - return self.folder_id - if key == "task_id": - return self.task_id - if key == "project_entity": - return self.project_entity - if key == "folder_entity": - return self.folder_entity - if key == "task_entity": - return self.task_entity raise KeyError(f"Key: {key} not found") def __contains__(self, key): @@ -76,22 +66,15 @@ class LauncherActionSelection: if key in { "AYON_PROJECT_NAME", "AVALON_PROJECT", - "project_entity", }: return self._project_name is not None if key in { "AYON_FOLDER_PATH", - "folder_id", - "folder_path", - "folder_entity", "AVALON_ASSET", }: return self._folder_id is not None if key in { "AYON_TASK_NAME", - "task_id", - "task_name", - "task_entity", "AVALON_TASK", }: return self._task_id is not None From 8fab7139694d74ab958f5eefeb9426e8d8576c23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 16:43:42 +0100 Subject: [PATCH 259/284] added deprecation warnings --- client/ayon_core/pipeline/actions.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 9ccc82e4a9..9b5234d811 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -1,4 +1,5 @@ import logging +import warnings import ayon_api @@ -53,6 +54,14 @@ class LauncherActionSelection: self._task_entity = task_entity def __getitem__(self, key): + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}: return self.project_name if key == {"AYON_FOLDER_PATH", "AVALON_ASSET"}: @@ -62,6 +71,14 @@ class LauncherActionSelection: raise KeyError(f"Key: {key} not found") def __contains__(self, key): + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) # Fake missing keys check for backwards compatibility if key in { "AYON_PROJECT_NAME", @@ -81,6 +98,14 @@ class LauncherActionSelection: return False def get(self, key, default=None): + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) try: return self[key] except KeyError: From b75a3bca4f1dc0d159e265066a73a481f757997c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 16:57:10 +0100 Subject: [PATCH 260/284] fix in conditions --- client/ayon_core/pipeline/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 9b5234d811..0d2a05f7cd 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -64,9 +64,9 @@ class LauncherActionSelection: ) if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}: return self.project_name - if key == {"AYON_FOLDER_PATH", "AVALON_ASSET"}: + if key in {"AYON_FOLDER_PATH", "AVALON_ASSET"}: return self.folder_path - if key == {"AYON_TASK_NAME", "AVALON_TASK"}: + if key in {"AYON_TASK_NAME", "AVALON_TASK"}: return self.task_name raise KeyError(f"Key: {key} not found") From 5b5ab5df8b93e07ce19dc5e5c3654bcb184c57cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 17:00:24 +0100 Subject: [PATCH 261/284] implemented rest of dict-like methods --- client/ayon_core/pipeline/actions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 0d2a05f7cd..a2eb8e7eee 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -70,6 +70,10 @@ class LauncherActionSelection: return self.task_name raise KeyError(f"Key: {key} not found") + def __iter__(self): + for key in self.keys(): + yield key + def __contains__(self, key): warnings.warn( ( @@ -111,6 +115,23 @@ class LauncherActionSelection: except KeyError: return default + def items(self): + for key, value in ( + ("AYON_PROJECT_NAME", self.project_name), + ("AYON_FOLDER_PATH", self.folder_path), + ("AYON_TASK_NAME", self.task_name), + ): + if value is not None: + yield (key, value) + + def keys(self): + for key, _ in self.items(): + yield key + + def values(self): + for _, value in self.items(): + yield value + def get_project_name(self): """Selected project name. From 2b149b50ee3a6e019b4cdeb75c446a460af60a54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Mar 2024 17:28:16 +0100 Subject: [PATCH 262/284] added deprecation to method docstrings --- client/ayon_core/pipeline/actions.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index a2eb8e7eee..eae2fc94b5 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -102,6 +102,12 @@ class LauncherActionSelection: return False def get(self, key, default=None): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ warnings.warn( ( "Using deprecated access to selection data. Please use" @@ -116,6 +122,12 @@ class LauncherActionSelection: return default def items(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ for key, value in ( ("AYON_PROJECT_NAME", self.project_name), ("AYON_FOLDER_PATH", self.folder_path), @@ -125,10 +137,22 @@ class LauncherActionSelection: yield (key, value) def keys(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ for key, _ in self.items(): yield key def values(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ for _, value in self.items(): yield value From 6aeea664c63c5d5e47a12350508e157919f3523b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 17:36:17 +0100 Subject: [PATCH 263/284] Rename `Options` to `Context` --- client/ayon_core/plugins/publish/help/validate_containers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/help/validate_containers.xml b/client/ayon_core/plugins/publish/help/validate_containers.xml index 5d18bb4c19..321e73a303 100644 --- a/client/ayon_core/plugins/publish/help/validate_containers.xml +++ b/client/ayon_core/plugins/publish/help/validate_containers.xml @@ -10,7 +10,7 @@ Scene contains one or more outdated loaded containers, eg. versions loaded into ### How to repair? Use 'Scene Inventory' and update all highlighted old container to latest OR - refresh Publish and switch 'Validate Containers' toggle on 'Options' tab. +refresh Publish and switch 'Validate Containers' toggle on 'Context' tab. WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets. From 0a300c56c29452d28af4d27d3b7209a499150fba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 19:44:16 +0100 Subject: [PATCH 264/284] Fix typos --- client/ayon_core/plugins/publish/extract_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 905158c851..1c8214e829 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1374,7 +1374,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Make sure output width and height is not an odd number # When this can happen: # - if output definition has set width and height with odd number - # - `instance.data` contain width and height with odd numbeer + # - `instance.data` contain width and height with odd number if output_width % 2 != 0: self.log.warning(( "Converting output width from odd to even number. {} -> {}" @@ -1820,8 +1820,8 @@ class OverscanCrop: """ # crop=width:height:x:y - explicit start x, y position # crop=width:height - x, y are related to center by width/height - # pad=width:heigth:x:y - explicit start x, y position - # pad=width:heigth - x, y are set to 0 by default + # pad=width:height:x:y - explicit start x, y position + # pad=width:height - x, y are set to 0 by default width = self.width() height = self.height() From 23a8df847482316ddf525e2f71fb01bae79bb7fa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 19:44:30 +0100 Subject: [PATCH 265/284] Remove unused variables --- client/ayon_core/plugins/publish/extract_review.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 1c8214e829..6b90fe6e61 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1228,16 +1228,6 @@ class ExtractReview(pyblish.api.InstancePlugin): reformat_in_baking = bool("reformated" in new_repre["tags"]) self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking)) - # Get instance data - pixel_aspect = temp_data["pixel_aspect"] - - if reformat_in_baking: - self.log.debug(( - "Using resolution from input. It is already " - "reformated from upstream process" - )) - pixel_aspect = 1 - # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] try: From 5a546c35ade165e4b8acd1357fea44219dd0b19b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 19:44:54 +0100 Subject: [PATCH 266/284] Fix typo --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 6b90fe6e61..b50b415537 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1258,7 +1258,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if reformat_in_baking: self.log.debug(( "Using resolution from input. It is already " - "reformated from upstream process" + "reformatted from upstream process" )) pixel_aspect = 1 output_width = input_width From 447e3156af3dca3cc4c8798ad820ba3d873460ba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 19:53:03 +0100 Subject: [PATCH 267/284] Opt-out earlier if no burnins per representation to process --- client/ayon_core/plugins/publish/extract_burnin.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index ab6353a29f..727d7f1bc2 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -194,6 +194,16 @@ class ExtractBurnin(publish.Extractor): ).format(host_name, product_type, task_name, profile)) return + burnins_per_repres = self._get_burnins_per_representations( + instance, burnin_defs + ) + if not burnins_per_repres: + self.log.debug( + "Skipped instance. No representations found matching a burnin" + "definition in: %s", burnin_defs + ) + return + burnin_options = self._get_burnin_options() # Prepare basic data for processing @@ -204,9 +214,6 @@ class ExtractBurnin(publish.Extractor): # Args that will execute the script executable_args = ["run", scriptpath] - burnins_per_repres = self._get_burnins_per_representations( - instance, burnin_defs - ) for repre, repre_burnin_defs in burnins_per_repres: # Create copy of `_burnin_data` and `_temp_data` for repre. burnin_data = copy.deepcopy(_burnin_data) From caebcd2b43d480360064fac78d783dd8f29a9c56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 10:32:02 +0100 Subject: [PATCH 268/284] Support making maya ExtractGpuCache optional --- .../hosts/maya/plugins/publish/extract_gpu_cache.py | 6 +++++- server_addon/maya/server/settings/publishers.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py index 19825b769c..4b293b5785 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -5,7 +5,8 @@ from maya import cmds from ayon_core.pipeline import publish -class ExtractGPUCache(publish.Extractor): +class ExtractGPUCache(publish.Extractor, + publish.OptionalPyblishPluginMixin): """Extract the content of the instance to a GPU cache file.""" label = "GPU Cache" @@ -20,6 +21,9 @@ class ExtractGPUCache(publish.Extractor): useBaseTessellation = True def process(self, instance): + if not self.is_active(instance.data): + return + cmds.loadPlugin("gpuCache", quiet=True) staging_dir = self.staging_dir(instance) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index f1e63f36be..fa670b5b90 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -373,6 +373,8 @@ class ExtractLookModel(BaseSettingsModel): class ExtractGPUCacheModel(BaseSettingsModel): enabled: bool = True + optional: bool = True + active: bool = True families: list[str] = SettingsField(default_factory=list, title="Families") step: float = SettingsField(1.0, ge=1.0, title="Step") stepSave: int = SettingsField(1, ge=1, title="Step Save") @@ -1341,6 +1343,8 @@ DEFAULT_PUBLISH_SETTINGS = { }, "ExtractGPUCache": { "enabled": False, + "optional": False, + "active": True, "families": [ "model", "animation", From 45b912c42b7853cd615c096b1bbda33b2a3e008c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 10:41:31 +0100 Subject: [PATCH 269/284] Use `SettingsField` --- server_addon/maya/server/settings/publishers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index fa670b5b90..dc00c41627 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -372,9 +372,9 @@ class ExtractLookModel(BaseSettingsModel): class ExtractGPUCacheModel(BaseSettingsModel): - enabled: bool = True - optional: bool = True - active: bool = True + enabled: bool = SettingsField(title="Enabled") + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") families: list[str] = SettingsField(default_factory=list, title="Families") step: float = SettingsField(1.0, ge=1.0, title="Step") stepSave: int = SettingsField(1, ge=1, title="Step Save") From bae17cf95e1bf55725f42b56ff7082734112805a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 10:48:39 +0100 Subject: [PATCH 270/284] Expose `ExtractModel` (Model Maya Scene) export to settings --- server_addon/maya/server/settings/publishers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index dc00c41627..27288053a2 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -316,6 +316,12 @@ class ExtractObjModel(BaseSettingsModel): optional: bool = SettingsField(title="Optional") +class ExtractModelModel(BaseSettingsModel): + enabled: bool = SettingsField(title="Enabled") + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") + + class ExtractMayaSceneRawModel(BaseSettingsModel): """Add loaded instances to those published families:""" enabled: bool = SettingsField(title="ExtractMayaSceneRaw") @@ -801,6 +807,10 @@ class PublishersModel(BaseSettingsModel): default_factory=ExtractGPUCacheModel, title="Extract GPU Cache", ) + ExtractModel: ExtractModelModel = SettingsField( + default_factory=ExtractModelModel, + title="Extract Model (Maya Scene)" + ) DEFAULT_SUFFIX_NAMING = { @@ -1357,5 +1367,10 @@ DEFAULT_PUBLISH_SETTINGS = { "optimizeAnimationsForMotionBlur": True, "writeMaterials": True, "useBaseTessellation": True + }, + "ExtractModel": { + "enabled": True, + "optional": True, + "active": True, } } From e9f038fde6570cbcd7d877728f30ff8443a0426f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 10:55:15 +0100 Subject: [PATCH 271/284] Bump Maya server addon version --- server_addon/maya/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 71b4bc4ca6..1a4f79a972 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.12" +__version__ = "0.1.13" From 5f0f7afd434f020aeeacffeef037dbc6ad2db73d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 11:56:50 +0100 Subject: [PATCH 272/284] Allow loading `usd` to substance painter --- .../ayon_core/hosts/substancepainter/plugins/load/load_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py index f2254c0907..d940d7b05c 100644 --- a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py +++ b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py @@ -18,7 +18,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" product_types = {"*"} - representations = ["abc", "fbx", "obj", "gltf"] + representations = ["abc", "fbx", "obj", "gltf", "usd", "usda", "usdc"] label = "Load mesh" order = -10 From 973ac33aa4f883f6f1bc75db63dc1ef5494fe7a8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 13:26:38 +0100 Subject: [PATCH 273/284] Do not recreate the same `dict`. `get_last_versions` already returns by `productId` --- client/ayon_core/hosts/maya/api/lib.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index b18d3a0c33..28feff6a37 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1981,14 +1981,10 @@ def assign_look(nodes, product_name="lookDefault"): product_entity["id"] for product_entity in product_entities_by_folder_id.values() } - last_version_entities = ayon_api.get_last_versions( + last_version_entities_by_product_id = ayon_api.get_last_versions( project_name, product_ids ) - last_version_entities_by_product_id = { - last_version_entity["productId"]: last_version_entity - for last_version_entity in last_version_entities - } for folder_id, asset_nodes in grouped.items(): product_entity = product_entities_by_folder_id.get(folder_id) From fe6990a647eb5a544c16799526d9c521dea091ba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 13:27:36 +0100 Subject: [PATCH 274/284] List looks based on product type instead of name --- client/ayon_core/hosts/maya/api/lib.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 28feff6a37..d5031c0426 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1876,18 +1876,9 @@ def list_looks(project_name, folder_id): list[dict[str, Any]]: List of look products. """ - # # get all products with look leading in - # the name associated with the asset - # TODO this should probably look for product type 'look' instead of - # checking product name that can not start with product type - product_entities = ayon_api.get_products( - project_name, folder_ids=[folder_id] - ) - return [ - product_entity - for product_entity in product_entities - if product_entity["name"].startswith("look") - ] + return list(ayon_api.get_products( + project_name, folder_ids=[folder_id], product_types={"look"} + )) def assign_look_by_version(nodes, version_id): From 7375587c87e811244933e780378e5b2e8d93a6fb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 13:32:18 +0100 Subject: [PATCH 275/284] Get both json and ma representation with one query --- client/ayon_core/hosts/maya/api/lib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index d5031c0426..a7fe9a04e4 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1897,12 +1897,15 @@ def assign_look_by_version(nodes, version_id): project_name = get_current_project_name() # Get representations of shader file and relationships - look_representation = ayon_api.get_representation_by_name( - project_name, "ma", version_id - ) - json_representation = ayon_api.get_representation_by_name( - project_name, "json", version_id + representations = ayon_api.get_representations( + project_name=project_name, + representation_names={"ma", "json"}, + version_ids=[version_id] ) + look_representation = next( + repre for repre in representations if repre["name"] == "ma") + json_representation = next( + repre for repre in representations if repre["name"] == "json") # See if representation is already loaded, if so reuse it. host = registered_host() From 602b9a8b2c260e341be1c6c22dd11f6300699b43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 13:38:33 +0100 Subject: [PATCH 276/284] Change default argument value `lookDefault` to `lookMain` For a long time OpenPype (and thus AYON) has been using `main` as default variant as opposed to `default` --- client/ayon_core/hosts/maya/api/lib.py | 2 +- .../ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index a7fe9a04e4..6fb36f10e4 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1942,7 +1942,7 @@ def assign_look_by_version(nodes, version_id): apply_shaders(relationships, shader_nodes, nodes) -def assign_look(nodes, product_name="lookDefault"): +def assign_look(nodes, product_name="lookMain"): """Assigns a look to a node. Optimizes the nodes by grouping by folder id and finding diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py index 74cdbeb7d4..88ef4b201a 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -51,7 +51,7 @@ def assign_vrayproxy_shaders(vrayproxy, assignments): index += 1 -def vrayproxy_assign_look(vrayproxy, product_name="lookDefault"): +def vrayproxy_assign_look(vrayproxy, product_name="lookMain"): # type: (str, str) -> None """Assign look to vray proxy. From eead61e6e92ffd6ff0a0f36ab8c8aea5ddcfbae9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 15:50:54 +0100 Subject: [PATCH 277/284] Parent look assigner UI to Maya window when opening via toolbox --- client/ayon_core/hosts/maya/api/customize.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/api/customize.py b/client/ayon_core/hosts/maya/api/customize.py index 4db8819ff5..16255f69ba 100644 --- a/client/ayon_core/hosts/maya/api/customize.py +++ b/client/ayon_core/hosts/maya/api/customize.py @@ -113,7 +113,9 @@ def override_toolbox_ui(): annotation="Look Manager", label="Look Manager", image=os.path.join(icons, "lookmanager.png"), - command=show_look_assigner, + command=lambda: show_look_assigner( + parent=parent_widget + ), width=icon_size, height=icon_size, parent=parent From 19c8a17bc1df40d63fbce65b29d19677a9dc7ee9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 20:45:24 +0100 Subject: [PATCH 278/284] Fix Collect Render - allow passing if no render cameras are set, so that validator can report it instead --- .../maya/plugins/publish/collect_render.py | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py index ff959afabc..21095935a2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py @@ -1,24 +1,19 @@ # -*- coding: utf-8 -*- """Collect render data. -This collector will go through render layers in maya and prepare all data -needed to create instances and their representations for submission and -publishing on farm. +This collector will go through renderlayer instances and prepare all data +needed to detect the expected rendered files for a layer, with resolution, +frame ranges and collects the data needed for publishing on the farm. Requires: instance -> families - instance -> setMembers - instance -> folderPath context -> currentFile - context -> workspaceDir context -> user -Optional: - Provides: instance -> label - instance -> productName + instance -> subset instance -> attachTo instance -> setMembers instance -> publish @@ -26,6 +21,8 @@ Provides: instance -> frameEnd instance -> byFrameStep instance -> renderer + instance -> family + instance -> asset instance -> time instance -> author instance -> source @@ -71,8 +68,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin): # TODO: Re-add force enable of workfile instance? # TODO: Re-add legacy layer support with LAYER_ prefix but in Creator - # TODO: Set and collect active state of RenderLayer in Creator using - # renderlayer.isRenderable() context = instance.context layer = instance.data["transientData"]["layer"] @@ -112,7 +107,13 @@ class CollectMayaRender(pyblish.api.InstancePlugin): except UnsupportedRendererException as exc: raise KnownPublishError(exc) render_products = layer_render_products.layer_data.products - assert render_products, "no render products generated" + if not render_products: + self.log.error( + "No render products generated for '%s'. You might not have " + "any render camera in the renderlayer or render end frame is " + "lower than start frame.", + instance.name + ) expected_files = [] multipart = False for product in render_products: @@ -130,16 +131,21 @@ class CollectMayaRender(pyblish.api.InstancePlugin): }) has_cameras = any(product.camera for product in render_products) - assert has_cameras, "No render cameras found." - - self.log.debug("multipart: {}".format( - multipart)) - assert expected_files, "no file names were generated, this is a bug" - self.log.debug( - "expected files: {}".format( - json.dumps(expected_files, indent=4, sort_keys=True) + if render_products and not has_cameras: + self.log.error( + "No render cameras found for: %s", + instance ) - ) + if not expected_files: + self.log.warning( + "No file names were generated, this is a bug.") + + for render_product in render_products: + self.log.debug(render_product) + self.log.debug("multipart: {}".format(multipart)) + self.log.debug("expected files: {}".format( + json.dumps(expected_files, indent=4, sort_keys=True) + )) # if we want to attach render to product, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV @@ -151,14 +157,14 @@ class CollectMayaRender(pyblish.api.InstancePlugin): ) # append full path - aov_dict = {} image_directory = os.path.join( cmds.workspace(query=True, rootDirectory=True), cmds.workspace(fileRuleEntry="images") ) # replace relative paths with absolute. Render products are # returned as list of dictionaries. - publish_meta_path = None + publish_meta_path = "NOT-SET" + aov_dict = {} for aov in expected_files: full_paths = [] aov_first_key = list(aov.keys())[0] @@ -169,14 +175,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin): publish_meta_path = os.path.dirname(full_path) aov_dict[aov_first_key] = full_paths full_exp_files = [aov_dict] - self.log.debug(full_exp_files) - - if publish_meta_path is None: - raise KnownPublishError("Unable to detect any expected output " - "images for: {}. Make sure you have a " - "renderable camera and a valid frame " - "range set for your renderlayer." - "".format(instance.name)) frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) @@ -222,7 +220,8 @@ class CollectMayaRender(pyblish.api.InstancePlugin): common_publish_meta_path = "/" + common_publish_meta_path self.log.debug( - "Publish meta path: {}".format(common_publish_meta_path)) + "Publish meta path: {}".format(common_publish_meta_path) + ) # Get layer specific settings, might be overrides colorspace_data = lib.get_color_management_preferences() From 25d07f4eac784232cfd3679c5df36dfbadad107c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 22:30:55 +0100 Subject: [PATCH 279/284] Improve error validation report --- .../publish/validate_render_single_camera.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py index 0171318813..e186d74b89 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -1,4 +1,5 @@ import re +import inspect import pyblish.api from maya import cmds @@ -36,7 +37,10 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, return invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Invalid cameras for render.") + raise PublishValidationError( + "Invalid render cameras.", + description=self.get_description() + ) @classmethod def get_invalid(cls, instance): @@ -51,17 +55,30 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, RenderSettings.get_image_prefix_attr(renderer) ) - + renderlayer = instance.data["renderlayer"] if len(cameras) > 1: if re.search(cls.R_CAMERA_TOKEN, file_prefix): # if there is token in prefix and we have more then # 1 camera, all is ok. return - cls.log.error("Multiple renderable cameras found for %s: %s " % - (instance.data["setMembers"], cameras)) - return [instance.data["setMembers"]] + cameras + cls.log.error( + "Multiple renderable cameras found for %s: %s ", + renderlayer, ", ".join(cameras)) + return [renderlayer] + cameras elif len(cameras) < 1: - cls.log.error("No renderable cameras found for %s " % - instance.data["setMembers"]) - return [instance.data["setMembers"]] + cls.log.error("No renderable cameras found for %s ", renderlayer) + return [renderlayer] + + def get_description(self): + return inspect.cleandoc( + """### Render Cameras Invalid + + Your render cameras are misconfigured. You may have no render + camera set or have multiple cameras with a render filename + prefix that does not include the `` token. + + See the logs for more details about the cameras. + + """ + ) From 413f30456f339193e73a18b6a3dad85d22022b48 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 10:12:42 +0200 Subject: [PATCH 280/284] Fix some more typos --- client/ayon_core/plugins/publish/extract_review.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index b50b415537..790f7a32ed 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -619,7 +619,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) - # Set output frames len to 1 when ouput is single image + # Set output frames len to 1 when output is single image if ( temp_data["output_ext_is_image"] and not temp_data["output_is_sequence"] @@ -955,7 +955,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("New representation ext: `{}`".format(output_ext)) - # Output is image file sequence witht frames + # Output is image file sequence with frames output_ext_is_image = bool(output_ext in self.image_exts) output_is_sequence = bool( output_ext_is_image @@ -967,7 +967,7 @@ class ExtractReview(pyblish.api.InstancePlugin): frame_end = temp_data["output_frame_end"] filename_base = "{}_{}".format(filename, filename_suffix) - # Temporary tempalte for frame filling. Example output: + # Temporary template for frame filling. Example output: # "basename.%04d.exr" when `frame_end` == 1001 repr_file = "{}.%{:0>2}d.{}".format( filename_base, len(str(frame_end)), output_ext @@ -997,7 +997,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Creating dir: {}".format(dst_staging_dir)) os.makedirs(dst_staging_dir) - # Store stagingDir to representaion + # Store stagingDir to representation new_repre["stagingDir"] = dst_staging_dir # Store paths to temp data @@ -1545,7 +1545,7 @@ class ExtractReview(pyblish.api.InstancePlugin): custom_tags (list): Custom Tags of processed representation. Returns: - list: Containg all output definitions matching entered tags. + list: Containing all output definitions matching entered tags. """ filtered_outputs = [] @@ -1859,7 +1859,7 @@ class OverscanCrop: # Replace "px" (and spaces before) with single space string_value = re.sub(r"([ ]+)?px", " ", string_value) string_value = re.sub(r"([ ]+)%", "%", string_value) - # Make sure +/- sign at the beggining of string is next to number + # Make sure +/- sign at the beginning of string is next to number string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value) # Make sure +/- sign in the middle has zero spaces before number under # which belongs From f7b62d133a9c716476fbb8b44c278a43867db802 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 10:34:00 +0200 Subject: [PATCH 281/284] Update client/ayon_core/hosts/maya/api/lib.py --- client/ayon_core/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 6fb36f10e4..7569e88e4c 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1897,11 +1897,11 @@ def assign_look_by_version(nodes, version_id): project_name = get_current_project_name() # Get representations of shader file and relationships - representations = ayon_api.get_representations( + representations = list(ayon_api.get_representations( project_name=project_name, representation_names={"ma", "json"}, version_ids=[version_id] - ) + )) look_representation = next( repre for repre in representations if repre["name"] == "ma") json_representation = next( From 57d225009293a3123f02fd2b48e4e38948e0f711 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 10:50:03 +0200 Subject: [PATCH 282/284] Convert `ABOUT_TO_SAVE` to `_about_to_save` Value is not a public constant, but a private global --- client/ayon_core/hosts/fusion/api/pipeline.py | 18 +++++++++--------- client/ayon_core/hosts/houdini/api/pipeline.py | 14 +++++++------- client/ayon_core/hosts/maya/api/pipeline.py | 14 +++++++------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 03773790e4..2d1073ec7d 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -43,7 +43,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") # Track whether the workfile tool is about to save -ABOUT_TO_SAVE = False +_about_to_save = False class FusionLogHandler(logging.Handler): @@ -176,15 +176,15 @@ def on_save(event): validate_comp_prefs(comp) # We are now starting the actual save directly - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = False + global _about_to_save + _about_to_save = False def on_task_changed(): - global ABOUT_TO_SAVE - print(f"Task changed: {ABOUT_TO_SAVE}") + global _about_to_save + print(f"Task changed: {_about_to_save}") # TODO: Only do this if not headless - if ABOUT_TO_SAVE: + if _about_to_save: # Let's prompt the user to update the context settings or not prompt_reset_context() @@ -228,7 +228,7 @@ def before_workfile_save(event): # have been shut down, and restarted - which will restart it to the # environment Fusion started with; not necessarily where the artist # is currently working. - # The `ABOUT_TO_SAVE` var is used to detect context changes when + # The `_about_to_save` var is used to detect context changes when # saving into another asset. If we keep it False it will be ignored # as context change. As such, before we change tasks we will only # consider it the current filepath is within the currently known @@ -239,8 +239,8 @@ def before_workfile_save(event): filepath = comp.GetAttrs()["COMPS_FileName"] workdir = os.environ.get("AYON_WORKDIR") if Path(workdir) in Path(filepath).parents: - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = True + global _about_to_save + _about_to_save = True def ls(): diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index 787d0a01a1..4797cf36a0 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -39,7 +39,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") # Track whether the workfile tool is about to save -ABOUT_TO_SAVE = False +_about_to_save = False class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -292,8 +292,8 @@ def ls(): def before_workfile_save(event): - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = True + global _about_to_save + _about_to_save = True def before_save(): @@ -308,13 +308,13 @@ def on_save(): lib.update_houdini_vars_context_dialog() # We are now starting the actual save directly - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = False + global _about_to_save + _about_to_save = False def on_task_changed(): - global ABOUT_TO_SAVE - if not IS_HEADLESS and ABOUT_TO_SAVE: + global _about_to_save + if not IS_HEADLESS and _about_to_save: # Let's prompt the user to update the context settings or not lib.prompt_reset_context() diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 2be452a22a..8e6e2ccd8a 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -68,7 +68,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" # Track whether the workfile tool is about to save -ABOUT_TO_SAVE = False +_about_to_save = False class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -585,8 +585,8 @@ def on_save(): lib.set_id(node, new_id, overwrite=False) # We are now starting the actual save directly - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = False + global _about_to_save + _about_to_save = False def on_open(): @@ -657,8 +657,8 @@ def on_task_changed(): lib.set_context_settings() lib.update_content_on_context_change() - global ABOUT_TO_SAVE - if not lib.IS_HEADLESS and ABOUT_TO_SAVE: + global _about_to_save + if not lib.IS_HEADLESS and _about_to_save: # Let's prompt the user to update the context settings or not lib.prompt_reset_context() @@ -676,8 +676,8 @@ def before_workfile_save(event): if workdir_path: create_workspace_mel(workdir_path, project_name) - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = True + global _about_to_save + _about_to_save = True def workfile_save_before_xgen(event): From 73c4553133112708c15fee52c5dc72077961c6b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Apr 2024 12:16:16 +0200 Subject: [PATCH 283/284] add bundle name to render jobs --- .../deadline/plugins/publish/submit_aftereffects_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_blender_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_fusion_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_harmony_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_max_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 2 +- 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index a284464009..1993444041 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -80,6 +80,7 @@ class AfterEffectsSubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py index ae19e63a37..d28ed9cdce 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -102,6 +102,7 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index cf124c0bcc..80f32d4db0 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -225,6 +225,7 @@ class FusionSubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py index beb8afc3a3..15326550b3 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -273,6 +273,7 @@ class HarmonySubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py index 1abefa515a..b75c19ddc8 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py @@ -106,6 +106,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index 5602b02707..d0fb923eb6 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -207,6 +207,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index ac01af901c..9101ddf303 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -376,6 +376,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, keys = [ "PYTHONPATH", "PATH", + "AYON_BUNDLE_NAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", @@ -388,7 +389,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "TOOL_ENV", "FOUNDRY_LICENSE", "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", ] # add allowed keys from preset if any From a3da47fe815ccbdef51b3c7b0c2ec5ac406620b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Apr 2024 12:16:29 +0200 Subject: [PATCH 284/284] pass settings variant to jobs --- .../deadline/plugins/publish/submit_aftereffects_deadline.py | 1 + .../deadline/plugins/publish/submit_blender_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_fusion_deadline.py | 1 + .../deadline/plugins/publish/submit_harmony_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_max_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 1 + .../deadline/plugins/publish/submit_publish_cache_job.py | 3 +++ .../modules/deadline/plugins/publish/submit_publish_job.py | 3 +++ 9 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index 1993444041..675346105c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -81,6 +81,7 @@ class AfterEffectsSubmitDeadline( "FTRACK_API_USER", "FTRACK_SERVER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py index d28ed9cdce..ab342c1a9d 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -103,6 +103,7 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_SERVER", "OPENPYPE_SG_USER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index 80f32d4db0..bfb65708e6 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -226,6 +226,7 @@ class FusionSubmitDeadline( "FTRACK_API_USER", "FTRACK_SERVER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py index 15326550b3..d52b16b27d 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -274,6 +274,7 @@ class HarmonySubmitDeadline( "FTRACK_API_USER", "FTRACK_SERVER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py index b75c19ddc8..cba05f6948 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py @@ -107,12 +107,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_SERVER", "OPENPYPE_SG_USER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", "AYON_WORKDIR", "AYON_APP_NAME", - "IS_TEST" + "IS_TEST", ] environment = { diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index d0fb923eb6..0300b12104 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -208,6 +208,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_SERVER", "OPENPYPE_SG_USER", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index 9101ddf303..d70cb75bf3 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -377,6 +377,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "PYTHONPATH", "PATH", "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 50bd414587..910b2e46db 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -133,6 +133,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "AYON_RENDER_JOB": "0", "AYON_REMOTE_PUBLISH": "0", "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], + "AYON_DEFAULT_SETTINGS_VARIANT": ( + os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] + ), } # add environments from self.environ_keys diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 84bac6d017..af5839d0cf 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -210,6 +210,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "AYON_RENDER_JOB": "0", "AYON_REMOTE_PUBLISH": "0", "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], + "AYON_DEFAULT_SETTINGS_VARIANT": ( + os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] + ), } # add environments from self.environ_keys