From f00e6c1195c9d5a7c5c75e9ae2c5df737ae29d13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 15:43:21 +0200 Subject: [PATCH 01/43] set stdout and stderr of application process to devnull if sys.stdout is not set --- openpype/lib/applications.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index d82b7cd847..ff5ef92d82 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1,4 +1,5 @@ import os +import sys import re import copy import json @@ -675,6 +676,10 @@ class ApplicationLaunchContext: ) self.kwargs["creationflags"] = flags + if not sys.stdout: + self.kwargs["stdout"] = subprocess.DEVNULL + self.kwargs["stderr"] = subprocess.DEVNULL + self.prelaunch_hooks = None self.postlaunch_hooks = None From 5fa4e091e3a317e313025b922ce1ab720b437f42 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 15:43:44 +0200 Subject: [PATCH 02/43] modify stderr in non python prelaunch hook to use DEVNULL instead of STDOUT --- openpype/hooks/pre_non_python_host_launch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index 393a878f76..b91be137ab 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -49,5 +49,7 @@ class NonPythonHostHook(PreLaunchHook): if remainders: self.launch_context.launch_args.extend(remainders) + # This must be set otherwise it wouldn't be possible to catch output + # when build PpenPype is used. self.launch_context.kwargs["stdout"] = subprocess.DEVNULL - self.launch_context.kwargs["stderr"] = subprocess.STDOUT + self.launch_context.kwargs["stderr"] = subprocess.DEVNULL From 05d29dc844cc8940145b889842bb0e120c014766 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 16:06:51 +0200 Subject: [PATCH 03/43] fix typo --- openpype/hooks/pre_non_python_host_launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index b91be137ab..0447f4a06f 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -50,6 +50,6 @@ class NonPythonHostHook(PreLaunchHook): self.launch_context.launch_args.extend(remainders) # This must be set otherwise it wouldn't be possible to catch output - # when build PpenPype is used. + # when build OpenPype is used. self.launch_context.kwargs["stdout"] = subprocess.DEVNULL self.launch_context.kwargs["stderr"] = subprocess.DEVNULL From 9aa83282821209248de79ad8a64ede9959c9ca7a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 10:19:32 +0200 Subject: [PATCH 04/43] with windows shell prelaunch hook to found app hook and simplyfied it --- openpype/hooks/pre_foundry_apps.py | 28 +++++++++++++++ openpype/hooks/pre_with_windows_shell.py | 44 ------------------------ 2 files changed, 28 insertions(+), 44 deletions(-) create mode 100644 openpype/hooks/pre_foundry_apps.py delete mode 100644 openpype/hooks/pre_with_windows_shell.py diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py new file mode 100644 index 0000000000..85f68c6b60 --- /dev/null +++ b/openpype/hooks/pre_foundry_apps.py @@ -0,0 +1,28 @@ +import subprocess +from openpype.lib import PreLaunchHook + + +class LaunchFoundryAppsWindows(PreLaunchHook): + """Foundry applications have specific way how to launch them. + + Nuke is executed "like" python process so it is required to pass + `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console. + At the same time the newly created console won't create it's own stdout + and stderr handlers so they should not be redirected to DEVNULL. + """ + + # Should be as last hook because must change launch arguments to string + order = 1000 + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + platforms = ["windows"] + + def execute(self): + # Change `creationflags` to CREATE_NEW_CONSOLE + # - on Windows will nuke create new window using it's console + # Set `stdout` and `stderr` to None so new created console does not + # have redirected output to DEVNULL in build + self.launch_context.kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE, + "stdout": None, + "stderr": None + }) diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py deleted file mode 100644 index 441ab1a675..0000000000 --- a/openpype/hooks/pre_with_windows_shell.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import subprocess -from openpype.lib import PreLaunchHook - - -class LaunchWithWindowsShell(PreLaunchHook): - """Add shell command before executable. - - Some hosts have issues when are launched directly from python in that case - it is possible to prepend shell executable which will trigger process - instead. - """ - - # Should be as last hook because must change launch arguments to string - order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] - platforms = ["windows"] - - def execute(self): - launch_args = self.launch_context.clear_launch_args( - self.launch_context.launch_args) - new_args = [ - # Get comspec which is cmd.exe in most cases. - os.environ.get("COMSPEC", "cmd.exe"), - # NOTE change to "/k" if want to keep console opened - "/c", - # Convert arguments to command line arguments (as string) - "\"{}\"".format( - subprocess.list2cmdline(launch_args) - ) - ] - # Convert list to string - # WARNING this only works if is used as string - args_string = " ".join(new_args) - self.log.info(( - "Modified launch arguments to be launched with shell \"{}\"." - ).format(args_string)) - - # Replace launch args with new one - self.launch_context.launch_args = args_string - # Change `creationflags` to CREATE_NEW_CONSOLE - self.launch_context.kwargs["creationflags"] = ( - subprocess.CREATE_NEW_CONSOLE - ) From 313e433d726344fc2eabe6debb78ba8464f72dd2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 09:34:57 +0200 Subject: [PATCH 05/43] client#115 - added Texture batch for Standalone Publisher Added collector Added validator Added family --- .../plugins/publish/collect_texture.py | 220 ++++++++++++++++++ .../plugins/publish/validate_texture_batch.py | 47 ++++ .../project_settings/standalonepublisher.json | 34 ++- 3 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py new file mode 100644 index 0000000000..7b79fd1061 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -0,0 +1,220 @@ +import os +import copy +import re +import opentimelineio as otio +import pyblish.api +from openpype import lib as plib +import json + +class CollectTextures(pyblish.api.ContextPlugin): + """Collect workfile (and its resource_files) and textures.""" + + order = pyblish.api.CollectorOrder + label = "Collect Textures" + hosts = ["standalonepublisher"] + families = ["texture_batch"] + actions = [] + + main_workfile_extensions = ['mra'] + other_workfile_extensions = ['spp', 'psd'] + texture_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", + "gif", "svg"] + + color_space = ["lin_srgb", "raw", "acesg"] + + version_regex = re.compile(r"^(.+)_v([0-9]+)") + udim_regex = re.compile(r"_1[0-9]{3}\.") + + def process(self, context): + self.context = context + import json + def convertor(value): + return str(value) + + workfile_subset = "texturesMainWorkfile" + resource_files = {} + workfile_files = {} + representations = {} + version_data = {} + asset_builds = set() + asset = None + for instance in context: + if not asset: + asset = instance.data["asset"] # selected from SP + + self.log.info("instance.data:: {}".format( + json.dumps(instance.data, indent=4, default=convertor))) + processed_instance = False + for repre in instance.data["representations"]: + ext = repre["ext"].replace('.', '') + asset_build = version = None + if ext in self.main_workfile_extensions or \ + ext in self.other_workfile_extensions: + self.log.info('workfile') + asset_build, version = \ + self._parse_asset_build(repre["files"], + self.version_regex) + asset_builds.add((asset_build, version, + workfile_subset, 'workfile')) + processed_instance = True + + if not representations.get(workfile_subset): + representations[workfile_subset] = [] + + # asset_build must be here to tie workfile and texture + if not workfile_files.get(asset_build): + workfile_files[asset_build] = [] + + if ext in self.main_workfile_extensions: + representations[workfile_subset].append(repre) + workfile_files[asset_build].append(repre["files"]) + + if ext in self.other_workfile_extensions: + self.log.info("other") + # add only if not added already from main + if not representations.get(workfile_subset): + representations[workfile_subset].append(repre) + + if not workfile_files.get(asset_build): + workfile_files[asset_build].append(repre["files"]) + + if not resource_files.get(workfile_subset): + resource_files[workfile_subset] = [] + item = { + "files": [os.path.join(repre["stagingDir"], + repre["files"])], + "source": "standalone publisher" + } + resource_files[workfile_subset].append(item) + + if ext in self.texture_extensions: + c_space = self._get_color_space(repre["files"][0], + self.color_space) + subset = "texturesMain_{}".format(c_space) + + asset_build, version = \ + self._parse_asset_build(repre["files"][0], + self.version_regex) + + if not representations.get(subset): + representations[subset] = [] + representations[subset].append(repre) + + udim = self._parse_udim(repre["files"][0], self.udim_regex) + + if not version_data.get(subset): + version_data[subset] = [] + ver_data = { + "color_space": c_space, + "UDIM": udim, + } + version_data[subset].append(ver_data) + + asset_builds.add( + (asset_build, version, subset, "textures")) + processed_instance = True + + if processed_instance: + self.context.remove(instance) + + self.log.info("asset_builds:: {}".format(asset_builds)) + self._create_new_instances(context, + asset, + asset_builds, + resource_files, + representations, + version_data, + workfile_files) + + def _create_new_instances(self, context, asset, asset_builds, + resource_files, representations, + version_data, workfile_files): + """Prepare new instances from collected data. + + Args: + context (ContextPlugin) + asset (string): selected asset from SP + asset_builds (set) of tuples + (asset_build, version, subset, family) + resource_files (list) of resource dicts + representations (dict) of representation files, key is + asset_build + """ + for asset_build, version, subset, family in asset_builds: + + self.log.info("resources:: {}".format(resource_files)) + self.log.info("-"*25) + self.log.info("representations:: {}".format(representations)) + self.log.info("-"*25) + self.log.info("workfile_files:: {}".format(workfile_files)) + + new_instance = context.create_instance(subset) + new_instance.data.update( + { + "subset": subset, + "asset": asset, + "label": subset, + "name": subset, + "family": family, + "version": int(version), + "representations": representations.get(subset), + "families": [family] + } + ) + if resource_files.get(subset): + new_instance.data.update({ + "resources": resource_files.get(subset) + }) + + repre = representations.get(subset)[0] + new_instance.context.data["currentFile"] = os.path.join( + repre["stagingDir"], repre["files"][0]) + + ver_data = version_data.get(subset) + if ver_data: + ver_data = ver_data[0] + if workfile_files.get(asset_build): + ver_data['workfile'] = workfile_files.get(asset_build)[0] + + new_instance.data.update( + {"versionData": ver_data} + ) + + self.log.info("new instance:: {}".format(json.dumps(new_instance.data, indent=4))) + + def _parse_asset_build(self, name, version_regex): + regex_result = version_regex.findall(name) + asset_name = None # ?? + version_number = 1 + if regex_result: + asset_name, version_number = regex_result[0] + + return asset_name, version_number + + def _parse_udim(self, name, udim_regex): + regex_result = udim_regex.findall(name) + udim = None + if not regex_result: + self.log.warning("Didn't find UDIM in {}".format(name)) + else: + udim = re.sub("[^0-9]", '', regex_result[0]) + + return udim + + def _get_color_space(self, name, color_spaces): + """Looks for color_space from a list in a file name.""" + color_space = None + found = [cs for cs in color_spaces if + re.search("_{}_".format(cs), name)] + + if not found: + self.log.warning("No color space found in {}".format(name)) + else: + if len(found) > 1: + msg = "Multiple color spaces found in {}->{}".format(name, + found) + self.log.warning(msg) + + color_space = found[0] + + return color_space diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py new file mode 100644 index 0000000000..e222004456 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -0,0 +1,47 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatch(pyblish.api.ContextPlugin): + """Validates that collected instnaces for Texture batch are OK. + + Validates: + some textures are present + workfile has resource files (optional) + texture version matches to workfile version + """ + + label = "Validate Texture Batch" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile", "textures"] + + def process(self, context): + + workfiles = [] + workfiles_in_textures = [] + for instance in context: + if instance.data["family"] == "workfile": + workfiles.append(instance.data["representations"][0]["files"]) + + if not instance.data.get("resources"): + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + self.log.warning(msg) + + if instance.data["family"] == "textures": + wfile = instance.data["versionData"]["workfile"] + workfiles_in_textures.append(wfile) + + version_str = "v{:03d}".format(instance.data["version"]) + assert version_str in wfile, \ + "Not matching version, texture {} - workfile {}".format( + instance.data["version"], wfile + ) + + msg = "Not matching set of workfiles and textures." + \ + "{} not equal to {}".format(set(workfiles), + set(workfiles_in_textures)) +\ + "\nCheck that both workfile and textures are present" + keys = set(workfiles) == set(workfiles_in_textures) + assert keys, msg diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 7172612a74..5590fa6349 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -105,16 +105,33 @@ "label": "Render", "family": "render", "icon": "image", - "defaults": ["Animation", "Lighting", "Lookdev", "Compositing"], + "defaults": [ + "Animation", + "Lighting", + "Lookdev", + "Compositing" + ], "help": "Rendered images or video files" }, "create_mov_batch": { - "name": "mov_batch", - "label": "Batch Mov", - "family": "render_mov_batch", - "icon": "image", - "defaults": ["Main"], - "help": "Process multiple Mov files and publish them for layout and comp." + "name": "mov_batch", + "label": "Batch Mov", + "family": "render_mov_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Process multiple Mov files and publish them for layout and comp." + }, + "create_texture_batch": { + "name": "texture_batch", + "label": "Texture Batch", + "family": "texture_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Texture files with UDIM together with worfile" }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", @@ -127,7 +144,8 @@ "create_image": "Image", "create_matchmove": "Matchmove", "create_render": "Render", - "create_mov_batch": "Batch Mov" + "create_mov_batch": "Batch Mov", + "create_texture_batch": "Batch Texture" } }, "publish": { From ab78b19b5f9a450a2af72a66e883401b0eb6dfd9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 13:31:06 +0200 Subject: [PATCH 06/43] client#115 - fixes --- .../plugins/publish/collect_texture.py | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 7b79fd1061..12858595dd 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -22,6 +22,9 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["lin_srgb", "raw", "acesg"] + workfile_subset_template = "texturesMainWorkfile" + texture_subset_template = "texturesMain_{color_space}" + version_regex = re.compile(r"^(.+)_v([0-9]+)") udim_regex = re.compile(r"_1[0-9]{3}\.") @@ -31,7 +34,6 @@ class CollectTextures(pyblish.api.ContextPlugin): def convertor(value): return str(value) - workfile_subset = "texturesMainWorkfile" resource_files = {} workfile_files = {} representations = {} @@ -48,11 +50,19 @@ class CollectTextures(pyblish.api.ContextPlugin): for repre in instance.data["representations"]: ext = repre["ext"].replace('.', '') asset_build = version = None + + workfile_subset = self.workfile_subset_template + + if isinstance(repre["files"], list): + repre_file = repre["files"][0] + else: + repre_file = repre["files"] + if ext in self.main_workfile_extensions or \ ext in self.other_workfile_extensions: self.log.info('workfile') asset_build, version = \ - self._parse_asset_build(repre["files"], + self._parse_asset_build(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) @@ -61,13 +71,9 @@ class CollectTextures(pyblish.api.ContextPlugin): if not representations.get(workfile_subset): representations[workfile_subset] = [] - # asset_build must be here to tie workfile and texture - if not workfile_files.get(asset_build): - workfile_files[asset_build] = [] - if ext in self.main_workfile_extensions: representations[workfile_subset].append(repre) - workfile_files[asset_build].append(repre["files"]) + workfile_files[asset_build] = repre_file if ext in self.other_workfile_extensions: self.log.info("other") @@ -75,8 +81,9 @@ class CollectTextures(pyblish.api.ContextPlugin): if not representations.get(workfile_subset): representations[workfile_subset].append(repre) + # only overwrite if not present if not workfile_files.get(asset_build): - workfile_files[asset_build].append(repre["files"]) + workfile_files[asset_build] = repre_file if not resource_files.get(workfile_subset): resource_files[workfile_subset] = [] @@ -88,19 +95,21 @@ class CollectTextures(pyblish.api.ContextPlugin): resource_files[workfile_subset].append(item) if ext in self.texture_extensions: - c_space = self._get_color_space(repre["files"][0], + c_space = self._get_color_space(repre_file, self.color_space) - subset = "texturesMain_{}".format(c_space) + subset_formatting_data = {"color_space": c_space} + subset = self.texture_subset_template.format( + **subset_formatting_data) asset_build, version = \ - self._parse_asset_build(repre["files"][0], + self._parse_asset_build(repre_file, self.version_regex) if not representations.get(subset): representations[subset] = [] representations[subset].append(repre) - udim = self._parse_udim(repre["files"][0], self.udim_regex) + udim = self._parse_udim(repre_file, self.udim_regex) if not version_data.get(subset): version_data[subset] = [] @@ -148,6 +157,13 @@ class CollectTextures(pyblish.api.ContextPlugin): self.log.info("-"*25) self.log.info("workfile_files:: {}".format(workfile_files)) + upd_representations = representations.get(subset) + if upd_representations and family != 'workfile': + for repre in upd_representations: + repre.pop("frameStart", None) + repre.pop("frameEnd", None) + repre.pop("fps", None) + new_instance = context.create_instance(subset) new_instance.data.update( { @@ -157,8 +173,8 @@ class CollectTextures(pyblish.api.ContextPlugin): "name": subset, "family": family, "version": int(version), - "representations": representations.get(subset), - "families": [family] + "representations": upd_representations, + "families": [] } ) if resource_files.get(subset): @@ -166,15 +182,22 @@ class CollectTextures(pyblish.api.ContextPlugin): "resources": resource_files.get(subset) }) - repre = representations.get(subset)[0] - new_instance.context.data["currentFile"] = os.path.join( - repre["stagingDir"], repre["files"][0]) + workfile = workfile_files.get(asset_build) + # store origin + if family == 'workfile': + new_instance.data["source"] = "standalone publisher" + else: + repre = representations.get(subset)[0] + new_instance.context.data["currentFile"] = os.path.join( + repre["stagingDir"], workfile) + + # add data for version document ver_data = version_data.get(subset) if ver_data: ver_data = ver_data[0] - if workfile_files.get(asset_build): - ver_data['workfile'] = workfile_files.get(asset_build)[0] + if workfile: + ver_data['workfile'] = workfile new_instance.data.update( {"versionData": ver_data} From d889f6f24571974ac64bb4d0aa3d3aba5427ad6f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 19:41:39 +0200 Subject: [PATCH 07/43] client#115 - added extractor to fill transfers --- .../plugins/publish/extract_resources.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py new file mode 100644 index 0000000000..1183180833 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py @@ -0,0 +1,42 @@ +import os +import pyblish.api + + +class ExtractResources(pyblish.api.InstancePlugin): + """ + Extracts files from instance.data["resources"]. + + These files are additional (textures etc.), currently not stored in + representations! + + Expects collected 'resourcesDir'. (list of dicts with 'files' key and + list of source urls) + + Provides filled 'transfers' (list of tuples (source_url, target_url)) + """ + + label = "Extract Resources SP" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + families = ["workfile"] + + def process(self, instance): + if not instance.data.get("resources"): + self.log.info("No resources") + return + + if not instance.data.get("transfers"): + instance.data["transfers"] = [] + + publish_dir = instance.data["resourcesDir"] + + transfers = [] + for resource in instance.data["resources"]: + for file_url in resource.get("files", []): + file_name = os.path.basename(file_url) + dest_url = os.path.join(publish_dir, file_name) + transfers.append((file_url, dest_url)) + + self.log.info("transfers:: {}".format(transfers)) + instance.data["transfers"].extend(transfers) From 1c54a4c23e539b417015089f02f9a60af08e07cc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jun 2021 09:20:44 +0200 Subject: [PATCH 08/43] client#115 - added udim support to integrate_new Fixes --- .../plugins/publish/collect_texture.py | 200 ++++++++++++++---- .../publish/extract_workfile_location.py | 41 ++++ openpype/plugins/publish/integrate_new.py | 24 ++- .../defaults/project_anatomy/templates.json | 2 +- 4 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 12858595dd..0e2b21927f 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -1,13 +1,20 @@ import os -import copy import re -import opentimelineio as otio import pyblish.api -from openpype import lib as plib import json +from avalon.api import format_template_with_optional_keys + + class CollectTextures(pyblish.api.ContextPlugin): - """Collect workfile (and its resource_files) and textures.""" + """Collect workfile (and its resource_files) and textures. + + Provides: + 1 instance per workfile (with 'resources' filled if needed) + (workfile family) + 1 instance per group of textures + (textures family) + """ order = pyblish.api.CollectorOrder label = "Collect Textures" @@ -22,14 +29,29 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["lin_srgb", "raw", "acesg"] - workfile_subset_template = "texturesMainWorkfile" - texture_subset_template = "texturesMain_{color_space}" + version_regex = re.compile(r"v([0-9]+)") + udim_regex = re.compile(r"_1[0-9]{3}\.") + + #currently implemented placeholders ["color_space"] + input_naming_patterns = { + # workfile: ctr_envCorridorMain_texturing_v005.mra > + # expected groups: [(asset),(filler),(version)] + # texture: T_corridorMain_aluminium1_BaseColor_lin_srgb_1029.exr + # expected groups: [(asset), (filler),(color_space),(udim)] + r'^ctr_env([^.]+)_(.+)_v([0-9]{3,}).+': + r'^T_([^_.]+)_(.*)_({color_space})_(1[0-9]{3}).+' + } + + workfile_subset_template = "textures{}Workfile" + # implemented keys: ["color_space", "channel", "subset"] + texture_subset_template = "textures{subset}_{channel}" version_regex = re.compile(r"^(.+)_v([0-9]+)") udim_regex = re.compile(r"_1[0-9]{3}\.") def process(self, context): self.context = context + import json def convertor(value): return str(value) @@ -41,9 +63,19 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_builds = set() asset = None for instance in context: + if not self.input_naming_patterns: + raise ValueError("Naming patterns are not configured. \n" + "Ask admin to provide naming conventions " + "for workfiles and textures.") + if not asset: asset = instance.data["asset"] # selected from SP + parsed_subset = instance.data["subset"].replace( + instance.data["family"], '') + workfile_subset = self.workfile_subset_template.format( + parsed_subset) + self.log.info("instance.data:: {}".format( json.dumps(instance.data, indent=4, default=convertor))) processed_instance = False @@ -51,19 +83,20 @@ class CollectTextures(pyblish.api.ContextPlugin): ext = repre["ext"].replace('.', '') asset_build = version = None - workfile_subset = self.workfile_subset_template - if isinstance(repre["files"], list): repre_file = repre["files"][0] else: repre_file = repre["files"] if ext in self.main_workfile_extensions or \ - ext in self.other_workfile_extensions: - self.log.info('workfile') - asset_build, version = \ - self._parse_asset_build(repre_file, - self.version_regex) + ext in self.other_workfile_extensions: + + asset_build = self._get_asset_build( + repre_file, + self.input_naming_patterns.keys(), + self.color_space + ) + version = self._get_version(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) processed_instance = True @@ -95,15 +128,32 @@ class CollectTextures(pyblish.api.ContextPlugin): resource_files[workfile_subset].append(item) if ext in self.texture_extensions: - c_space = self._get_color_space(repre_file, - self.color_space) - subset_formatting_data = {"color_space": c_space} - subset = self.texture_subset_template.format( - **subset_formatting_data) + c_space = self._get_color_space( + repre_file, + self.color_space + ) - asset_build, version = \ - self._parse_asset_build(repre_file, - self.version_regex) + channel = self._get_channel_name( + repre_file, + list(self.input_naming_patterns.values()), + self.color_space + ) + + formatting_data = { + "color_space": c_space, + "channel": channel, + "subset": parsed_subset + } + self.log.debug("data::{}".format(formatting_data)) + subset = format_template_with_optional_keys( + formatting_data, self.texture_subset_template) + + asset_build = self._get_asset_build( + repre_file, + self.input_naming_patterns.values(), + self.color_space + ) + version = self._get_version(repre_file, self.version_regex) if not representations.get(subset): representations[subset] = [] @@ -149,21 +199,15 @@ class CollectTextures(pyblish.api.ContextPlugin): representations (dict) of representation files, key is asset_build """ + # sort workfile first + asset_builds = sorted(asset_builds, + key=lambda tup: tup[3], reverse=True) + + # workfile must have version, textures might + main_version = None for asset_build, version, subset, family in asset_builds: - - self.log.info("resources:: {}".format(resource_files)) - self.log.info("-"*25) - self.log.info("representations:: {}".format(representations)) - self.log.info("-"*25) - self.log.info("workfile_files:: {}".format(workfile_files)) - - upd_representations = representations.get(subset) - if upd_representations and family != 'workfile': - for repre in upd_representations: - repre.pop("frameStart", None) - repre.pop("frameEnd", None) - repre.pop("fps", None) - + if not main_version: + main_version = version new_instance = context.create_instance(subset) new_instance.data.update( { @@ -172,8 +216,7 @@ class CollectTextures(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, - "version": int(version), - "representations": upd_representations, + "version": int(version or main_version), "families": [] } ) @@ -203,18 +246,43 @@ class CollectTextures(pyblish.api.ContextPlugin): {"versionData": ver_data} ) - self.log.info("new instance:: {}".format(json.dumps(new_instance.data, indent=4))) + upd_representations = representations.get(subset) + if upd_representations and family != 'workfile': + upd_representations = self._update_representations( + upd_representations) - def _parse_asset_build(self, name, version_regex): - regex_result = version_regex.findall(name) - asset_name = None # ?? - version_number = 1 - if regex_result: - asset_name, version_number = regex_result[0] + new_instance.data["representations"] = upd_representations - return asset_name, version_number + def _get_asset_build(self, name, input_naming_patterns, color_spaces): + """Loops through configured workfile patterns to find asset name. - def _parse_udim(self, name, udim_regex): + Asset name used to bind workfile and its textures. + + Args: + name (str): workfile name + input_naming_patterns (list): + [workfile_pattern] or [texture_pattern] + """ + for input_pattern in input_naming_patterns: + for cs in color_spaces: + pattern = input_pattern.replace('{color_space}', cs) + regex_result = re.findall(pattern, name) + + if regex_result: + asset_name = regex_result[0][0].lower() + return asset_name + + raise ValueError("Couldnt find asset name in {}".format(name)) + + def _get_version(self, name, version_regex): + found = re.search(version_regex, name) + if found: + return found.group().replace("v", "") + + self.log.info("No version found in the name {}".format(name)) + + def _get_udim(self, name, udim_regex): + """Parses from 'name' udim value with 'udim_regex'.""" regex_result = udim_regex.findall(name) udim = None if not regex_result: @@ -225,7 +293,11 @@ class CollectTextures(pyblish.api.ContextPlugin): return udim def _get_color_space(self, name, color_spaces): - """Looks for color_space from a list in a file name.""" + """Looks for color_space from a list in a file name. + + Color space seems not to be recognizable by regex pattern, set of + known space spaces must be provided. + """ color_space = None found = [cs for cs in color_spaces if re.search("_{}_".format(cs), name)] @@ -241,3 +313,37 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = found[0] return color_space + + def _get_channel_name(self, name, input_naming_patterns, color_spaces): + """Return parsed channel name. + + Unknown format of channel name and color spaces >> cs are known + list - 'color_space' used as a placeholder + """ + for texture_pattern in input_naming_patterns: + for cs in color_spaces: + pattern = texture_pattern.replace('{color_space}', cs) + ret = re.findall(pattern, name) + if ret: + return ret.pop()[1] + + def _update_representations(self, upd_representations): + """Frames dont have sense for textures, add collected udims instead.""" + udims = [] + for repre in upd_representations: + repre.pop("frameStart", None) + repre.pop("frameEnd", None) + repre.pop("fps", None) + + files = repre.get("files", []) + if not isinstance(files, list): + files = [files] + + for file_name in files: + udim = self._get_udim(file_name, self.udim_regex) + udims.append(udim) + + repre["udim"] = udims # must be this way, used for filling path + + return upd_representations + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py new file mode 100644 index 0000000000..4345cef6dc --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -0,0 +1,41 @@ +import os +import pyblish.api + + +class ExtractWorkfileUrl(pyblish.api.ContextPlugin): + """ + Modifies 'workfile' field to contain link to published workfile. + + Expects that batch contains only single workfile and matching + (multiple) textures. + """ + + label = "Extract Workfile Url SP" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + families = ["textures"] + + def process(self, context): + filepath = None + + # first loop for workfile + for instance in context: + if instance.data["family"] == 'workfile': + anatomy = context.data['anatomy'] + template_data = instance.data.get("anatomyData") + rep_name = instance.data.get("representations")[0].get("name") + template_data["representation"] = rep_name + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + filepath = os.path.normpath(template_filled) + self.log.info("Using published scene for render {}".format( + filepath)) + + if not filepath: + raise ValueError("Texture batch doesn't contain workfile.") + + # then apply to all textures + for instance in context: + if instance.data["family"] == 'textures': + instance.data["versionData"]["workfile"] = filepath diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c5ce6d23aa..6d2a95f232 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -380,7 +380,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files = list() for i in [1, 2]: - template_data["frame"] = src_padding_exp % i + template_data["representation"] = repre['ext'] + if not repre.get("udim"): + template_data["frame"] = src_padding_exp % i + else: + template_data["udim"] = src_padding_exp % i + anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] if repre_context is None: @@ -388,7 +393,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files.append( os.path.normpath(template_filled) ) - template_data["frame"] = repre_context["frame"] + if not repre.get("udim"): + template_data["frame"] = repre_context["frame"] + else: + template_data["udim"] = repre_context["udim"] self.log.debug( "test_dest_files: {}".format(str(test_dest_files))) @@ -453,7 +461,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_start_frame = dst_padding # Store used frame value to template data - template_data["frame"] = dst_start_frame + if repre.get("frame"): + template_data["frame"] = dst_start_frame + dst = "{0}{1}{2}".format( dst_head, dst_start_frame, @@ -476,6 +486,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "Given file name is a full path" ) + template_data["representation"] = repre['ext'] + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] @@ -488,6 +502,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre['published_path'] = dst self.log.debug("__ dst: {}".format(dst)) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + repre["publishedFiles"] = published_files for key in self.db_representation_context_keys: @@ -1045,6 +1062,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) shutil.copy(file_url, new_name) + os.remove(file_url) else: self.log.debug( "Renaming file {} to {}".format( diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 63477b9d82..53abd35ed5 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -17,7 +17,7 @@ }, "publish": { "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", - "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}><_{udim}>.{ext}", "path": "{@folder}/{@file}", "thumbnail": "{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}.{ext}" }, From 1742904d920d420d56ebc47e6cd2ccd35c04805e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 12 Jul 2021 12:46:39 +0200 Subject: [PATCH 09/43] Textures publishing - copy from 2.x --- .../plugins/publish/collect_texture.py | 240 ++++++++++++------ .../plugins/publish/validate_texture_batch.py | 47 +--- .../plugins/publish/validate_texture_name.py | 50 ++++ .../publish/validate_texture_versions.py | 24 ++ .../publish/validate_texture_workfiles.py | 22 ++ 5 files changed, 273 insertions(+), 110 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 0e2b21927f..b8f8f05dc9 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -9,6 +9,11 @@ from avalon.api import format_template_with_optional_keys class CollectTextures(pyblish.api.ContextPlugin): """Collect workfile (and its resource_files) and textures. + Currently implements use case with Mari and Substance Painter, where + one workfile is main (.mra - Mari) with possible additional workfiles + (.spp - Substance) + + Provides: 1 instance per workfile (with 'resources' filled if needed) (workfile family) @@ -22,40 +27,39 @@ class CollectTextures(pyblish.api.ContextPlugin): families = ["texture_batch"] actions = [] + # from presets main_workfile_extensions = ['mra'] other_workfile_extensions = ['spp', 'psd'] texture_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", "gif", "svg"] - color_space = ["lin_srgb", "raw", "acesg"] + # additional families (ftrack etc.) + workfile_families = [] + textures_families = [] - version_regex = re.compile(r"v([0-9]+)") - udim_regex = re.compile(r"_1[0-9]{3}\.") + color_space = ["linsRGB", "raw", "acesg"] #currently implemented placeholders ["color_space"] + # describing patterns in file names splitted by regex groups input_naming_patterns = { - # workfile: ctr_envCorridorMain_texturing_v005.mra > - # expected groups: [(asset),(filler),(version)] - # texture: T_corridorMain_aluminium1_BaseColor_lin_srgb_1029.exr - # expected groups: [(asset), (filler),(color_space),(udim)] - r'^ctr_env([^.]+)_(.+)_v([0-9]{3,}).+': - r'^T_([^_.]+)_(.*)_({color_space})_(1[0-9]{3}).+' + # workfile: corridorMain_v001.mra > + # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr + r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+': + r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + } + # matching regex group position to 'input_naming_patterns' + input_naming_groups = { + ('asset', 'filler', 'version'): + ('asset', 'shader', 'version', 'channel', 'color_space', 'udim') } workfile_subset_template = "textures{}Workfile" - # implemented keys: ["color_space", "channel", "subset"] - texture_subset_template = "textures{subset}_{channel}" - - version_regex = re.compile(r"^(.+)_v([0-9]+)") - udim_regex = re.compile(r"_1[0-9]{3}\.") + # implemented keys: ["color_space", "channel", "subset", "shader"] + texture_subset_template = "textures{subset}_{shader}_{channel}" def process(self, context): self.context = context - import json - def convertor(value): - return str(value) - resource_files = {} workfile_files = {} representations = {} @@ -76,8 +80,6 @@ class CollectTextures(pyblish.api.ContextPlugin): workfile_subset = self.workfile_subset_template.format( parsed_subset) - self.log.info("instance.data:: {}".format( - json.dumps(instance.data, indent=4, default=convertor))) processed_instance = False for repre in instance.data["representations"]: ext = repre["ext"].replace('.', '') @@ -94,9 +96,15 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_build = self._get_asset_build( repre_file, self.input_naming_patterns.keys(), + self.input_naming_groups.keys(), + self.color_space + ) + version = self._get_version( + repre_file, + self.input_naming_patterns.keys(), + self.input_naming_groups.keys(), self.color_space ) - version = self._get_version(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) processed_instance = True @@ -105,14 +113,17 @@ class CollectTextures(pyblish.api.ContextPlugin): representations[workfile_subset] = [] if ext in self.main_workfile_extensions: - representations[workfile_subset].append(repre) + # workfiles can have only single representation + # currently OP is not supporting different extensions in + # representation files + representations[workfile_subset] = [repre] + workfile_files[asset_build] = repre_file if ext in self.other_workfile_extensions: - self.log.info("other") # add only if not added already from main if not representations.get(workfile_subset): - representations[workfile_subset].append(repre) + representations[workfile_subset] = [repre] # only overwrite if not present if not workfile_files.get(asset_build): @@ -135,39 +146,49 @@ class CollectTextures(pyblish.api.ContextPlugin): channel = self._get_channel_name( repre_file, - list(self.input_naming_patterns.values()), + self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space + ) + + shader = self._get_shader_name( + repre_file, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), self.color_space ) formatting_data = { "color_space": c_space, "channel": channel, + "shader": shader, "subset": parsed_subset } - self.log.debug("data::{}".format(formatting_data)) subset = format_template_with_optional_keys( formatting_data, self.texture_subset_template) asset_build = self._get_asset_build( repre_file, self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space + ) + version = self._get_version( + repre_file, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), self.color_space ) - version = self._get_version(repre_file, self.version_regex) - if not representations.get(subset): representations[subset] = [] representations[subset].append(repre) - udim = self._parse_udim(repre_file, self.udim_regex) - - if not version_data.get(subset): - version_data[subset] = [] ver_data = { "color_space": c_space, - "UDIM": udim, + "channel_name": channel, + "shader_name": shader } - version_data[subset].append(ver_data) + version_data[subset] = ver_data asset_builds.add( (asset_build, version, subset, "textures")) @@ -176,7 +197,6 @@ class CollectTextures(pyblish.api.ContextPlugin): if processed_instance: self.context.remove(instance) - self.log.info("asset_builds:: {}".format(asset_builds)) self._create_new_instances(context, asset, asset_builds, @@ -195,9 +215,13 @@ class CollectTextures(pyblish.api.ContextPlugin): asset (string): selected asset from SP asset_builds (set) of tuples (asset_build, version, subset, family) - resource_files (list) of resource dicts - representations (dict) of representation files, key is - asset_build + resource_files (list) of resource dicts - to store additional + files to main workfile + representations (list) of dicts - to store workfile info OR + all collected texture files, key is asset_build + version_data (dict) - prepared to store into version doc in DB + workfile_files (dict) - to store workfile to add to textures + key is asset_build """ # sort workfile first asset_builds = sorted(asset_builds, @@ -217,28 +241,38 @@ class CollectTextures(pyblish.api.ContextPlugin): "name": subset, "family": family, "version": int(version or main_version), - "families": [] + "asset_build": asset_build # remove in validator } ) - if resource_files.get(subset): - new_instance.data.update({ - "resources": resource_files.get(subset) - }) - workfile = workfile_files.get(asset_build) + workfile = workfile_files.get(asset_build, "DUMMY") + + if resource_files.get(subset): + # add resources only when workfile is main style + for ext in self.main_workfile_extensions: + if ext in workfile: + new_instance.data.update({ + "resources": resource_files.get(subset) + }) + break # store origin if family == 'workfile': + families = self.workfile_families + new_instance.data["source"] = "standalone publisher" else: + families = self.textures_families + repre = representations.get(subset)[0] new_instance.context.data["currentFile"] = os.path.join( repre["stagingDir"], workfile) + new_instance.data["families"] = families + # add data for version document ver_data = version_data.get(subset) if ver_data: - ver_data = ver_data[0] if workfile: ver_data['workfile'] = workfile @@ -253,7 +287,13 @@ class CollectTextures(pyblish.api.ContextPlugin): new_instance.data["representations"] = upd_representations - def _get_asset_build(self, name, input_naming_patterns, color_spaces): + self.log.debug("new instance - {}:: {}".format( + family, + json.dumps(new_instance.data, indent=4))) + + def _get_asset_build(self, name, + input_naming_patterns, input_naming_groups, + color_spaces): """Loops through configured workfile patterns to find asset name. Asset name used to bind workfile and its textures. @@ -262,35 +302,34 @@ class CollectTextures(pyblish.api.ContextPlugin): name (str): workfile name input_naming_patterns (list): [workfile_pattern] or [texture_pattern] + input_naming_groups (list) + ordinal position of regex groups matching to input_naming.. + color_spaces (list) - predefined color spaces """ - for input_pattern in input_naming_patterns: - for cs in color_spaces: - pattern = input_pattern.replace('{color_space}', cs) - regex_result = re.findall(pattern, name) + asset_name = "NOT_AVAIL" - if regex_result: - asset_name = regex_result[0][0].lower() - return asset_name + return self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'asset') or asset_name - raise ValueError("Couldnt find asset name in {}".format(name)) + def _get_version(self, name, input_naming_patterns, input_naming_groups, + color_spaces): + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'version') - def _get_version(self, name, version_regex): - found = re.search(version_regex, name) if found: - return found.group().replace("v", "") + return found.replace('v', '') self.log.info("No version found in the name {}".format(name)) - def _get_udim(self, name, udim_regex): - """Parses from 'name' udim value with 'udim_regex'.""" - regex_result = udim_regex.findall(name) - udim = None - if not regex_result: - self.log.warning("Didn't find UDIM in {}".format(name)) - else: - udim = re.sub("[^0-9]", '', regex_result[0]) + def _get_udim(self, name, input_naming_patterns, input_naming_groups, + color_spaces): + """Parses from 'name' udim value.""" + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'udim') + if found: + return found - return udim + self.log.warning("Didn't find UDIM in {}".format(name)) def _get_color_space(self, name, color_spaces): """Looks for color_space from a list in a file name. @@ -314,18 +353,65 @@ class CollectTextures(pyblish.api.ContextPlugin): return color_space - def _get_channel_name(self, name, input_naming_patterns, color_spaces): + def _get_shader_name(self, name, input_naming_patterns, + input_naming_groups, color_spaces): + """Return parsed shader name. + + Shader name is needed for overlapping udims (eg. udims might be + used for different materials, shader needed to not overwrite). + + Unknown format of channel name and color spaces >> cs are known + list - 'color_space' used as a placeholder + """ + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'shader') + if found: + return found + + self.log.warning("Didn't find shader in {}".format(name)) + + def _get_channel_name(self, name, input_naming_patterns, + input_naming_groups, color_spaces): """Return parsed channel name. Unknown format of channel name and color spaces >> cs are known list - 'color_space' used as a placeholder """ - for texture_pattern in input_naming_patterns: + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'channel') + if found: + return found + + self.log.warning("Didn't find channel in {}".format(name)) + + def _parse(self, name, input_naming_patterns, input_naming_groups, + color_spaces, key): + """Universal way to parse 'name' with configurable regex groups. + + Args: + name (str): workfile name + input_naming_patterns (list): + [workfile_pattern] or [texture_pattern] + input_naming_groups (list) + ordinal position of regex groups matching to input_naming.. + color_spaces (list) - predefined color spaces + + Raises: + ValueError - if broken 'input_naming_groups' + """ + for input_pattern in input_naming_patterns: for cs in color_spaces: - pattern = texture_pattern.replace('{color_space}', cs) - ret = re.findall(pattern, name) - if ret: - return ret.pop()[1] + pattern = input_pattern.replace('{color_space}', cs) + regex_result = re.findall(pattern, name) + if regex_result: + idx = list(input_naming_groups)[0].index(key) + if idx < 0: + msg = "input_naming_groups must " +\ + "have '{}' key".format(key) + raise ValueError(msg) + + parsed_value = regex_result[0][idx] + return parsed_value def _update_representations(self, upd_representations): """Frames dont have sense for textures, add collected udims instead.""" @@ -335,15 +421,21 @@ class CollectTextures(pyblish.api.ContextPlugin): repre.pop("frameEnd", None) repre.pop("fps", None) + # ignore unique name from SP, use extension instead + # SP enforces unique name, here different subsets >> unique repres + repre["name"] = repre["ext"].replace('.', '') + files = repre.get("files", []) if not isinstance(files, list): files = [files] for file_name in files: - udim = self._get_udim(file_name, self.udim_regex) + udim = self._get_udim(file_name, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space) udims.append(udim) repre["udim"] = udims # must be this way, used for filling path return upd_representations - diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py index e222004456..af200b59e0 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -2,46 +2,21 @@ import pyblish.api import openpype.api -class ValidateTextureBatch(pyblish.api.ContextPlugin): - """Validates that collected instnaces for Texture batch are OK. +class ValidateTextureBatch(pyblish.api.InstancePlugin): + """Validates that some texture files are present.""" - Validates: - some textures are present - workfile has resource files (optional) - texture version matches to workfile version - """ - - label = "Validate Texture Batch" + label = "Validate Texture Presence" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder - families = ["workfile", "textures"] - - def process(self, context): - - workfiles = [] - workfiles_in_textures = [] - for instance in context: - if instance.data["family"] == "workfile": - workfiles.append(instance.data["representations"][0]["files"]) - - if not instance.data.get("resources"): - msg = "No resources for workfile {}".\ - format(instance.data["name"]) - self.log.warning(msg) + families = ["workfile"] + optional = False + def process(self, instance): + present = False + for instance in instance.context: if instance.data["family"] == "textures": - wfile = instance.data["versionData"]["workfile"] - workfiles_in_textures.append(wfile) + self.log.info("Some textures present.") - version_str = "v{:03d}".format(instance.data["version"]) - assert version_str in wfile, \ - "Not matching version, texture {} - workfile {}".format( - instance.data["version"], wfile - ) + return - msg = "Not matching set of workfiles and textures." + \ - "{} not equal to {}".format(set(workfiles), - set(workfiles_in_textures)) +\ - "\nCheck that both workfile and textures are present" - keys = set(workfiles) == set(workfiles_in_textures) - assert keys, msg + assert present, "No textures found in published batch!" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py new file mode 100644 index 0000000000..92f930c3fc --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py @@ -0,0 +1,50 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): + """Validates that all instances had properly formatted name.""" + + label = "Validate Texture Batch Naming" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile", "textures"] + optional = False + + def process(self, instance): + file_name = instance.data["representations"][0]["files"] + if isinstance(file_name, list): + file_name = file_name[0] + + msg = "Couldnt find asset name in '{}'\n".format(file_name) + \ + "File name doesn't follow configured pattern.\n" + \ + "Please rename the file." + assert "NOT_AVAIL" not in instance.data["asset_build"], msg + + instance.data.pop("asset_build") + + if instance.data["family"] == "textures": + file_name = instance.data["representations"][0]["files"][0] + self._check_proper_collected(instance.data["versionData"], + file_name) + + def _check_proper_collected(self, versionData, file_name): + """ + Loop through collected versionData to check if name parsing was OK. + Args: + versionData: (dict) + + Returns: + raises AssertionException + """ + missing_key_values = [] + for key, value in versionData.items(): + if not value: + missing_key_values.append(key) + + msg = "Collected data {} doesn't contain values for {}".format( + versionData, missing_key_values) + "\n" + \ + "Name of the texture file doesn't match expected pattern.\n" + \ + "Please rename file(s) {}".format(file_name) + + assert not missing_key_values, msg diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py new file mode 100644 index 0000000000..3985cb8933 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -0,0 +1,24 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): + """Validates that versions match in workfile and textures.""" + label = "Validate Texture Batch Versions" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["textures"] + optional = True + + def process(self, instance): + wfile = instance.data["versionData"]["workfile"] + + version_str = "v{:03d}".format(instance.data["version"]) + if 'DUMMY' in wfile: + self.log.warning("Textures are missing attached workfile") + else: + msg = "Not matching version: texture v{:03d} - workfile {}" + assert version_str in wfile, \ + msg.format( + instance.data["version"], wfile + ) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py new file mode 100644 index 0000000000..556a73dc4f --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -0,0 +1,22 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): + """Validates that textures workfile has collected resources (optional). + + Collected recourses means secondary workfiles (in most cases). + """ + + label = "Validate Texture Workfile" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile"] + optional = True + + def process(self, instance): + if instance.data["family"] == "workfile": + if not instance.data.get("resources"): + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + self.log.warning(msg) From 170b63ff1404a216337ee6a801b9492fc6796b9c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 10:54:02 +0200 Subject: [PATCH 10/43] Textures - added multiple validations --- .../plugins/publish/collect_texture.py | 4 +-- .../publish/extract_workfile_location.py | 3 +- .../publish/validate_texture_has_workfile.py | 20 +++++++++++ .../publish/validate_texture_versions.py | 36 +++++++++++++------ .../publish/validate_texture_workfiles.py | 9 +++-- 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index b8f8f05dc9..5a418dd8da 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -245,7 +245,7 @@ class CollectTextures(pyblish.api.ContextPlugin): } ) - workfile = workfile_files.get(asset_build, "DUMMY") + workfile = workfile_files.get(asset_build) if resource_files.get(subset): # add resources only when workfile is main style @@ -266,7 +266,7 @@ class CollectTextures(pyblish.api.ContextPlugin): repre = representations.get(subset)[0] new_instance.context.data["currentFile"] = os.path.join( - repre["stagingDir"], workfile) + repre["stagingDir"], workfile or 'dummy.txt') new_instance.data["families"] = families diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index 4345cef6dc..f91851c201 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -33,7 +33,8 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): filepath)) if not filepath: - raise ValueError("Texture batch doesn't contain workfile.") + self.log.info("Texture batch doesn't contain workfile.") + return # then apply to all textures for instance in context: diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py new file mode 100644 index 0000000000..7cd540668c --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py @@ -0,0 +1,20 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureHasWorkfile(pyblish.api.InstancePlugin): + """Validates that textures have appropriate workfile attached. + + Workfile is optional, disable this Validator after Refresh if you are + sure it is not needed. + """ + label = "Validate Texture Has Workfile" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["textures"] + optional = True + + def process(self, instance): + wfile = instance.data["versionData"].get("workfile") + + assert wfile, "Textures are missing attached workfile" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py index 3985cb8933..426151e390 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -3,22 +3,36 @@ import openpype.api class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): - """Validates that versions match in workfile and textures.""" + """Validates that versions match in workfile and textures. + + Workfile is optional, so if you are sure, you can disable this + validator after Refresh. + + Validates that only single version is published at a time. + """ label = "Validate Texture Batch Versions" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder families = ["textures"] - optional = True + optional = False def process(self, instance): - wfile = instance.data["versionData"]["workfile"] + wfile = instance.data["versionData"].get("workfile") version_str = "v{:03d}".format(instance.data["version"]) - if 'DUMMY' in wfile: - self.log.warning("Textures are missing attached workfile") - else: - msg = "Not matching version: texture v{:03d} - workfile {}" - assert version_str in wfile, \ - msg.format( - instance.data["version"], wfile - ) + + if not wfile: # no matching workfile, do not check versions + self.log.info("No workfile present for textures") + return + + msg = "Not matching version: texture v{:03d} - workfile {}" + assert version_str in wfile, \ + msg.format( + instance.data["version"], wfile + ) + + present_versions = [] + for instance in instance.context: + present_versions.append(instance.data["version"]) + + assert len(present_versions) == 1, "Too many versions in a batch!" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index 556a73dc4f..189246144d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -8,7 +8,7 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): Collected recourses means secondary workfiles (in most cases). """ - label = "Validate Texture Workfile" + label = "Validate Texture Workfile Has Resources" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder families = ["workfile"] @@ -16,7 +16,6 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["family"] == "workfile": - if not instance.data.get("resources"): - msg = "No resources for workfile {}".\ - format(instance.data["name"]) - self.log.warning(msg) + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + assert instance.data.get("resources"), msg From ae2dfc66f17ddc6d4a893037c9f497b4b88d6777 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 13:01:12 +0200 Subject: [PATCH 11/43] Textures - settings schema + defaults --- .../project_settings/standalonepublisher.json | 54 +++++++++ .../schema_project_standalonepublisher.json | 113 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 5590fa6349..37807983a8 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -149,6 +149,60 @@ } }, "publish": { + "CollectTextures": { + "enabled": true, + "active": true, + "main_workfile_extensions": [ + "mra" + ], + "other_workfile_extensions": [ + "spp", + "psd" + ], + "texture_extensions": [ + "exr", + "dpx", + "jpg", + "jpeg", + "png", + "tiff", + "tga", + "gif", + "svg" + ], + "workfile_families": [], + "texture_families": [], + "color_space": [ + "linsRGB", + "raw", + "acesg" + ], + "input_naming_patterns": { + "workfile": [ + "^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+" + ], + "textures": [ + "^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+" + ] + }, + "input_naming_groups": { + "workfile": [ + "asset", + "filler", + "version" + ], + "textures": [ + "asset", + "shader", + "version", + "channel", + "color_space", + "udim" + ] + }, + "workfile_subset_template": "textures{Subset}Workfile", + "texture_subset_template": "textures{Subset}_{Shader}_{Channel}" + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 0ef7612805..41e6360a86 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -56,6 +56,119 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectTextures", + "label": "Collect Textures", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "list", + "key": "main_workfile_extensions", + "object_type": "text", + "label": "Main workfile extensions" + }, + { + "key": "other_workfile_extensions", + "label": "Support workfile extensions", + "type": "list", + "object_type": "text" + }, + { + "type": "list", + "key": "texture_extensions", + "object_type": "text", + "label": "Texture extensions" + }, + { + "type": "list", + "key": "workfile_families", + "object_type": "text", + "label": "Additional families for workfile" + }, + { + "type": "list", + "key": "texture_families", + "object_type": "text", + "label": "Additional families for textures" + }, + { + "type": "list", + "key": "color_space", + "object_type": "text", + "label": "Color spaces" + }, + { + "type": "dict", + "collapsible": false, + "key": "input_naming_patterns", + "label": "Regex patterns for naming conventions", + "children": [ + { + "type": "label", + "label": "Add regex groups matching expected name" + }, + { + "type": "list", + "object_type": "text", + "key": "workfile", + "label": "Workfile naming pattern" + }, + { + "type": "list", + "object_type": "text", + "key": "textures", + "label": "Textures naming pattern" + } + ] + }, + { + "type": "dict", + "collapsible": false, + "key": "input_naming_groups", + "label": "Group order for regex patterns", + "children": [ + { + "type": "label", + "label": "Add names of matched groups in correct order. Available values: ('filler', 'asset', 'shader', 'version', 'channel', 'color_space', 'udim')" + }, + { + "type": "list", + "object_type": "text", + "key": "workfile", + "label": "Workfile group positions" + }, + { + "type": "list", + "object_type": "text", + "key": "textures", + "label": "Textures group positions" + } + ] + }, + { + "type": "text", + "key": "workfile_subset_template", + "label": "Subset name template for workfile" + }, + { + "type": "text", + "key": "texture_subset_template", + "label": "Subset name template for textures" + } + ] + }, { "type": "dict", "collapsible": true, From 218522338c057e8087e6a3090f136c17fab97701 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 13:02:53 +0200 Subject: [PATCH 12/43] Textures - changes because of settings --- .../plugins/publish/collect_texture.py | 59 +++++++++++-------- .../publish/validate_texture_versions.py | 4 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 5a418dd8da..0fa554aa8b 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -5,6 +5,8 @@ import json from avalon.api import format_template_with_optional_keys +from openpype.lib import prepare_template_data + class CollectTextures(pyblish.api.ContextPlugin): """Collect workfile (and its resource_files) and textures. @@ -44,18 +46,19 @@ class CollectTextures(pyblish.api.ContextPlugin): input_naming_patterns = { # workfile: corridorMain_v001.mra > # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr - r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+': - r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + "workfile": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+', + "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', } # matching regex group position to 'input_naming_patterns' input_naming_groups = { - ('asset', 'filler', 'version'): - ('asset', 'shader', 'version', 'channel', 'color_space', 'udim') + "workfile": ('asset', 'filler', 'version'), + "textures": ('asset', 'shader', 'version', 'channel', 'color_space', + 'udim') } - workfile_subset_template = "textures{}Workfile" + workfile_subset_template = "textures{Subset}Workfile" # implemented keys: ["color_space", "channel", "subset", "shader"] - texture_subset_template = "textures{subset}_{shader}_{channel}" + texture_subset_template = "textures{Subset}_{Shader}_{Channel}" def process(self, context): self.context = context @@ -77,8 +80,14 @@ class CollectTextures(pyblish.api.ContextPlugin): parsed_subset = instance.data["subset"].replace( instance.data["family"], '') - workfile_subset = self.workfile_subset_template.format( - parsed_subset) + + fill_pairs = { + "subset": parsed_subset + } + + fill_pairs = prepare_template_data(fill_pairs) + workfile_subset = format_template_with_optional_keys( + fill_pairs, self.workfile_subset_template) processed_instance = False for repre in instance.data["representations"]: @@ -95,14 +104,14 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_build = self._get_asset_build( repre_file, - self.input_naming_patterns.keys(), - self.input_naming_groups.keys(), + self.input_naming_patterns["workfile"], + self.input_naming_groups["workfile"], self.color_space ) version = self._get_version( repre_file, - self.input_naming_patterns.keys(), - self.input_naming_groups.keys(), + self.input_naming_patterns["workfile"], + self.input_naming_groups["workfile"], self.color_space ) asset_builds.add((asset_build, version, @@ -146,15 +155,15 @@ class CollectTextures(pyblish.api.ContextPlugin): channel = self._get_channel_name( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) shader = self._get_shader_name( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) @@ -164,19 +173,21 @@ class CollectTextures(pyblish.api.ContextPlugin): "shader": shader, "subset": parsed_subset } + + fill_pairs = prepare_template_data(formatting_data) subset = format_template_with_optional_keys( - formatting_data, self.texture_subset_template) + fill_pairs, self.texture_subset_template) asset_build = self._get_asset_build( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) version = self._get_version( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) if not representations.get(subset): @@ -404,7 +415,7 @@ class CollectTextures(pyblish.api.ContextPlugin): pattern = input_pattern.replace('{color_space}', cs) regex_result = re.findall(pattern, name) if regex_result: - idx = list(input_naming_groups)[0].index(key) + idx = list(input_naming_groups).index(key) if idx < 0: msg = "input_naming_groups must " +\ "have '{}' key".format(key) @@ -431,8 +442,8 @@ class CollectTextures(pyblish.api.ContextPlugin): for file_name in files: udim = self._get_udim(file_name, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space) udims.append(udim) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py index 426151e390..90d0e8e512 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -31,8 +31,8 @@ class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): instance.data["version"], wfile ) - present_versions = [] + present_versions = set() for instance in instance.context: - present_versions.append(instance.data["version"]) + present_versions.add(instance.data["version"]) assert len(present_versions) == 1, "Too many versions in a batch!" From bf1948b354791cbed390408fae1ffcc983f696a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 14:02:36 +0200 Subject: [PATCH 13/43] Textures - added documentation --- .../assets/standalone_creators.png | Bin 0 -> 13991 bytes .../settings_project_standalone.md | 81 ++++++++++++++++++ website/sidebars.js | 3 +- 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 website/docs/project_settings/assets/standalone_creators.png create mode 100644 website/docs/project_settings/settings_project_standalone.md diff --git a/website/docs/project_settings/assets/standalone_creators.png b/website/docs/project_settings/assets/standalone_creators.png new file mode 100644 index 0000000000000000000000000000000000000000..cfadfa305da0af097bb729015aca8896a7b6cc34 GIT binary patch literal 13991 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 literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_standalone.md b/website/docs/project_settings/settings_project_standalone.md new file mode 100644 index 0000000000..5180486d29 --- /dev/null +++ b/website/docs/project_settings/settings_project_standalone.md @@ -0,0 +1,81 @@ +--- +id: settings_project_standalone +title: Project Standalone Publisher Setting +sidebar_label: Standalone Publisher +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Project settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overriden per project. + +:::warning Default studio values +Projects always use default project values unless they have [project override](../admin_settings#project-overrides) (orage colour). Any changes in default project may affect all existing projects. +::: + +## Creator Plugins + +Contains list of implemented families to show in middle menu in Standalone Publisher. Each plugin must contain: +- name +- label +- family +- icon +- default subset(s) +- help (additional short information about family) + +![example of creator plugin](assets/standalone_creators.png) + +## Publish plugins + +### Collect Textures + +Serves to collect all needed information about workfiles and textures created from those. Allows to publish +main workfile (for example from Mari), additional worfiles (from Substance Painter) and exported textures. + +Available configuration: +- Main workfile extension - only single workfile can be "main" one +- Support workfile extensions - additional workfiles will be published to same folder as "main", just under `resourses` subfolder +- Texture extension - what kind of formats are expected for textures +- Additional families for workfile - should any family ('ftrack', 'review') be added to published workfile +- Additional families for textures - should any family ('ftrack', 'review') be added to published textures + +#### Naming conventions + +Implementation tries to be flexible and cover multiple naming conventions for workfiles and textures. + +##### Workfile naming pattern + +Provide regex matching pattern containing regex groups used to parse workfile name to learn needed information. (For example +build name.) + +Example: +```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` - parses `corridorMain_v001` into three groups: +- asset build (`corridorMain`) +- filler (in this case empty) +- version (`001`) + +In case of different naming pattern, additional groups could be added or removed. + +##### Workfile group positions + +For each matching regex group set in previous paragraph, its ordinal position is required (in case of need for addition of new groups etc.) +Number of groups added here must match number of parsing groups from `Workfile naming pattern`. + +Same configuration is available for texture files. + +##### Output names + +Output names of published workfiles and textures could be configured separately: +- Subset name template for workfile +- Subset name template for textures (implemented keys: ["color_space", "channel", "subset", "shader"]) + + +### Validate Scene Settings + +#### Check Frame Range for Extensions + +Configure families, file extension and task to validate that DB setting (frame range) matches currently published values. + +### ExtractThumbnailSP + +Plugin responsible for generating thumbnails, configure appropriate values for your version o ffmpeg. \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index d38973e40f..488814a385 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -65,7 +65,8 @@ module.exports = { label: "Project Settings", items: [ "project_settings/settings_project_global", - "project_settings/settings_project_nuke" + "project_settings/settings_project_nuke", + "project_settings/settings_project_standalone" ], }, ], From cb8aa03b64cf41e1289a6ed6d25d2143363f7b71 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 20 Jul 2021 11:19:16 +0200 Subject: [PATCH 14/43] Textures - fix - multiple version loaded at same time fails in better spot --- .../plugins/publish/collect_texture.py | 8 ++++++-- .../tools/standalonepublish/widgets/widget_drop_frame.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 0fa554aa8b..439168ea10 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -421,8 +421,12 @@ class CollectTextures(pyblish.api.ContextPlugin): "have '{}' key".format(key) raise ValueError(msg) - parsed_value = regex_result[0][idx] - return parsed_value + try: + parsed_value = regex_result[0][idx] + return parsed_value + except IndexError: + self.log.warning("Wrong index, probably " + "wrong name {}".format(name)) def _update_representations(self, upd_representations): """Frames dont have sense for textures, add collected udims instead.""" diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index 63dcb82e83..7fe43c4203 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -211,7 +211,8 @@ class DropDataFrame(QtWidgets.QFrame): folder_path = os.path.dirname(collection.head) if file_base[-1] in ['.', '_']: file_base = file_base[:-1] - file_ext = collection.tail + file_ext = os.path.splitext( + collection.format('{head}{padding}{tail}'))[1] repr_name = file_ext.replace('.', '') range = collection.format('{ranges}') From d2fa34b52b442f0b4f81754d8ded322fb8f6c6b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:52:01 +0200 Subject: [PATCH 15/43] store scene frame start to context --- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index d8bb03f541..79cc01740a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -155,6 +155,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), "sceneMarkOutState": mark_out_state == "set", + "sceneStartFrame": int(lib.execute_george("tv_startframe")), "sceneBgColor": self._get_bg_color() } self.log.debug( From bdd065a840418410132c8f12a71a17466946260a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:53:14 +0200 Subject: [PATCH 16/43] use scene start frame as an offset --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 536df2adb0..472d57db36 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -49,6 +49,14 @@ class ExtractSequence(pyblish.api.Extractor): family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.context.data["sceneMarkOut"] + + # Scene start frame offsets the output files, so we need to offset the + # marks. + scene_start_frame = instance.context.data["sceneStartFrame"] + difference = scene_start_frame - mark_in + mark_in += difference + mark_out += difference + # Frame start/end may be stored as float frame_start = int(instance.data["frameStart"]) frame_end = int(instance.data["frameEnd"]) From f079508c20fb86b7acba414b2ffc038c61f759bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:53:22 +0200 Subject: [PATCH 17/43] fix variable name --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 472d57db36..1df7512588 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -106,7 +106,7 @@ class ExtractSequence(pyblish.api.Extractor): self.log.warning(( "Lowering representation range to {} frames." " Changed frame end {} -> {}" - ).format(output_range + 1, mark_out, new_mark_out)) + ).format(output_range + 1, mark_out, new_output_frame_end)) output_frame_end = new_output_frame_end # ------------------------------------------------------------------- From 2b38639f9ef427b56fe9cc26f29634fd50d68fa7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:58:15 +0200 Subject: [PATCH 18/43] added validator for checking start frame --- .../plugins/publish/validate_start_frame.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py new file mode 100644 index 0000000000..d769d47736 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -0,0 +1,27 @@ +import pyblish.api +from avalon.tvpaint import lib + + +class RepairStartFrame(pyblish.api.Action): + """Repair start frame.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + lib.execute_george("tv_startframe 0") + + +class ValidateStartFrame(pyblish.api.ContextPlugin): + """Validate start frame being at frame 0.""" + + label = "Validate Start Frame" + order = pyblish.api.ValidatorOrder + hosts = ["tvpaint"] + actions = [RepairStartFrame] + optional = True + + def process(self, context): + start_frame = lib.execute_george("tv_startframe") + assert int(start_frame) == 0, "Start frame has to be frame 0." From 4e9ee047ae573c7cfe7c97bc6e43e6e7bd51630d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:58:22 +0200 Subject: [PATCH 19/43] added settings for new validator which is turned off by default --- .../settings/defaults/project_settings/tvpaint.json | 5 +++++ .../projects_schema/schema_project_tvpaint.json | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 763802a73f..47f486aa98 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -18,6 +18,11 @@ "optional": true, "active": true }, + "ValidateStartFrame": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateAssetName": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 67aa4b0a06..368141813f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -52,6 +52,17 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateStartFrame", + "label": "Validate Scene Start Frame", + "docstring": "Validate first frame of scene is set to '0'." + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 303f2d08cf075c13c1e172d7ef87370194910500 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 24 May 2021 14:39:27 +0000 Subject: [PATCH 20/43] Create draft PR for #1002 --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index d8be0bdb37..cfd4191e36 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443 +Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a From f804d4bd40cc5afc260c3a757046a3c8e385d248 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Jul 2021 16:07:15 +0200 Subject: [PATCH 21/43] pass right type to get_hierarchical_attributes_values --- .../ftrack/event_handlers_server/event_sync_to_avalon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index e60045bd50..1dd056adee 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -1259,7 +1259,7 @@ class SyncToAvalonEvent(BaseEvent): self.process_session, entity, hier_attrs, - self.cust_attr_types_by_id + self.cust_attr_types_by_id.values() ) for key, val in hier_values.items(): output[key] = val From e1e3dd4dd5cd95f6553190b3d690655d8e228d63 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 22 Jul 2021 21:41:41 +0200 Subject: [PATCH 22/43] Textures - fixed defaults Broken file name shouldnt fail in collect --- .../plugins/publish/collect_texture.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 439168ea10..d70a0a75b8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -41,13 +41,13 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["linsRGB", "raw", "acesg"] - #currently implemented placeholders ["color_space"] + # currently implemented placeholders ["color_space"] # describing patterns in file names splitted by regex groups input_naming_patterns = { # workfile: corridorMain_v001.mra > # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr "workfile": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+', - "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', # noqa } # matching regex group position to 'input_naming_patterns' input_naming_groups = { @@ -168,10 +168,10 @@ class CollectTextures(pyblish.api.ContextPlugin): ) formatting_data = { - "color_space": c_space, - "channel": channel, - "shader": shader, - "subset": parsed_subset + "color_space": c_space or '', # None throws exception + "channel": channel or '', + "shader": shader or '', + "subset": parsed_subset or '' } fill_pairs = prepare_template_data(formatting_data) @@ -195,9 +195,9 @@ class CollectTextures(pyblish.api.ContextPlugin): representations[subset].append(repre) ver_data = { - "color_space": c_space, - "channel_name": channel, - "shader_name": shader + "color_space": c_space or '', + "channel_name": channel or '', + "shader_name": shader or '' } version_data[subset] = ver_data @@ -251,7 +251,7 @@ class CollectTextures(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, - "version": int(version or main_version), + "version": int(version or main_version or 1), "asset_build": asset_build # remove in validator } ) From f6bfce0ae0a412ba19129c9562c62770b3bd5b76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 15:24:12 +0200 Subject: [PATCH 23/43] nuke: recreating creator node function --- openpype/hosts/nuke/api/lib.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index eefbcc5d20..5f898a9a67 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1739,3 +1739,68 @@ def process_workfile_builder(): log.info("Opening last workfile...") # open workfile open_file(last_workfile_path) + + +def recreate_instance(origin_node, avalon_data=None): + """Recreate input instance to different data + + Args: + origin_node (nuke.Node): Nuke node to be recreating from + avalon_data (dict, optional): data to be used in new node avalon_data + + Returns: + nuke.Node: newly created node + """ + knobs_wl = ["render", "publish", "review", "ypos", + "use_limit", "first", "last"] + # get data from avalon knobs + data = anlib.get_avalon_knob_data( + origin_node) + + # add input data to avalon data + if avalon_data: + data.update(avalon_data) + + # capture all node knobs allowed in op_knobs + knobs_data = {k: origin_node[k].value() + for k in origin_node.knobs() + for key in knobs_wl + if key in k} + + # get node dependencies + inputs = origin_node.dependencies() + outputs = origin_node.dependent() + + # remove the node + nuke.delete(origin_node) + + # create new node + # get appropriate plugin class + creator_plugin = None + for Creator in api.discover(api.Creator): + if Creator.__name__ == data["creator"]: + creator_plugin = Creator + break + + # create write node with creator + new_node_name = data["subset"] + new_node = creator_plugin(new_node_name, data["asset"]).process() + + # white listed knobs to the new node + for _k, _v in knobs_data.items(): + try: + print(_k, _v) + new_node[_k].setValue(_v) + except Exception as e: + print(e) + + # connect to original inputs + for i, n in enumerate(inputs): + new_node.setInput(i, n) + + # connect to outputs + if len(outputs) > 0: + for dn in outputs: + dn.setInput(0, new_node) + + return new_node From c64852c214400de0840442f69d414e6c6e821d0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 15:24:46 +0200 Subject: [PATCH 24/43] global: changing context validator to use recreator for nuke --- .../publish/validate_instance_in_context.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/validate_instance_in_context.py b/openpype/plugins/publish/validate_instance_in_context.py index 29f002f142..61b4d82027 100644 --- a/openpype/plugins/publish/validate_instance_in_context.py +++ b/openpype/plugins/publish/validate_instance_in_context.py @@ -92,15 +92,16 @@ class RepairSelectInvalidInstances(pyblish.api.Action): context_asset = context.data["assetEntity"]["name"] for instance in instances: - self.set_attribute(instance, context_asset) + if "nuke" in pyblish.api.registered_hosts(): + import openpype.hosts.nuke.api as nuke_api + origin_node = instance[0] + nuke_api.lib.recreate_instance( + origin_node, avalon_data={"asset": context_asset} + ) + else: + self.set_attribute(instance, context_asset) def set_attribute(self, instance, context_asset): - if "nuke" in pyblish.api.registered_hosts(): - import nuke - nuke.toNode( - instance.data.get("name") - )["avalon:asset"].setValue(context_asset) - if "maya" in pyblish.api.registered_hosts(): from maya import cmds cmds.setAttr( From 5293718d0ca15593fb1020c758fef5370ebeff72 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 16:36:10 +0200 Subject: [PATCH 25/43] publisher: missing version in subset prop --- .../plugins/publish/collect_editorial_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index dbf2574a9d..60a8cf48fc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -181,7 +181,8 @@ class CollectInstances(pyblish.api.InstancePlugin): } }) for subset, properities in self.subsets.items(): - if properities["version"] == 0: + version = properities.get("version") + if version and version == 0: properities.pop("version") # adding Review-able instance From 6dfac0797ba355bd5a010169e26ef591d16a3d29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 09:53:27 +0200 Subject: [PATCH 26/43] added funtion to load openpype default settings value --- openpype/settings/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 4a3e66de33..dcbfbf7334 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -315,6 +315,11 @@ class DuplicatedEnvGroups(Exception): super(DuplicatedEnvGroups, self).__init__(msg) +def load_openpype_default_settings(): + """Load openpype default settings.""" + return load_jsons_from_dir(DEFAULTS_DIR) + + def reset_default_settings(): global _DEFAULT_SETTINGS _DEFAULT_SETTINGS = None @@ -322,7 +327,7 @@ def reset_default_settings(): def get_default_settings(): # TODO add cacher - return load_jsons_from_dir(DEFAULTS_DIR) + return load_openpype_default_settings() # global _DEFAULT_SETTINGS # if _DEFAULT_SETTINGS is None: # _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) From 1a5266e91698d9f153e5a5a25a98f0e85140058e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:07 +0200 Subject: [PATCH 27/43] added function to load general environments --- openpype/settings/lib.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index dcbfbf7334..d917b18d61 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -873,6 +873,25 @@ def get_environments(): return find_environments(get_system_settings(False)) +def get_general_environments(): + """Get general environments. + + Function is implemented to be able load general environments without using + `get_default_settings`. + """ + # Use only openpype defaults. + # - prevent to use `get_system_settings` where `get_default_settings` + # is used + default_values = load_openpype_default_settings() + studio_overrides = get_studio_system_settings_overrides() + result = apply_overrides(default_values, studio_overrides) + environments = result["general"]["environment"] + + clear_metadata_from_settings(environments) + + return environments + + def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 22876bbbdee42d76976223658a04886b3a94f682 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:18 +0200 Subject: [PATCH 28/43] added few docstrings --- openpype/settings/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index d917b18d61..5c2c0dcd94 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -321,11 +321,20 @@ def load_openpype_default_settings(): def reset_default_settings(): + """Reset cache of default settings. Can't be used now.""" global _DEFAULT_SETTINGS _DEFAULT_SETTINGS = None def get_default_settings(): + """Get default settings. + + Todo: + Cache loaded defaults. + + Returns: + dict: Loaded default settings. + """ # TODO add cacher return load_openpype_default_settings() # global _DEFAULT_SETTINGS From 00ea737307da1af989fb7770e8212142a9853f25 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:46 +0200 Subject: [PATCH 29/43] start.py can use `get_general_environments` if is available --- start.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/start.py b/start.py index 1b5c25ae3a..419a956835 100644 --- a/start.py +++ b/start.py @@ -208,14 +208,21 @@ def set_openpype_global_environments() -> None: """Set global OpenPype's environments.""" import acre - from openpype.settings import get_environments + try: + from openpype.settings import get_general_environments - all_env = get_environments() + general_env = get_general_environments() + + except Exception: + # Backwards compatibility for OpenPype versions where + # `get_general_environments` does not exists yet + from openpype.settings import get_environments + + all_env = get_environments() + general_env = all_env["global"] - # TODO Global environments will be stored in "general" settings so loading - # will be modified and can be done in igniter. env = acre.merge( - acre.parse(all_env["global"]), + acre.parse(general_env), dict(os.environ) ) os.environ.clear() From f3f2d96bd370eefb16e52270c41a93ae43f547a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jul 2021 13:22:24 +0200 Subject: [PATCH 30/43] imageio: fix grouping --- .../projects_schema/schemas/schema_anatomy_imageio.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 2b2eab8868..3c589f9492 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -3,6 +3,7 @@ "key": "imageio", "label": "Color Management and Output Formats", "is_file": true, + "is_group": true, "children": [ { "key": "hiero", @@ -14,7 +15,6 @@ "type": "dict", "label": "Workfile", "collapsible": false, - "is_group": true, "children": [ { "type": "form", @@ -89,7 +89,6 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, - "is_group": true, "children": [ { "type": "list", @@ -124,7 +123,6 @@ "type": "dict", "label": "Viewer", "collapsible": false, - "is_group": true, "children": [ { "type": "text", @@ -138,7 +136,6 @@ "type": "dict", "label": "Workfile", "collapsible": false, - "is_group": true, "children": [ { "type": "form", @@ -236,7 +233,6 @@ "type": "dict", "label": "Nodes", "collapsible": true, - "is_group": true, "children": [ { "key": "requiredNodes", @@ -339,7 +335,6 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, - "is_group": true, "children": [ { "type": "list", From c250df6b57616d18c1ecb335b8145621eb5b33fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 26 Jul 2021 13:47:22 +0200 Subject: [PATCH 31/43] Textures publishing - fix - missing field --- .../plugins/publish/extract_workfile_location.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index f91851c201..18bf0394ae 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -26,6 +26,7 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): template_data = instance.data.get("anatomyData") rep_name = instance.data.get("representations")[0].get("name") template_data["representation"] = rep_name + template_data["ext"] = rep_name anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled["publish"]["path"] filepath = os.path.normpath(template_filled) From abda7f9afa6092631a4162ecf00739a9da039fb4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 26 Jul 2021 13:57:23 +0200 Subject: [PATCH 32/43] Textures publishing - added additional example for textures --- .../settings_project_standalone.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/website/docs/project_settings/settings_project_standalone.md b/website/docs/project_settings/settings_project_standalone.md index 5180486d29..b359dc70d0 100644 --- a/website/docs/project_settings/settings_project_standalone.md +++ b/website/docs/project_settings/settings_project_standalone.md @@ -49,19 +49,36 @@ Provide regex matching pattern containing regex groups used to parse workfile na build name.) Example: -```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` - parses `corridorMain_v001` into three groups: + +- pattern: ```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` +- with groups: ```["asset", "filler", "version"]``` + +parses `corridorMain_v001` into three groups: - asset build (`corridorMain`) - filler (in this case empty) - version (`001`) -In case of different naming pattern, additional groups could be added or removed. +Advanced example (for texture files): + +- pattern: ```^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+``` +- with groups: ```["asset", "shader", "version", "channel", "color_space", "udim"]``` + +parses `corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr`: +- asset build (`corridorMain`) +- shader (`aluminiumID`) +- version (`001`) +- channel (`baseColor`) +- color_space (`linsRGB`) +- udim (`1001`) + + +In case of different naming pattern, additional groups could be added or removed. Number of matching groups (`(...)`) must be same as number of items in `Group order for regex patterns` ##### Workfile group positions For each matching regex group set in previous paragraph, its ordinal position is required (in case of need for addition of new groups etc.) -Number of groups added here must match number of parsing groups from `Workfile naming pattern`. -Same configuration is available for texture files. +Number of groups added here must match number of parsing groups from `Workfile naming pattern`. ##### Output names From 4b62088e1b2b273410b17a71db8a9450f9e6c892 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 10:43:48 +0200 Subject: [PATCH 33/43] added setting to check create project structure by default --- .../settings/defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 7cf5568662..dae5a591e9 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -136,7 +136,8 @@ "Pypeclub", "Administrator", "Project manager" - ] + ], + "create_project_structure_checked": false }, "clean_hierarchical_attr": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index a94ebc8888..1cc08b96f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -441,6 +441,18 @@ "key": "role_list", "label": "Roles", "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "label", + "label": "Check \"Create project structure\" by default" + }, + { + "type": "boolean", + "key": "create_project_structure_checked", + "label": "Checked" } ] }, From aab871fea755064c78ab509024520b755a55884f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:24 +0200 Subject: [PATCH 34/43] use CUST_ATTR_AUTO_SYNC constance for custom attribute name --- .../ftrack/event_handlers_user/action_prepare_project.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 5c40ec0d30..43b8f34dfd 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -90,14 +90,12 @@ class PrepareProjectLocal(BaseAction): items.extend(ca_items) - # This item will be last (before enumerators) - # - sets value of auto synchronization - auto_sync_name = "avalon_auto_sync" + # Set value of auto synchronization auto_sync_value = project_entity["custom_attributes"].get( CUST_ATTR_AUTO_SYNC, False ) auto_sync_item = { - "name": auto_sync_name, + "name": CUST_ATTR_AUTO_SYNC, "type": "boolean", "value": auto_sync_value, "label": "AutoSync to Avalon" From fb1a39bd83c56dc5ebd809ad8ac3a3e4a97275e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:43 +0200 Subject: [PATCH 35/43] commit custom attributes changes --- .../event_handlers_user/action_prepare_project.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 43b8f34dfd..eddad851e3 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -393,10 +393,12 @@ class PrepareProjectLocal(BaseAction): project_settings.save() - entity = entities[0] - for key, value in custom_attribute_values.items(): - entity["custom_attributes"][key] = value - self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + # Change custom attributes on project + if custom_attribute_values: + for key, value in custom_attribute_values.items(): + project_entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + session.commit() return True From b180d7be2247f6e948cac9cedb81fec28f28804e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:55 +0200 Subject: [PATCH 36/43] add h3 to enum labels --- .../ftrack/event_handlers_user/action_prepare_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index eddad851e3..5f64adf920 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -246,7 +246,7 @@ class PrepareProjectLocal(BaseAction): multiselect_enumerators.append(self.item_splitter) multiselect_enumerators.append({ "type": "label", - "value": in_data["label"] + "value": "

{}

".format(in_data["label"]) }) default = in_data["default"] From ccce38eebbf1143b9f13d1889d261b9b2611e475 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:10:24 +0200 Subject: [PATCH 37/43] add create project structure checkbox --- .../action_prepare_project.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 5f64adf920..c53303b7f9 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -24,7 +24,9 @@ class PrepareProjectLocal(BaseAction): settings_key = "prepare_project" - # Key to store info about trigerring create folder structure + # Key to store info about trigerring create folder structure\ + create_project_structure_key = "create_folder_structure" + create_project_structure_identifier = "create.project.structure" item_splitter = {"type": "label", "value": "---"} _keys_order = ( "fps", @@ -103,6 +105,27 @@ class PrepareProjectLocal(BaseAction): # Add autosync attribute items.append(auto_sync_item) + # This item will be last before enumerators + # Ask if want to trigger Action Create Folder Structure + create_project_structure_checked = ( + project_settings + ["project_settings"] + ["ftrack"] + ["user_handlers"] + ["prepare_project"] + ["create_project_structure_checked"] + ).value + items.append({ + "type": "label", + "value": "

Want to create basic Folder Structure?

" + }) + items.append({ + "name": self.create_project_structure_key, + "type": "boolean", + "value": create_project_structure_checked, + "label": "Check if Yes" + }) + # Add enumerator items at the end for item in multiselect_enumerators: items.append(item) @@ -307,10 +330,13 @@ class PrepareProjectLocal(BaseAction): return items, multiselect_enumerators def launch(self, session, entities, event): - if not event['data'].get('values', {}): + in_data = event["data"].get("values") + if not in_data: return - in_data = event['data']['values'] + create_project_structure_checked = in_data.pop( + self.create_project_structure_key + ) root_values = {} root_key = "__root__" @@ -400,6 +426,11 @@ class PrepareProjectLocal(BaseAction): self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) session.commit() + # Trigger create project structure action + if create_project_structure_checked: + self.trigger_action( + self.create_project_structure_identifier, event + ) return True From 699c3b5e060b9a4b1cf397b566c198427930af8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:14:15 +0200 Subject: [PATCH 38/43] update server prepare project action with all changes --- .../action_prepare_project.py | 86 ++++++++++++++----- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py index 12d687bbf2..3a96ae3311 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py @@ -1,6 +1,8 @@ import json +from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings +from openpype.lib import create_project from openpype.modules.ftrack.lib import ( ServerAction, @@ -21,8 +23,24 @@ class PrepareProjectServer(ServerAction): role_list = ["Pypeclub", "Administrator", "Project Manager"] - # Key to store info about trigerring create folder structure + settings_key = "prepare_project" + item_splitter = {"type": "label", "value": "---"} + _keys_order = ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "resolutionHeight", + "resolutionWidth", + "pixelAspect", + "applications", + "tools_env", + "library_project", + ) def discover(self, session, entities, event): """Show only on project.""" @@ -47,13 +65,7 @@ class PrepareProjectServer(ServerAction): project_entity = entities[0] project_name = project_entity["full_name"] - try: - project_settings = ProjectSettings(project_name) - except ValueError: - return { - "message": "Project is not synchronized yet", - "success": False - } + project_settings = ProjectSettings(project_name) project_anatom_settings = project_settings["project_anatomy"] root_items = self.prepare_root_items(project_anatom_settings) @@ -78,14 +90,13 @@ class PrepareProjectServer(ServerAction): items.extend(ca_items) - # This item will be last (before enumerators) - # - sets value of auto synchronization - auto_sync_name = "avalon_auto_sync" + # This item will be last before enumerators + # Set value of auto synchronization auto_sync_value = project_entity["custom_attributes"].get( CUST_ATTR_AUTO_SYNC, False ) auto_sync_item = { - "name": auto_sync_name, + "name": CUST_ATTR_AUTO_SYNC, "type": "boolean", "value": auto_sync_value, "label": "AutoSync to Avalon" @@ -199,7 +210,18 @@ class PrepareProjectServer(ServerAction): str([key for key in attributes_to_set]) )) - for key, in_data in attributes_to_set.items(): + attribute_keys = set(attributes_to_set.keys()) + keys_order = [] + for key in self._keys_order: + if key in attribute_keys: + keys_order.append(key) + + attribute_keys = attribute_keys - set(keys_order) + for key in sorted(attribute_keys): + keys_order.append(key) + + for key in keys_order: + in_data = attributes_to_set[key] attr = in_data["object"] # initial item definition @@ -225,7 +247,7 @@ class PrepareProjectServer(ServerAction): multiselect_enumerators.append(self.item_splitter) multiselect_enumerators.append({ "type": "label", - "value": in_data["label"] + "value": "

{}

".format(in_data["label"]) }) default = in_data["default"] @@ -286,10 +308,10 @@ class PrepareProjectServer(ServerAction): return items, multiselect_enumerators def launch(self, session, entities, event): - if not event['data'].get('values', {}): + in_data = event["data"].get("values") + if not in_data: return - in_data = event['data']['values'] root_values = {} root_key = "__root__" @@ -337,7 +359,27 @@ class PrepareProjectServer(ServerAction): self.log.debug("Setting Custom Attribute values") - project_name = entities[0]["full_name"] + project_entity = entities[0] + project_name = project_entity["full_name"] + + # Try to find project document + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({ + "type": "project" + }) + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + if not project_doc: + project_code = project_entity["name"] + self.log.info("Creating project \"{} [{}]\"".format( + project_name, project_code + )) + create_project(project_name, project_code, dbcon=dbcon) + + dbcon.uninstall() + project_settings = ProjectSettings(project_name) project_anatomy_settings = project_settings["project_anatomy"] project_anatomy_settings["roots"] = root_data @@ -352,10 +394,12 @@ class PrepareProjectServer(ServerAction): project_settings.save() - entity = entities[0] - for key, value in custom_attribute_values.items(): - entity["custom_attributes"][key] = value - self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + # Change custom attributes on project + if custom_attribute_values: + for key, value in custom_attribute_values.items(): + project_entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + session.commit() return True From fcde4277e33422ffa2f67b656ad37d799925b25c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:15:17 +0200 Subject: [PATCH 39/43] removed slash from comment --- .../ftrack/event_handlers_user/action_prepare_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index c53303b7f9..ea0bfa2971 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -24,7 +24,7 @@ class PrepareProjectLocal(BaseAction): settings_key = "prepare_project" - # Key to store info about trigerring create folder structure\ + # Key to store info about trigerring create folder structure create_project_structure_key = "create_folder_structure" create_project_structure_identifier = "create.project.structure" item_splitter = {"type": "label", "value": "---"} From c2ffeb89538dc0fe845a98e78ec7ad34858ff7ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jul 2021 11:30:12 +0200 Subject: [PATCH 40/43] Textures publishing - tweaked validator Look for resources (secondary workfiles) only for main workfile. --- .../plugins/publish/validate_texture_workfiles.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index 189246144d..aa3aad71db 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -14,8 +14,16 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): families = ["workfile"] optional = True + # from presets + main_workfile_extensions = ['mra'] + def process(self, instance): if instance.data["family"] == "workfile": - msg = "No resources for workfile {}".\ + ext = instance.data["representations"][0]["ext"] + if ext not in self.main_workfile_extensions: + self.log.warning("Only secondary workfile present!") + return + + msg = "No secondary workfiles present for workfile {}".\ format(instance.data["name"]) assert instance.data.get("resources"), msg From 834d6b681697c2cd93f862789a8ba5d2666f2a71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 14:30:21 +0200 Subject: [PATCH 41/43] all anatomy children must be groups otherwise schema error is raised --- .../settings/entities/anatomy_entities.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/entities/anatomy_entities.py b/openpype/settings/entities/anatomy_entities.py index d048ffabba..9edd0d943c 100644 --- a/openpype/settings/entities/anatomy_entities.py +++ b/openpype/settings/entities/anatomy_entities.py @@ -1,5 +1,6 @@ from .dict_immutable_keys_entity import DictImmutableKeysEntity from .lib import OverrideState +from .exceptions import EntitySchemaError class AnatomyEntity(DictImmutableKeysEntity): @@ -23,3 +24,22 @@ class AnatomyEntity(DictImmutableKeysEntity): if not child_obj.has_project_override: child_obj.add_to_project_override() return super(AnatomyEntity, self).on_child_change(child_obj) + + def schema_validations(self): + non_group_children = [] + for key, child_obj in self.non_gui_children.items(): + if not child_obj.is_group: + non_group_children.append(key) + + if non_group_children: + _non_group_children = [ + "project_anatomy/{}".format(key) + for key in non_group_children + ] + reason = ( + "Anatomy must have all children as groups." + " Non-group children {}" + ).format(", ".join(_non_group_children)) + raise EntitySchemaError(self, reason) + + return super(AnatomyEntity, self).schema_validations() From 13f6661a7c88bde185d6033159227c92da3c0891 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 14:32:26 +0200 Subject: [PATCH 42/43] added brief description to readme --- openpype/settings/entities/schemas/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index d457e44e74..e5122094f6 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -577,6 +577,15 @@ How output of the schema could look like on save: } ``` +## Anatomy +Anatomy represents data stored on project document. + +### anatomy +- entity works similarly to `dict` +- anatomy has always all keys overriden with overrides + - overrides are not applied as all anatomy data must be available from project document + - all children must be groups + ## Proxy wrappers - should wraps multiple inputs only visually - these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled From 3f97ee17b3d141663576aa238a70affef10a89e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:53:10 +0200 Subject: [PATCH 43/43] modified error message --- openpype/settings/entities/anatomy_entities.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/anatomy_entities.py b/openpype/settings/entities/anatomy_entities.py index 9edd0d943c..489e1f8294 100644 --- a/openpype/settings/entities/anatomy_entities.py +++ b/openpype/settings/entities/anatomy_entities.py @@ -38,8 +38,11 @@ class AnatomyEntity(DictImmutableKeysEntity): ] reason = ( "Anatomy must have all children as groups." - " Non-group children {}" - ).format(", ".join(_non_group_children)) + " Set 'is_group' to `true` on > {}" + ).format(", ".join([ + '"{}"'.format(item) + for item in _non_group_children + ])) raise EntitySchemaError(self, reason) return super(AnatomyEntity, self).schema_validations()