From ed76a59d9d6fd691c8eaadd3e678952b583aa2a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 22:53:00 +0100 Subject: [PATCH 001/432] Remove unused code --- .../hosts/maya/plugins/publish/collect_render.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index cbddb86e53..cca3b43fec 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -104,13 +104,12 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if deadline_settings["enabled"]: deadline_url = render_instance.data.get("deadlineUrl") - self._rs = renderSetup.instance() - current_layer = self._rs.getVisibleRenderLayer() - maya_render_layers = { - layer.name(): layer for layer in self._rs.getRenderLayers() - } - self.maya_layers = maya_render_layers + # Retrieve render setup layers + rs = renderSetup.instance() + maya_render_layers = { + layer.name(): layer for layer in rs.getRenderLayers() + } for layer in collected_render_layers: try: @@ -473,10 +472,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): return pool_a, pool_b - def _get_overrides(self, layer): - rset = self.maya_layers[layer].renderSettingsCollectionInstance() - return rset.getOverrides() - @staticmethod def get_render_attribute(attr, layer): """Get attribute from render options. From f3ac88fb54732fc533e507f6064610a6fdbcd716 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 23:05:30 +0100 Subject: [PATCH 002/432] Move deadline url logic closer together --- .../maya/plugins/publish/collect_render.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index cca3b43fec..934f81e298 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -71,7 +71,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): def process(self, context): """Entry point to collector.""" render_instance = None - deadline_url = None for instance in context: if "rendering" in instance.data["families"]: @@ -95,16 +94,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): asset = api.Session["AVALON_ASSET"] workspace = context.data["workspaceDir"] - deadline_settings = ( - context.data - ["system_settings"] - ["modules"] - ["deadline"] - ) - - if deadline_settings["enabled"]: - deadline_url = render_instance.data.get("deadlineUrl") - # Retrieve render setup layers rs = renderSetup.instance() maya_render_layers = { @@ -348,8 +337,12 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "aovSeparator": aov_separator } - if deadline_url: - data["deadlineUrl"] = deadline_url + # Collect Deadline url if Deadline module is enabled + deadline_settings = ( + context.data["system_settings"]["modules"]["deadline"] + ) + if deadline_settings["enabled"]: + data["deadlineUrl"] = render_instance.data.get("deadlineUrl") if self.sync_workfile_version: data["version"] = context.data["version"] From 542f634d73f01ed81127d655ec0902fdc2d006b5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 23:07:10 +0100 Subject: [PATCH 003/432] Re-use "read" logic from avalon.maya --- .../hosts/maya/plugins/publish/collect_render.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 934f81e298..caee978b3f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -49,7 +49,8 @@ import maya.app.renderSetup.model.renderSetup as renderSetup import pyblish.api -from avalon import maya, api +import avalon.maya +from avalon import api from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501 from openpype.hosts.maya.api import lib @@ -352,16 +353,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance.data["version"] = context.data["version"] # Apply each user defined attribute as data - for attr in cmds.listAttr(layer, userDefined=True) or list(): - try: - value = cmds.getAttr("{}.{}".format(layer, attr)) - except Exception: - # Some attributes cannot be read directly, - # such as mesh and color attributes. These - # are considered non-essential to this - # particular publishing pipeline. - value = None - + for attr, value in avalon.maya.read(layer).items(): data[attr] = value # handle standalone renderers @@ -401,7 +393,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): dict: only overrides with values """ - attributes = maya.read(render_globals) + attributes = avalon.maya.read(render_globals) options = {"renderGlobals": {}} options["renderGlobals"]["Priority"] = attributes["priority"] From f18b12e354ba5ed523474df7262261aab63a9d25 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 23:08:55 +0100 Subject: [PATCH 004/432] Bugfix: use 'renderer' variable that was defined to correctly capture renderman independent of its versions --- openpype/hosts/maya/plugins/publish/collect_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index caee978b3f..059988c754 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -310,8 +310,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), - "renderer": self.get_render_attribute("currentRenderer", - layer=layer_name), + "renderer": renderer, # instance subset "family": "renderlayer", "families": ["renderlayer"], From 20c4f86b8fdc06e2016c46563a5c1181258fe0cc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 23:11:10 +0100 Subject: [PATCH 005/432] Preserve logic to get renderer from in the renderlayer --- openpype/hosts/maya/plugins/publish/collect_render.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 059988c754..f8d3761b7c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -155,9 +155,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): layer_name = "rs_{}".format(expected_layer_name) # collect all frames we are expecting to be rendered - renderer = cmds.getAttr( - "defaultRenderGlobals.currentRenderer" - ).lower() + renderer = self.get_render_attribute("currentRenderer", + layer=layer_name) # handle various renderman names if renderer.startswith("renderman"): renderer = "renderman" From a2c05a9f382645a6b37191aefd7b011a0f2ebd6d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 00:11:07 +0100 Subject: [PATCH 006/432] Simplify subset detection code --- .../maya/plugins/publish/collect_render.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index f8d3761b7c..aa5ac40be9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -135,22 +135,21 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.warning(msg) continue - # test if there are sets (subsets) to attach render to + # detect if there are sets (subsets) to attach render to sets = cmds.sets(layer, query=True) or [] attach_to = [] - if sets: - for s in sets: - if "family" not in cmds.listAttr(s): - continue + for s in sets: + if not cmds.attributeQuery("family", node=s, exists=True): + continue - attach_to.append( - { - "version": None, # we need integrator for that - "subset": s, - "family": cmds.getAttr("{}.family".format(s)), - } - ) - self.log.info(" -> attach render to: {}".format(s)) + attach_to.append( + { + "version": None, # we need integrator for that + "subset": s, + "family": cmds.getAttr("{}.family".format(s)), + } + ) + self.log.info(" -> attach render to: {}".format(s)) layer_name = "rs_{}".format(expected_layer_name) From 47622d5dd0a41c73fa9f459c119b18d95825aa16 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 00:18:03 +0100 Subject: [PATCH 007/432] Don't collect aov_separator from settings twice --- openpype/hosts/maya/plugins/publish/collect_render.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index aa5ac40be9..44114efd5d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -281,16 +281,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.info("collecting layer: {}".format(layer_name)) # Get layer specific settings, might be overrides - try: - aov_separator = self._aov_chars[( - context.data["project_settings"] - ["create"] - ["CreateRender"] - ["aov_separator"] - )] - except KeyError: - aov_separator = "_" - data = { "subset": expected_layer_name, "attachTo": attach_to, From 9f5eb074e474e0392fb02def4369ba61d07f0ef1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 00:22:10 +0100 Subject: [PATCH 008/432] Don't provide render instance to override render products aov separator and potentially other things. - Leave it up to validators to ensure the output matches what the user wanted it to match so we can never submit wrong renders. --- .../hosts/maya/plugins/publish/collect_render.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 44114efd5d..f4ba862955 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -160,22 +160,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if renderer.startswith("renderman"): renderer = "renderman" - try: - aov_separator = self._aov_chars[( - context.data["project_settings"] - ["create"] - ["CreateRender"] - ["aov_separator"] - )] - except KeyError: - aov_separator = "_" - - render_instance.data["aovSeparator"] = aov_separator - # return all expected files for all cameras and aovs in given # frame range - layer_render_products = get_layer_render_products( - layer_name, render_instance) + layer_render_products = get_layer_render_products(layer_name) render_products = layer_render_products.layer_data.products assert render_products, "no render products generated" exp_files = [] From 44003cc95292ad7983c56335ecc16b8d4606489d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 00:37:23 +0100 Subject: [PATCH 009/432] Move logic closer together --- openpype/hosts/maya/plugins/publish/collect_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index f4ba862955..3aa1335c74 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -215,6 +215,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) aov_dict[aov.keys()[0]] = full_paths + full_exp_files.append(aov_dict) frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) @@ -238,8 +239,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): frame_start_handle = frame_start_render frame_end_handle = frame_end_render - full_exp_files.append(aov_dict) - # find common path to store metadata # so if image prefix is branching to many directories # metadata file will be located in top-most common From ae35f0e7ab292f2b9d0b2126629e9da11fb3a0ca Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 01:38:27 +0100 Subject: [PATCH 010/432] Refactor the "set_default_render_settings" logic out of CreateRender - This is a first step to allow the default render settings to be applied from elsewhere. - Also simplifies the logic of the actual Creator --- .../maya/plugins/create/create_render.py | 278 +++++++++--------- 1 file changed, 140 insertions(+), 138 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fa5e73f3ed..ff230a0ff2 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -24,6 +24,142 @@ from avalon.api import Session from avalon.api import CreatorError +class RenderSettings(object): + + _image_prefix_nodes = { + 'mentalray': 'defaultRenderGlobals.imageFilePrefix', + 'vray': 'vraySettings.fileNamePrefix', + 'arnold': 'defaultRenderGlobals.imageFilePrefix', + 'renderman': 'defaultRenderGlobals.imageFilePrefix', + 'redshift': 'defaultRenderGlobals.imageFilePrefix' + } + + _image_prefixes = { + 'mentalray': 'maya///{aov_separator}', # noqa + 'vray': 'maya///', + 'arnold': 'maya///{aov_separator}', # noqa + 'renderman': 'maya///{aov_separator}', + 'redshift': 'maya///{aov_separator}' # noqa + } + + _aov_chars = { + "dot": ".", + "dash": "-", + "underscore": "_" + } + + def __init__(self, project_settings): + self._project_settings = project_settings + + @staticmethod + def apply_defaults(renderer, project_settings=None): + if project_settings is None: + project_settings = get_project_settings(Session["AVALON_PROJECT"]) + + render_settings = RenderSettings(project_settings) + render_settings.set_default_renderer_settings(renderer) + + def set_default_renderer_settings(self, renderer): + """Set basic settings based on renderer. + + Args: + renderer (str): Renderer name. + + """ + # project_settings/maya/create/CreateRender/aov_separator + try: + aov_separator = self._aov_chars[( + self._project_settings["maya"] + ["create"] + ["CreateRender"] + ["aov_separator"] + )] + except KeyError: + aov_separator = "_" + + prefix = self._image_prefixes[renderer] + prefix = prefix.replace("{aov_separator}", aov_separator) + cmds.setAttr(self._image_prefix_nodes[renderer], + prefix, + type="string") + + asset = get_asset() + width = asset["data"].get("resolutionWidth") + height = asset["data"].get("resolutionHeight") + + if renderer == "arnold": + # set format to exr + cmds.setAttr( + "defaultArnoldDriver.ai_translator", "exr", type="string") + self._set_global_output_settings() + + # resolution + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + + if renderer == "vray": + self._set_vray_settings(aov_separator, width, height) + + if renderer == "redshift": + # set format to exr + cmds.setAttr("RedshiftOptions.imageFormat", 1) + + # resolution + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + + self._set_global_output_settings() + + def _set_vray_settings(self, aov_separator, width, height): + # type: (dict) -> None + """Sets important settings for Vray.""" + settings = cmds.ls(type="VRaySettingsNode") + node = settings[0] if settings else cmds.createNode("VRaySettingsNode") + + # Set aov separator + # First we need to explicitly set the UI items in Render Settings + # because that is also what V-Ray updates to when that Render Settings + # UI did initialize before and refreshes again. + MENU = "vrayRenderElementSeparator" + if cmds.optionMenuGrp(MENU, query=True, exists=True): + items = cmds.optionMenuGrp(MENU, query=True, ill=True) + separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 + try: + sep_idx = separators.index(aov_separator) + except ValueError: + raise CreatorError( + "AOV character {} not in {}".format( + aov_separator, separators)) + + cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1) + + # Set the render element attribute as string. This is also what V-Ray + # sets whenever the `vrayRenderElementSeparator` menu items switch + cmds.setAttr( + "{}.fileNameRenderElementSeparator".format(node), + aov_separator, + type="string" + ) + + # set format to exr + cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") + + # animType + cmds.setAttr("{}.animType".format(node), 1) + + # resolution + cmds.setAttr("{}.width".format(node), width) + cmds.setAttr("{}.height".format(node), height) + + @staticmethod + def _set_global_output_settings(): + # enable animation + cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) + cmds.setAttr("defaultRenderGlobals.animation", 1) + cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) + cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) + + class CreateRender(plugin.Creator): """Create *render* instance. @@ -70,31 +206,6 @@ class CreateRender(plugin.Creator): _user = None _password = None - # renderSetup instance - _rs = None - - _image_prefix_nodes = { - 'mentalray': 'defaultRenderGlobals.imageFilePrefix', - 'vray': 'vraySettings.fileNamePrefix', - 'arnold': 'defaultRenderGlobals.imageFilePrefix', - 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' - } - - _image_prefixes = { - 'mentalray': 'maya///{aov_separator}', # noqa - 'vray': 'maya///', - 'arnold': 'maya///{aov_separator}', # noqa - 'renderman': 'maya///{aov_separator}', - 'redshift': 'maya///{aov_separator}' # noqa - } - - _aov_chars = { - "dot": ".", - "dash": "-", - "underscore": "_" - } - _project_settings = None def __init__(self, *args, **kwargs): @@ -107,17 +218,6 @@ class CreateRender(plugin.Creator): self._project_settings = get_project_settings( Session["AVALON_PROJECT"]) - # project_settings/maya/create/CreateRender/aov_separator - try: - self.aov_separator = self._aov_chars[( - self._project_settings["maya"] - ["create"] - ["CreateRender"] - ["aov_separator"] - )] - except KeyError: - self.aov_separator = "_" - try: default_servers = deadline_settings["deadline_urls"] project_servers = ( @@ -174,8 +274,8 @@ class CreateRender(plugin.Creator): ]) cmds.setAttr("{}.machineList".format(self.instance), lock=True) - self._rs = renderSetup.instance() - layers = self._rs.getRenderLayers() + rs = renderSetup.instance() + layers = rs.getRenderLayers() if use_selection: print(">>> processing existing layers") sets = [] @@ -190,7 +290,7 @@ class CreateRender(plugin.Creator): # if no render layers are present, create default one with # asterisk selector if not layers: - render_layer = self._rs.createRenderLayer('Main') + render_layer = rs.createRenderLayer('Main') collection = render_layer.createCollection("defaultCollection") collection.getSelector().setPattern('*') @@ -200,7 +300,7 @@ class CreateRender(plugin.Creator): if renderer.startswith('renderman'): renderer = 'renderman' - self._set_default_renderer_settings(renderer) + RenderSettings.apply_defaults(renderer) return self.instance def _deadline_webservice_changed(self): @@ -422,101 +522,3 @@ class CreateRender(plugin.Creator): if "verify" not in kwargs: kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) return requests.get(*args, **kwargs) - - def _set_default_renderer_settings(self, renderer): - """Set basic settings based on renderer. - - Args: - renderer (str): Renderer name. - - """ - prefix = self._image_prefixes[renderer] - prefix = prefix.replace("{aov_separator}", self.aov_separator) - cmds.setAttr(self._image_prefix_nodes[renderer], - prefix, - type="string") - - asset = get_asset() - - if renderer == "arnold": - # set format to exr - - cmds.setAttr( - "defaultArnoldDriver.ai_translator", "exr", type="string") - self._set_global_output_settings() - # resolution - cmds.setAttr( - "defaultResolution.width", - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "defaultResolution.height", - asset["data"].get("resolutionHeight")) - - if renderer == "vray": - self._set_vray_settings(asset) - if renderer == "redshift": - _ = self._set_renderer_option( - "RedshiftOptions", "{}.imageFormat", 1 - ) - - # resolution - cmds.setAttr( - "defaultResolution.width", - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "defaultResolution.height", - asset["data"].get("resolutionHeight")) - - self._set_global_output_settings() - - def _set_vray_settings(self, asset): - # type: (dict) -> None - """Sets important settings for Vray.""" - settings = cmds.ls(type="VRaySettingsNode") - node = settings[0] if settings else cmds.createNode("VRaySettingsNode") - - # set separator - # set it in vray menu - if cmds.optionMenuGrp("vrayRenderElementSeparator", exists=True, - q=True): - items = cmds.optionMenuGrp( - "vrayRenderElementSeparator", ill=True, query=True) - - separators = [cmds.menuItem(i, label=True, query=True) for i in items] # noqa: E501 - try: - sep_idx = separators.index(self.aov_separator) - except ValueError: - raise CreatorError( - "AOV character {} not in {}".format( - self.aov_separator, separators)) - - cmds.optionMenuGrp( - "vrayRenderElementSeparator", sl=sep_idx + 1, edit=True) - cmds.setAttr( - "{}.fileNameRenderElementSeparator".format(node), - self.aov_separator, - type="string" - ) - # set format to exr - cmds.setAttr( - "{}.imageFormatStr".format(node), "exr", type="string") - - # animType - cmds.setAttr( - "{}.animType".format(node), 1) - - # resolution - cmds.setAttr( - "{}.width".format(node), - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "{}.height".format(node), - asset["data"].get("resolutionHeight")) - - @staticmethod - def _set_global_output_settings(): - # enable animation - cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) - cmds.setAttr("defaultRenderGlobals.animation", 1) - cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) - cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) From 542918135f88f2a38e60d9066d23402f9c11e9e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 01:42:55 +0100 Subject: [PATCH 011/432] Move more logic to the RenderSettings class --- .../hosts/maya/plugins/create/create_render.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index ff230a0ff2..05af3f32a9 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -52,7 +52,14 @@ class RenderSettings(object): self._project_settings = project_settings @staticmethod - def apply_defaults(renderer, project_settings=None): + def apply_defaults(renderer=None, project_settings=None): + if renderer is None: + renderer = cmds.getAttr( + 'defaultRenderGlobals.currentRenderer').lower() + # handle various renderman names + if renderer.startswith('renderman'): + renderer = 'renderman' + if project_settings is None: project_settings = get_project_settings(Session["AVALON_PROJECT"]) @@ -294,13 +301,7 @@ class CreateRender(plugin.Creator): collection = render_layer.createCollection("defaultCollection") collection.getSelector().setPattern('*') - renderer = cmds.getAttr( - 'defaultRenderGlobals.currentRenderer').lower() - # handle various renderman names - if renderer.startswith('renderman'): - renderer = 'renderman' - - RenderSettings.apply_defaults(renderer) + RenderSettings.apply_defaults() return self.instance def _deadline_webservice_changed(self): From 17be6f0f038d91c782b03d4fec3448e088ecfdf0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 01:48:28 +0100 Subject: [PATCH 012/432] Use log instead of print --- openpype/hosts/maya/plugins/create/create_render.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 05af3f32a9..fd37b8a709 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -284,10 +284,10 @@ class CreateRender(plugin.Creator): rs = renderSetup.instance() layers = rs.getRenderLayers() if use_selection: - print(">>> processing existing layers") + self.log.info("Processing existing layers") sets = [] for layer in layers: - print(" - creating set for {}:{}".format( + self.log.info(" - creating set for {}:{}".format( namespace, layer.name())) render_set = cmds.sets( n="{}:{}".format(namespace, layer.name())) @@ -301,6 +301,7 @@ class CreateRender(plugin.Creator): collection = render_layer.createCollection("defaultCollection") collection.getSelector().setPattern('*') + self.log.info("Applying default render settings..") RenderSettings.apply_defaults() return self.instance From 8b0f60eaaa5789a742b2db3feb556e6d4659be70 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 11:24:34 +0100 Subject: [PATCH 013/432] Collect the AOV separator for Render Products in Layer Data --- openpype/hosts/maya/api/lib_renderproducts.py | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index e8e4b9aaef..db2c6c1fdc 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -97,6 +97,12 @@ class LayerMetadata(object): # Render Products products = attr.ib(init=False, default=attr.Factory(list)) + # The AOV separator token. Note that not all renderers define an explicit + # render separator but allow to put the AOV/RenderPass token anywhere in + # the file path prefix. For those renderers we'll fall back to whatever + # is between the last occurrences of and tokens. + aov_separator = attr.ib(default="_") + @attr.s class RenderProduct(object): @@ -180,7 +186,6 @@ class ARenderProducts: self.layer = layer self.render_instance = render_instance self.multipart = False - self.aov_separator = render_instance.data.get("aovSeparator", "_") # Initialize self.layer_data = self._get_layer_data() @@ -316,6 +321,31 @@ class ARenderProducts: # defaultRenderLayer renders as masterLayer layer_name = "masterLayer" + # AOV separator - default behavior extracts the part between + # last occurences of and + # todo: This code also triggers for V-Ray which overrides it explicitly + # so this code will invalidly debug log it couldn't extract the + # aov separator even though it does set it in RenderProductsVray + layer_tokens = ["", ""] + aov_tokens = ["", ""] + + def match_last(tokens, text): + """regex match the last occurence from a list of tokens""" + pattern = "(?:.*)({})".format("|".join(tokens)) + return re.search(pattern, text, re.IGNORECASE) + + layer_match = match_last(layer_tokens, file_prefix) + aov_match = match_last(aov_tokens, file_prefix) + kwargs = {} + if layer_match and aov_match: + matches = sorted((layer_match, aov_match), + key=lambda match: match.end(1)) + separator = file_prefix[matches[0].end(1):matches[1].start(1)] + kwargs["aov_separator"] = separator + else: + log.debug("Couldn't extract aov separator from " + "file prefix: {}".format(file_prefix)) + # todo: Support Custom Frames sequences 0,5-10,100-120 # Deadline allows submitting renders with a custom frame list # to support those cases we might want to allow 'custom frames' @@ -332,7 +362,8 @@ class ARenderProducts: layerName=layer_name, renderer=self.renderer, defaultExt=self._get_attr("defaultRenderGlobals.imfPluginKey"), - filePrefix=file_prefix + filePrefix=file_prefix, + **kwargs ) def _generate_file_sequence( @@ -677,9 +708,15 @@ class RenderProductsVray(ARenderProducts): """ prefix = super(RenderProductsVray, self).get_renderer_prefix() - prefix = "{}{}".format(prefix, self.aov_separator) + aov_separator = self._get_aov_separator() + prefix = "{}{}".format(prefix, aov_separator) return prefix + def _get_aov_separator(self): + return self._get_attr( + "vraySettings.fileNameRenderElementSeparator" + ) + def _get_layer_data(self): # type: () -> LayerMetadata """Override to get vray specific extension.""" @@ -691,6 +728,8 @@ class RenderProductsVray(ARenderProducts): layer_data.defaultExt = default_ext layer_data.padding = self._get_attr("vraySettings.fileNamePadding") + layer_data.aov_separator = self._get_aov_separator() + return layer_data def get_render_products(self): From 0516bb0f6f5fa43be7a087dd1664bc1b8bb75425 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 11:39:17 +0100 Subject: [PATCH 014/432] Fix Redshift appending . even when or was explicitly set. --- openpype/hosts/maya/api/lib_renderproducts.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index db2c6c1fdc..49b6d6f0da 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -80,6 +80,13 @@ IMAGE_PREFIXES = { } +def has_tokens(string, tokens): + """Return whether any of tokens is in input string (case-insensitive)""" + pattern = "({})".format("|".join(re.escape(token) for token in tokens)) + match = re.search(pattern, string, re.IGNORECASE) + return bool(match) + + @attr.s class LayerMetadata(object): """Data class for Render Layer metadata.""" @@ -950,7 +957,11 @@ class RenderProductsRedshift(ARenderProducts): """ prefix = super(RenderProductsRedshift, self).get_renderer_prefix() - prefix = "{}.".format(prefix) + + # Only append . if no or is specified + if not has_tokens(prefix, ["", ""]): + prefix = "{}.".format(prefix) + return prefix def get_render_products(self): From f8e8ce5c61d485b753595f4e2dc9e0e20f1321c3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 11:39:43 +0100 Subject: [PATCH 015/432] Add docstring --- openpype/hosts/maya/api/lib_renderproducts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 49b6d6f0da..0e1b553619 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -720,6 +720,8 @@ class RenderProductsVray(ARenderProducts): return prefix def _get_aov_separator(self): + # type: () -> str + """Return the V-Ray AOV/Render Elements separator""" return self._get_attr( "vraySettings.fileNameRenderElementSeparator" ) From 862167fc6aa6bec3793b120eac8f84cd901a5035 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 14:23:48 +0100 Subject: [PATCH 016/432] Fix types --- openpype/hosts/maya/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fd37b8a709..f87e1eac5d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -118,7 +118,7 @@ class RenderSettings(object): self._set_global_output_settings() def _set_vray_settings(self, aov_separator, width, height): - # type: (dict) -> None + # type: (str, int, int) -> None """Sets important settings for Vray.""" settings = cmds.ls(type="VRaySettingsNode") node = settings[0] if settings else cmds.createNode("VRaySettingsNode") From ee3a3632731a5afbb3c7355f339f1818990a04ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 16:03:12 +0100 Subject: [PATCH 017/432] Move RenderSettings into its on api --- openpype/hosts/maya/api/render_settings.py | 165 +++++++++++++++++ .../maya/plugins/create/create_render.py | 173 ++---------------- .../publish/validate_render_single_camera.py | 18 +- 3 files changed, 186 insertions(+), 170 deletions(-) create mode 100644 openpype/hosts/maya/api/render_settings.py diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py new file mode 100644 index 0000000000..14f6468d1b --- /dev/null +++ b/openpype/hosts/maya/api/render_settings.py @@ -0,0 +1,165 @@ +import os +import sys + +from maya import cmds +import maya.app.renderSetup.model.renderSetup as renderSetup + +from openpype.hosts.maya.api import ( + lib, + plugin +) +from openpype.api import ( + get_system_settings, + get_project_settings, + get_asset) +from openpype.modules import ModulesManager + +from avalon.api import Session +from avalon.api import CreatorError + + +class RenderSettings(object): + + _image_prefix_nodes = { + 'mentalray': 'defaultRenderGlobals.imageFilePrefix', + 'vray': 'vraySettings.fileNamePrefix', + 'arnold': 'defaultRenderGlobals.imageFilePrefix', + 'renderman': 'defaultRenderGlobals.imageFilePrefix', + 'redshift': 'defaultRenderGlobals.imageFilePrefix' + } + + _image_prefixes = { + 'mentalray': 'maya///{aov_separator}', # noqa + 'vray': 'maya///', + 'arnold': 'maya///{aov_separator}', # noqa + 'renderman': 'maya///{aov_separator}', + 'redshift': 'maya///{aov_separator}' # noqa + } + + _aov_chars = { + "dot": ".", + "dash": "-", + "underscore": "_" + } + + @classmethod + def get_image_prefix_attr(cls, renderer): + return cls._image_prefix_nodes[renderer] + + def __init__(self, project_settings): + self._project_settings = project_settings + + @staticmethod + def apply_defaults(renderer=None, project_settings=None): + if renderer is None: + renderer = cmds.getAttr( + 'defaultRenderGlobals.currentRenderer').lower() + # handle various renderman names + if renderer.startswith('renderman'): + renderer = 'renderman' + + if project_settings is None: + project_settings = get_project_settings(Session["AVALON_PROJECT"]) + + render_settings = RenderSettings(project_settings) + render_settings.set_default_renderer_settings(renderer) + + def set_default_renderer_settings(self, renderer): + """Set basic settings based on renderer. + + Args: + renderer (str): Renderer name. + + """ + # project_settings/maya/create/CreateRender/aov_separator + try: + aov_separator = self._aov_chars[( + self._project_settings["maya"] + ["create"] + ["CreateRender"] + ["aov_separator"] + )] + except KeyError: + aov_separator = "_" + + prefix = self._image_prefixes[renderer] + prefix = prefix.replace("{aov_separator}", aov_separator) + cmds.setAttr(self._image_prefix_nodes[renderer], + prefix, + type="string") + + asset = get_asset() + width = asset["data"].get("resolutionWidth") + height = asset["data"].get("resolutionHeight") + + if renderer == "arnold": + # set format to exr + cmds.setAttr( + "defaultArnoldDriver.ai_translator", "exr", type="string") + self._set_global_output_settings() + + # resolution + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + + if renderer == "vray": + self._set_vray_settings(aov_separator, width, height) + + if renderer == "redshift": + # set format to exr + cmds.setAttr("RedshiftOptions.imageFormat", 1) + + # resolution + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + + self._set_global_output_settings() + + def _set_vray_settings(self, aov_separator, width, height): + # type: (str, int, int) -> None + """Sets important settings for Vray.""" + settings = cmds.ls(type="VRaySettingsNode") + node = settings[0] if settings else cmds.createNode("VRaySettingsNode") + + # Set aov separator + # First we need to explicitly set the UI items in Render Settings + # because that is also what V-Ray updates to when that Render Settings + # UI did initialize before and refreshes again. + MENU = "vrayRenderElementSeparator" + if cmds.optionMenuGrp(MENU, query=True, exists=True): + items = cmds.optionMenuGrp(MENU, query=True, ill=True) + separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 + try: + sep_idx = separators.index(aov_separator) + except ValueError: + raise CreatorError( + "AOV character {} not in {}".format( + aov_separator, separators)) + + cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1) + + # Set the render element attribute as string. This is also what V-Ray + # sets whenever the `vrayRenderElementSeparator` menu items switch + cmds.setAttr( + "{}.fileNameRenderElementSeparator".format(node), + aov_separator, + type="string" + ) + + # set format to exr + cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") + + # animType + cmds.setAttr("{}.animType".format(node), 1) + + # resolution + cmds.setAttr("{}.width".format(node), width) + cmds.setAttr("{}.height".format(node), height) + + @staticmethod + def _set_global_output_settings(): + # enable animation + cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) + cmds.setAttr("defaultRenderGlobals.animation", 1) + cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) + cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) \ No newline at end of file diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index f87e1eac5d..b75105736b 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -1,170 +1,27 @@ # -*- coding: utf-8 -*- """Create ``Render`` instance in Maya.""" -import os import json +import os +import sys + import appdirs import requests import six -import sys from maya import cmds -import maya.app.renderSetup.model.renderSetup as renderSetup - -from openpype.hosts.maya.api import ( - lib, - plugin -) -from openpype.api import ( - get_system_settings, - get_project_settings, - get_asset) -from openpype.modules import ModulesManager +from maya.app.renderSetup.model import renderSetup from avalon.api import Session -from avalon.api import CreatorError - - -class RenderSettings(object): - - _image_prefix_nodes = { - 'mentalray': 'defaultRenderGlobals.imageFilePrefix', - 'vray': 'vraySettings.fileNamePrefix', - 'arnold': 'defaultRenderGlobals.imageFilePrefix', - 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' - } - - _image_prefixes = { - 'mentalray': 'maya///{aov_separator}', # noqa - 'vray': 'maya///', - 'arnold': 'maya///{aov_separator}', # noqa - 'renderman': 'maya///{aov_separator}', - 'redshift': 'maya///{aov_separator}' # noqa - } - - _aov_chars = { - "dot": ".", - "dash": "-", - "underscore": "_" - } - - def __init__(self, project_settings): - self._project_settings = project_settings - - @staticmethod - def apply_defaults(renderer=None, project_settings=None): - if renderer is None: - renderer = cmds.getAttr( - 'defaultRenderGlobals.currentRenderer').lower() - # handle various renderman names - if renderer.startswith('renderman'): - renderer = 'renderman' - - if project_settings is None: - project_settings = get_project_settings(Session["AVALON_PROJECT"]) - - render_settings = RenderSettings(project_settings) - render_settings.set_default_renderer_settings(renderer) - - def set_default_renderer_settings(self, renderer): - """Set basic settings based on renderer. - - Args: - renderer (str): Renderer name. - - """ - # project_settings/maya/create/CreateRender/aov_separator - try: - aov_separator = self._aov_chars[( - self._project_settings["maya"] - ["create"] - ["CreateRender"] - ["aov_separator"] - )] - except KeyError: - aov_separator = "_" - - prefix = self._image_prefixes[renderer] - prefix = prefix.replace("{aov_separator}", aov_separator) - cmds.setAttr(self._image_prefix_nodes[renderer], - prefix, - type="string") - - asset = get_asset() - width = asset["data"].get("resolutionWidth") - height = asset["data"].get("resolutionHeight") - - if renderer == "arnold": - # set format to exr - cmds.setAttr( - "defaultArnoldDriver.ai_translator", "exr", type="string") - self._set_global_output_settings() - - # resolution - cmds.setAttr("defaultResolution.width", width) - cmds.setAttr("defaultResolution.height", height) - - if renderer == "vray": - self._set_vray_settings(aov_separator, width, height) - - if renderer == "redshift": - # set format to exr - cmds.setAttr("RedshiftOptions.imageFormat", 1) - - # resolution - cmds.setAttr("defaultResolution.width", width) - cmds.setAttr("defaultResolution.height", height) - - self._set_global_output_settings() - - def _set_vray_settings(self, aov_separator, width, height): - # type: (str, int, int) -> None - """Sets important settings for Vray.""" - settings = cmds.ls(type="VRaySettingsNode") - node = settings[0] if settings else cmds.createNode("VRaySettingsNode") - - # Set aov separator - # First we need to explicitly set the UI items in Render Settings - # because that is also what V-Ray updates to when that Render Settings - # UI did initialize before and refreshes again. - MENU = "vrayRenderElementSeparator" - if cmds.optionMenuGrp(MENU, query=True, exists=True): - items = cmds.optionMenuGrp(MENU, query=True, ill=True) - separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 - try: - sep_idx = separators.index(aov_separator) - except ValueError: - raise CreatorError( - "AOV character {} not in {}".format( - aov_separator, separators)) - - cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1) - - # Set the render element attribute as string. This is also what V-Ray - # sets whenever the `vrayRenderElementSeparator` menu items switch - cmds.setAttr( - "{}.fileNameRenderElementSeparator".format(node), - aov_separator, - type="string" - ) - - # set format to exr - cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") - - # animType - cmds.setAttr("{}.animType".format(node), 1) - - # resolution - cmds.setAttr("{}.width".format(node), width) - cmds.setAttr("{}.height".format(node), height) - - @staticmethod - def _set_global_output_settings(): - # enable animation - cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) - cmds.setAttr("defaultRenderGlobals.animation", 1) - cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) - cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) +from openpype.api import ( + get_system_settings, + get_project_settings +) +from openpype.hosts.maya.api import ( + lib, + plugin, + render_settings +) +from openpype.modules import ModulesManager class CreateRender(plugin.Creator): @@ -302,7 +159,7 @@ class CreateRender(plugin.Creator): collection.getSelector().setPattern('*') self.log.info("Applying default render settings..") - RenderSettings.apply_defaults() + render_settings.RenderSettings.apply_defaults() return self.instance def _deadline_webservice_changed(self): diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py index 0838b4fbf8..3f08e0cd62 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -1,19 +1,11 @@ import re import pyblish.api -import openpype.api -import openpype.hosts.maya.api.action - from maya import cmds - -ImagePrefixes = { - 'mentalray': 'defaultRenderGlobals.imageFilePrefix', - 'vray': 'vraySettings.fileNamePrefix', - 'arnold': 'defaultRenderGlobals.imageFilePrefix', - 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' -} +import openpype.api +import openpype.hosts.maya.api.action +from openpype.hosts.maya.api.render_settings import RenderSettings class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): @@ -46,7 +38,9 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): # handle various renderman names if renderer.startswith('renderman'): renderer = 'renderman' - file_prefix = cmds.getAttr(ImagePrefixes[renderer]) + + attr = RenderSettings.get_image_prefix_attr(renderer) + file_prefix = cmds.getAttr(attr) if len(cameras) > 1: if re.search(cls.R_CAMERA_TOKEN, file_prefix): From fc0891c7f0617ffde70f78456051e59b2d04e400 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Feb 2022 16:10:21 +0100 Subject: [PATCH 018/432] Cleanup render_settings.py imports + newline end of file --- openpype/hosts/maya/api/render_settings.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 14f6468d1b..48bf7fa56c 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -1,18 +1,8 @@ -import os -import sys - from maya import cmds -import maya.app.renderSetup.model.renderSetup as renderSetup -from openpype.hosts.maya.api import ( - lib, - plugin -) from openpype.api import ( - get_system_settings, get_project_settings, get_asset) -from openpype.modules import ModulesManager from avalon.api import Session from avalon.api import CreatorError @@ -162,4 +152,4 @@ class RenderSettings(object): cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) cmds.setAttr("defaultRenderGlobals.animation", 1) cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) - cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) \ No newline at end of file + cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) From 1177ee2a25403c143aa0d2639102ee1360ac77d2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 11 Apr 2022 15:21:59 +0300 Subject: [PATCH 019/432] Refactor Maya Create Render Schema --- .../schemas/schema_maya_create.json | 39 +- .../schemas/schema_maya_create_render.json | 417 ++++++++++++++++++ 2 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 6dc10ed2a5..4e92875677 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -29,42 +29,9 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "CreateRender", - "label": "Create Render", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "list", - "key": "defaults", - "label": "Default Subsets", - "object_type": "text" - }, - { - "key": "aov_separator", - "label": "AOV Separator character", - "type": "enum", - "multiselection": false, - "default": "underscore", - "enum_items": [ - {"dash": "- (dash)"}, - {"underscore": "_ (underscore)"}, - {"dot": ". (dot)"} - ] - }, - { - "type": "text", - "key": "default_render_image_folder", - "label": "Default render image folder" - } - ] + { + "type": "schema", + "name": "schema_maya_create_render" }, { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json new file mode 100644 index 0000000000..f4a724cd5c --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json @@ -0,0 +1,417 @@ +{ + "type": "dict", + "collapsible": true, + "key": "CreateRender", + "label": "Create Render", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + }, + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" + }, + { + "key": "aov_separator", + "label": "AOV Separator character", + "type": "enum", + "multiselection": false, + "default": "underscore", + "enum_items": [ + {"dash": "- (dash)"}, + {"underscore": "_ (underscore)"}, + {"dot": ". (dot)"} + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "arnold_renderer", + "label": "Arnold Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"jpeg": "jpeg"}, + {"png": "png"}, + {"deepexr": "deep exr"}, + {"tif": "tif"}, + {"exr": "exr"}, + {"maya": "maya"}, + {"mtoa_shaders": "mtoa_shaders"} + ] + }, + { + "key": "multilayer_exr", + "label": "Multilayer (exr)", + "type": "boolean" + }, + { + "key": "tiled", + "label": "Tiled (tif, exr)", + "type": "boolean" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"ID": "ID"}, + {"N": "N"}, + {"P": "P"}, + {"Pref": "Pref"}, + {"RGBA": "RGBA"}, + {"Z": "Z"}, + {"albedo": "albedo"}, + {"background": "background"}, + {"coat": "coat"}, + {"coat_albedo": "coat_albedo"}, + {"coat_direct": "coat_direct"}, + {"coat_indirect": "coat_indirect"}, + {"cputime": "cputime"}, + {"crypto_asset": "crypto_asset"}, + {"crypto_material": "cypto_material"}, + {"crypto_object": "crypto_object"}, + {"diffuse": "diffuse"}, + {"diffuse_albedo": "diffuse_albedo"}, + {"diffuse_direct": "diffuse_direct"}, + {"diffuse_indirect": "diffuse_indirect"}, + {"direct": "direct"}, + {"emission": "emission"}, + {"highlight": "highlight"}, + {"indirect": "indirect"}, + {"motionvector": "motionvector"}, + {"opacity": "opacity"}, + {"raycount": "raycount"}, + {"rim_light": "rim_light"}, + {"shadow": "shadow"}, + {"shadow_diff": "shadow_diff"}, + {"shadow_mask": "shadow_mask"}, + {"shadow_matte": "shadow_matte"}, + {"sheen": "sheen"}, + {"sheen_albedo": "sheen_albedo"}, + {"sheen_direct": "sheen_direct"}, + {"sheen_indirect": "sheen_indirect"}, + {"specular": "specular"}, + {"specular_albedo": "specular_albedo"}, + {"specular_direct": "specular_direct"}, + {"specular_indirect": "specular_indirect"}, + {"sss": "sss"}, + {"sss_albedo": "sss_albedo"}, + {"sss_direct": "sss_direct"}, + {"sss_indirect": "sss_indirect"}, + {"transmission": "transmission"}, + {"transmission_albedo": "transmission_albedo"}, + {"transmission_direct": "transmission_direct"}, + {"transmission_indirect": "transmission_indirect"}, + {"volume": "volume"}, + {"volume_Z": "volume_Z"}, + {"volume_albedo": "volume_albedo"}, + {"volume_direct": "volume_direct"}, + {"volume_indirect": "volume_indirect"}, + {"volume_opacity": "volume_opacity"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like AASamples" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "vray_renderer", + "label": "V-Ray Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "engine", + "label": "Production Engine", + "type": "enum", + "multiselection": false, + "defaults": "1", + "enum_items": [ + {"1": "V-Ray"}, + {"2": "V-Ray GPU"} + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"png": "png"}, + {"jpg": "jpg"}, + {"vrimg": "vrimg"}, + {"hdr": "hdr"}, + {"exr": "exr"}, + {"exr (multichannel)": "exr (multichannel)"}, + {"exr (deep)": "exr (deep)"}, + {"tga": "tga"}, + {"bmp": "bmp"}, + {"sgi": "sgi"} + ] + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"atmosphereChannel": "atmosphere"}, + {"backgroundChannel": "background"}, + {"bumpNormalsChannel": "bumpnormals"}, + {"causticsChannel": "caustics"}, + {"coatFilterChannel": "coat_filter"}, + {"coatGlossinessChannel": "coatGloss"}, + {"coatReflectionChannel": "coat_reflection"}, + {"vrayCoatChannel": "coat_specular"}, + {"CoverageChannel": "coverage"}, + {"cryptomatteChannel": "cryptomatte"}, + {"customColor": "custom_color"}, + {"drBucketChannel": "DR"}, + {"denoiserChannel": "denoiser"}, + {"diffuseChannel": "diffuse"}, + {"ExtraTexElement": "extraTex"}, + {"giChannel": "GI"}, + {"LightMixElement": "None"}, + {"lightingChannel": "lighting"}, + {"LightingAnalysisChannel": "LightingAnalysis"}, + {"materialIDChannel": "materialID"}, + {"MaterialSelectElement": "materialSelect"}, + {"matteShadowChannel": "matteShadow"}, + {"MultiMatteElement": "multimatte"}, + {"multimatteIDChannel": "multimatteID"}, + {"normalsChannel": "normals"}, + {"nodeIDChannel": "objectId"}, + {"objectSelectChannel": "objectSelect"}, + {"rawCoatFilterChannel": "raw_coat_filter"}, + {"rawCoatReflectionChannel": "raw_coat_reflection"}, + {"rawDiffuseFilterChannel": "rawDiffuseFilter"}, + {"rawGiChannel": "rawGI"}, + {"rawLightChannel": "rawLight"}, + {"rawReflectionChannel": "rawReflection"}, + {"rawReflectionFilterChannel": "rawReflectionFilter"}, + {"rawRefractionChannel": "rawRefraction"}, + {"rawRefractionFilterChannel": "rawRefractionFilter"}, + {"rawShadowChannel": "rawShadow"}, + {"rawSheenFilterChannel": "raw_sheen_filter"}, + {"rawSheenReflectionChannel": "raw_sheen_reflection"}, + {"rawTotalLightChannel": "rawTotalLight"}, + {"reflectIORChannel": "reflIOR"}, + {"reflectChannel": "reflect"}, + {"reflectionFilterChannel": "reflectionFilter"}, + {"reflectGlossinessChannel": "reflGloss"}, + {"refractChannel": "refract"}, + {"refractionFilterChannel": "refractionFilter"}, + {"refractGlossinessChannel": "refrGloss"}, + {"renderIDChannel": "renderId"}, + {"FastSSS2Channel": "SSS"}, + {"sampleRateChannel": "sampleRate"}, + {"samplerInfo": "samplerInfo"}, + {"selfIllumChannel": "selfIllum"}, + {"shadowChannel": "shadow"}, + {"sheenFilterChannel": "sheen_filter"}, + {"sheenGlossinessChannel": "sheenGloss"}, + {"sheenReflectionChannel": "sheen_reflection"}, + {"vraySheenChannel": "sheen_specular"}, + {"specularChannel": "specular"}, + {"Toon": "Toon"}, + {"toonLightingChannel": "toonLighting"}, + {"toonSpecularChannel": "toonSpecular"}, + {"totalLightChannel": "totalLight"}, + {"unclampedColorChannel": "unclampedColor"}, + {"VRScansPaintMaskChannel": "VRScansPaintMask"}, + {"VRScansZoneMaskChannel": "VRScansZoneMask"}, + {"velocityChannel": "velocity"}, + {"zdepthChannel": "zDepth"}, + {"LightSelectElement": "lightselect"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like aaFilterSize" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "redshift_renderer", + "label": "Redshift Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "primary_gi_engine", + "label": "Primary GI Engine", + "type": "enum", + "multiselection": false, + "defaults": "0", + "enum_items": [ + {"0": "None"}, + {"1": "Photon Map"}, + {"2": "Irradiance Cache"}, + {"3": "Brute Force"} + ] + }, + { + "key": "secondary_gi_engine", + "label": "Secondary GI Engine", + "type": "enum", + "multiselection": false, + "defaults": "0", + "enum_items": [ + {"0": "None"}, + {"1": "Photon Map"}, + {"2": "Irradiance Cache"}, + {"3": "Brute Force"} + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"iff": "Maya IFF"}, + {"exr": "OpenEXR"}, + {"tif": "TIFF"}, + {"png": "PNG"}, + {"tga": "Targa"}, + {"jpg": "JPEG"} + ] + }, + { + "key": "multilayer_exr", + "label": "Multilayer (exr)", + "type": "boolean" + }, + { + "key": "force_combine", + "label": "Force combine beauty and AOVs", + "type": "boolean" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< none >"}, + {"AO": "Ambient Occlusion"}, + {"Background": "Background"}, + {"Beauty": "Beauty"}, + {"BumpNormals": "Bump Normals"}, + {"Caustics": "Caustics"}, + {"CausticsRaw": "Caustics Raw"}, + {"Cryptomatte": "Cryptomatte"}, + {"Custom": "Custom"}, + {"Z": "Depth"}, + {"DiffuseFilter": "Diffuse Filter"}, + {"DiffuseLighting": "Diffuse Lighting"}, + {"DiffuseLightingRaw": "Diffuse Lighting Raw"}, + {"Emission": "Emission"}, + {"GI": "Global Illumination"}, + {"GIRaw": "Global Illumination Raw"}, + {"Matte": "Matte"}, + {"MotionVectors": "Ambient Occlusion"}, + {"N": "Normals"}, + {"ID": "ObjectID"}, + {"ObjectBumpNormal": "Object-Space Bump Normals"}, + {"ObjectPosition": "Object-Space Positions"}, + {"PuzzleMatte": "Puzzle Matte"}, + {"Reflections": "Reflections"}, + {"ReflectionsFilter": "Reflections Filter"}, + {"ReflectionsRaw": "Reflections Raw"}, + {"Refractions": "Refractions"}, + {"RefractionsFilter": "Refractions Filter"}, + {"RefractionsRaw": "Refractions Filter"}, + {"Shadows": "Shadows"}, + {"SpecularLighting": "Specular Lighting"}, + {"SSS": "Sub Surface Scatter"}, + {"SSSRaw": "Sub Surface Scatter Raw"}, + {"TotalDiffuseLightingRaw": "Total Diffuse Lighting Raw"}, + {"TotalTransLightingRaw": "Total Translucency Filter"}, + {"TransTint": "Translucency Filter"}, + {"TransGIRaw": "Translucency Lighting Raw"}, + {"VolumeFogEmission": "Volume Fog Emission"}, + {"VolumeFogTint": "Volume Fog Tint"}, + {"VolumeLighting": "Volume Lighting"}, + {"P": "World Position"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like reflectionMaxTraceDepth" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + } + ] +} \ No newline at end of file From 8c4d44fd46ab182ad442e47f3cfe8f7ba9a76f47 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 11 Apr 2022 15:22:17 +0300 Subject: [PATCH 020/432] add renderer settings --- .../defaults/project_settings/maya.json | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4cdfe1ca5d..c0b85eb0eb 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -43,8 +43,39 @@ "defaults": [ "Main" ], + "default_render_image_folder": "renders", "aov_separator": "underscore", - "default_render_image_folder": "renders" + "arnold_renderer": { + "image_prefix": "maya///{aov_separator}", + "image_format": "exr", + "multilayer_exr": false, + "tiled": true, + "aov_list": [ + "empty" + ], + "additional_options": {} + }, + "vray_renderer": { + "image_prefix": "maya///", + "engine": "1", + "image_format": "exr", + "aov_list": [ + "empty" + ], + "additional_options": {} + }, + "redshift_renderer": { + "image_prefix": "'maya///{aov_separator}", + "primary_gi_engine": "0", + "secondary_gi_engine": "0", + "image_format": "exr", + "multilayer_exr": false, + "force_combine": false, + "aov_list": [ + "empty" + ], + "additional_options": {} + } }, "CreateUnrealStaticMesh": { "enabled": true, From b64b0a66b06ea6fc8dfaeedc4a7c3bef1f53a609 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 20:28:56 +0300 Subject: [PATCH 021/432] add function to grab Arnold settings --- .../maya/plugins/create/create_render.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 4f0a394f85..84ac8f36ec 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -431,6 +431,20 @@ class CreateRender(plugin.Creator): kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) return requests.get(*args, **kwargs) + def _set_Arnold_settings(self): + """Sets settings for Arnold.""" + + img_ext = self.arnold_renderer.get("image_format") + self._set_global_output_settings() + # Resolution + resWidth = self.attributes.get("resolutionWidth") + resHeight = self.attributes.get("resolutionHeight") + + cmds.setAttr("defaultArnoldDriver.ai_translator", + img_ext, type="string") + cmds.setAttr("defaultResolution.width", resWidth) + cmds.setAttr("defaultResolution.height", resHeight) + def _set_default_renderer_settings(self, renderer): """Set basic settings based on renderer. @@ -448,18 +462,7 @@ class CreateRender(plugin.Creator): if renderer == "arnold": # set format to exr - - cmds.setAttr( - "defaultArnoldDriver.ai_translator", "exr", type="string") - self._set_global_output_settings() - # resolution - cmds.setAttr( - "defaultResolution.width", - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "defaultResolution.height", - asset["data"].get("resolutionHeight")) - + self._set_Arnold_settings() if renderer == "vray": self._set_vray_settings(asset) if renderer == "redshift": From 5d56323050e48951930c0439b88d646b94d98872 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 22:50:34 +0300 Subject: [PATCH 022/432] add redshift settings function --- .../hosts/maya/plugins/create/create_render.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 84ac8f36ec..f0317ccb9e 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -466,6 +466,7 @@ class CreateRender(plugin.Creator): if renderer == "vray": self._set_vray_settings(asset) if renderer == "redshift": + self._set_redshift_settings() cmds.setAttr("redshiftOptions.imageFormat", 1) # resolution @@ -478,6 +479,19 @@ class CreateRender(plugin.Creator): self._set_global_output_settings() + def _set_redshift_settings(self): + """Sets settings for Arnold.""" + + img_ext = self.redshift_renderer.get("image_format") + self._set_global_output_settings() + # Resolution + resWidth = self.attributes.get("resolutionWidth") + resHeight = self.attributes.get("resolutionHeight") + + cmds.setAttr("redshiftOptions.imageFormat", img_ext) + cmds.setAttr("defaultResolution.width", resWidth) + cmds.setAttr("defaultResolution.height", resHeight) + def _set_vray_settings(self, asset): # type: (dict) -> None """Sets important settings for Vray.""" From b62fa7451b4a69460e7a2e26c4a3d0e25ca23353 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 23:04:08 +0300 Subject: [PATCH 023/432] replace redshift settings setters with method --- openpype/hosts/maya/plugins/create/create_render.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index f0317ccb9e..757cc16fda 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -467,17 +467,6 @@ class CreateRender(plugin.Creator): self._set_vray_settings(asset) if renderer == "redshift": self._set_redshift_settings() - cmds.setAttr("redshiftOptions.imageFormat", 1) - - # resolution - cmds.setAttr( - "defaultResolution.width", - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "defaultResolution.height", - asset["data"].get("resolutionHeight")) - - self._set_global_output_settings() def _set_redshift_settings(self): """Sets settings for Arnold.""" From 7ea7a0f5f5827ab20160a8ad635c5613179a4352 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 20 Apr 2022 23:40:42 +0300 Subject: [PATCH 024/432] remove extra code in render creator --- openpype/hosts/maya/api/render_settings.py | 24 ++++++++++++------- .../maya/plugins/create/create_render.py | 4 +--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 48bf7fa56c..2614ca23e2 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -54,6 +54,20 @@ class RenderSettings(object): render_settings = RenderSettings(project_settings) render_settings.set_default_renderer_settings(renderer) + def _set_Arnold_settings(self): + """Sets settings for Arnold.""" + + img_ext = self.arnold_renderer.get("image_format") + self._set_global_output_settings() + # Resolution + resWidth = self.attributes.get("resolutionWidth") + resHeight = self.attributes.get("resolutionHeight") + + cmds.setAttr("defaultArnoldDriver.ai_translator", + img_ext, type="string") + cmds.setAttr("defaultResolution.width", resWidth) + cmds.setAttr("defaultResolution.height", resHeight) + def set_default_renderer_settings(self, renderer): """Set basic settings based on renderer. @@ -83,14 +97,8 @@ class RenderSettings(object): height = asset["data"].get("resolutionHeight") if renderer == "arnold": - # set format to exr - cmds.setAttr( - "defaultArnoldDriver.ai_translator", "exr", type="string") - self._set_global_output_settings() - - # resolution - cmds.setAttr("defaultResolution.width", width) - cmds.setAttr("defaultResolution.height", height) + # set renderer settings for Arnold from project settings + self._set_Arnold_settings() if renderer == "vray": self._set_vray_settings(aov_separator, width, height) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 5431cfea57..0ef9665fdf 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -421,9 +421,7 @@ class CreateRender(plugin.Creator): asset = get_asset() - if renderer == "arnold": - # set format to exr - self._set_Arnold_settings() + if renderer == "vray": self._set_vray_settings(asset) if renderer == "redshift": From 854ec3b762f61d71be5e9358fefb67945057f1dd Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 20 Apr 2022 23:44:28 +0300 Subject: [PATCH 025/432] add missing get_asset() --- openpype/hosts/maya/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 0ef9665fdf..313fb68fa7 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -14,7 +14,8 @@ from maya.app.renderSetup.model import renderSetup from avalon.api import Session from openpype.api import ( get_system_settings, - get_project_settings + get_project_settings, + get_asset, ) from openpype.hosts.maya.api import ( lib, @@ -421,7 +422,6 @@ class CreateRender(plugin.Creator): asset = get_asset() - if renderer == "vray": self._set_vray_settings(asset) if renderer == "redshift": From 3f488594bea9ecd1feda46987bea09195835a40c Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 00:50:41 +0300 Subject: [PATCH 026/432] remove unused function --- .../hosts/maya/plugins/create/create_render.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 313fb68fa7..e95e39e975 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -393,20 +393,6 @@ class CreateRender(plugin.Creator): kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) return requests.get(*args, **kwargs) - def _set_Arnold_settings(self): - """Sets settings for Arnold.""" - - img_ext = self.arnold_renderer.get("image_format") - self._set_global_output_settings() - # Resolution - resWidth = self.attributes.get("resolutionWidth") - resHeight = self.attributes.get("resolutionHeight") - - cmds.setAttr("defaultArnoldDriver.ai_translator", - img_ext, type="string") - cmds.setAttr("defaultResolution.width", resWidth) - cmds.setAttr("defaultResolution.height", resHeight) - def _set_default_renderer_settings(self, renderer): """Set basic settings based on renderer. From 0bc8ad9694c32eb096b67e1fc2dce335b53b9d1c Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 00:52:54 +0300 Subject: [PATCH 027/432] change placement of redshift settings function --- openpype/hosts/maya/api/render_settings.py | 13 +++++++++++++ openpype/hosts/maya/plugins/create/create_render.py | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 2614ca23e2..6c741046ed 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -113,6 +113,19 @@ class RenderSettings(object): self._set_global_output_settings() + def _set_redshift_settings(self): + """Sets settings for Arnold.""" + + img_ext = self.redshift_renderer.get("image_format") + self._set_global_output_settings() + # Resolution + resWidth = self.attributes.get("resolutionWidth") + resHeight = self.attributes.get("resolutionHeight") + + cmds.setAttr("redshiftOptions.imageFormat", img_ext) + cmds.setAttr("defaultResolution.width", resWidth) + cmds.setAttr("defaultResolution.height", resHeight) + def _set_vray_settings(self, aov_separator, width, height): # type: (str, int, int) -> None """Sets important settings for Vray.""" diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index e95e39e975..3ab83c5143 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -413,19 +413,6 @@ class CreateRender(plugin.Creator): if renderer == "redshift": self._set_redshift_settings() - def _set_redshift_settings(self): - """Sets settings for Arnold.""" - - img_ext = self.redshift_renderer.get("image_format") - self._set_global_output_settings() - # Resolution - resWidth = self.attributes.get("resolutionWidth") - resHeight = self.attributes.get("resolutionHeight") - - cmds.setAttr("redshiftOptions.imageFormat", img_ext) - cmds.setAttr("defaultResolution.width", resWidth) - cmds.setAttr("defaultResolution.height", resHeight) - def _set_vray_settings(self, asset): # type: (dict) -> None """Sets important settings for Vray.""" From 71434dee8b500bdabc7073535943b9fbdf047558 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 10:48:28 +0300 Subject: [PATCH 028/432] remove unused refactored function --- openpype/hosts/maya/plugins/create/create_render.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 3ab83c5143..93f305f3b9 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -456,11 +456,3 @@ class CreateRender(plugin.Creator): cmds.setAttr( "{}.height".format(node), asset["data"].get("resolutionHeight")) - - @staticmethod - def _set_global_output_settings(): - # enable animation - cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) - cmds.setAttr("defaultRenderGlobals.animation", 1) - cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) - cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) From 50e60acc22e297238be281984bbf871ceadf9ddd Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 10:57:30 +0300 Subject: [PATCH 029/432] removed unused refactored vray settings func --- .../maya/plugins/create/create_render.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 93f305f3b9..815b2a6b0f 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -412,47 +412,3 @@ class CreateRender(plugin.Creator): self._set_vray_settings(asset) if renderer == "redshift": self._set_redshift_settings() - - def _set_vray_settings(self, asset): - # type: (dict) -> None - """Sets important settings for Vray.""" - settings = cmds.ls(type="VRaySettingsNode") - node = settings[0] if settings else cmds.createNode("VRaySettingsNode") - - # set separator - # set it in vray menu - if cmds.optionMenuGrp("vrayRenderElementSeparator", exists=True, - q=True): - items = cmds.optionMenuGrp( - "vrayRenderElementSeparator", ill=True, query=True) - - separators = [cmds.menuItem(i, label=True, query=True) for i in items] # noqa: E501 - try: - sep_idx = separators.index(self.aov_separator) - except ValueError: - raise CreatorError( - "AOV character {} not in {}".format( - self.aov_separator, separators)) - - cmds.optionMenuGrp( - "vrayRenderElementSeparator", sl=sep_idx + 1, edit=True) - cmds.setAttr( - "{}.fileNameRenderElementSeparator".format(node), - self.aov_separator, - type="string" - ) - # set format to exr - cmds.setAttr( - "{}.imageFormatStr".format(node), "exr", type="string") - - # animType - cmds.setAttr( - "{}.animType".format(node), 1) - - # resolution - cmds.setAttr( - "{}.width".format(node), - asset["data"].get("resolutionWidth")) - cmds.setAttr( - "{}.height".format(node), - asset["data"].get("resolutionHeight")) From 6f58d72be5e5f23ecdc151eed1d135edd750b5e0 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 11:18:40 +0300 Subject: [PATCH 030/432] add/cleanup refactored redshift settings function --- openpype/hosts/maya/api/render_settings.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 6c741046ed..75646b858b 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -104,14 +104,7 @@ class RenderSettings(object): self._set_vray_settings(aov_separator, width, height) if renderer == "redshift": - # set format to exr - cmds.setAttr("RedshiftOptions.imageFormat", 1) - - # resolution - cmds.setAttr("defaultResolution.width", width) - cmds.setAttr("defaultResolution.height", height) - - self._set_global_output_settings() + self._set_redshift_settings() def _set_redshift_settings(self): """Sets settings for Arnold.""" From 105fb3e377e207f32f385fee4e03bcc363ff0b4a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 11:19:07 +0300 Subject: [PATCH 031/432] remove refactored default renderer settings func --- .../maya/plugins/create/create_render.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 815b2a6b0f..97f059077f 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -392,23 +392,3 @@ class CreateRender(plugin.Creator): if "verify" not in kwargs: kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) return requests.get(*args, **kwargs) - - def _set_default_renderer_settings(self, renderer): - """Set basic settings based on renderer. - - Args: - renderer (str): Renderer name. - - """ - prefix = self._image_prefixes[renderer] - prefix = prefix.replace("{aov_separator}", self.aov_separator) - cmds.setAttr(self._image_prefix_nodes[renderer], - prefix, - type="string") - - asset = get_asset() - - if renderer == "vray": - self._set_vray_settings(asset) - if renderer == "redshift": - self._set_redshift_settings() From 2cd42298e9b51d8323c1b4a023d74115ebf62bc0 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 11:27:57 +0300 Subject: [PATCH 032/432] remove unused import --- openpype/hosts/maya/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 97f059077f..f6e75c825c 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -15,7 +15,6 @@ from avalon.api import Session from openpype.api import ( get_system_settings, get_project_settings, - get_asset, ) from openpype.hosts.maya.api import ( lib, From 54ff5a8e53449bf4ae895e4ddb142dbe25b0fe95 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 20:17:40 +0300 Subject: [PATCH 033/432] remove extra import --- openpype/hosts/maya/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index f6e75c825c..86276c3f77 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -24,7 +24,6 @@ from openpype.hosts.maya.api import ( from openpype.modules import ModulesManager from openpype.pipeline import CreatorError -from avalon.api import Session class CreateRender(plugin.Creator): """Create *render* instance. From 2867a2f2a0da67112c5a893eddb131fcc3ee6832 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 20:18:10 +0300 Subject: [PATCH 034/432] fix redshift comment --- openpype/hosts/maya/api/render_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 75646b858b..1a2064986e 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -107,7 +107,7 @@ class RenderSettings(object): self._set_redshift_settings() def _set_redshift_settings(self): - """Sets settings for Arnold.""" + """Sets settings for Redshift.""" img_ext = self.redshift_renderer.get("image_format") self._set_global_output_settings() From 675c7a000601257fbfad78eb2339377e767d226d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 22:30:07 +0300 Subject: [PATCH 035/432] replace avalon CreatorError with OP's impl. --- openpype/hosts/maya/api/render_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/render_settings.py index 1a2064986e..48026e1510 100644 --- a/openpype/hosts/maya/api/render_settings.py +++ b/openpype/hosts/maya/api/render_settings.py @@ -5,7 +5,7 @@ from openpype.api import ( get_asset) from avalon.api import Session -from avalon.api import CreatorError +from openpype.pipeline import CreatorError class RenderSettings(object): From 74cc8230ea01c27277f5e61414fd3d7b3ec3f81d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 22:31:43 +0300 Subject: [PATCH 036/432] remove unused import --- openpype/hosts/maya/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 86276c3f77..bdd1844b5e 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -22,7 +22,6 @@ from openpype.hosts.maya.api import ( render_settings ) from openpype.modules import ModulesManager -from openpype.pipeline import CreatorError class CreateRender(plugin.Creator): From 4d4ca196f7808a5007893136dcf5d4c82d84cf21 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 23:44:08 +0300 Subject: [PATCH 037/432] remove redundant code --- openpype/hosts/maya/plugins/publish/collect_render.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 839ead8bd6..ab7b7a78ac 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -325,10 +325,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if instance.data['family'] == "workfile": instance.data["version"] = context.data["version"] - # Apply each user defined attribute as data - for attr, value in avalon.maya.read(layer).items(): - data[attr] = value - # handle standalone renderers if render_instance.data.get("vrayScene") is True: data["families"].append("vrayscene_render") From 9faa7e0b618f06af28081a521fe4cec4753092d5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:03:37 +0300 Subject: [PATCH 038/432] Rename file to match convention. --- .../hosts/maya/api/{render_settings.py => lib_rendersettings.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/maya/api/{render_settings.py => lib_rendersettings.py} (100%) diff --git a/openpype/hosts/maya/api/render_settings.py b/openpype/hosts/maya/api/lib_rendersettings.py similarity index 100% rename from openpype/hosts/maya/api/render_settings.py rename to openpype/hosts/maya/api/lib_rendersettings.py From 24a1dea3eb8688feec1ae5379d9708fe2f94d95f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:04:19 +0300 Subject: [PATCH 039/432] Append render settings schema. --- .../projects_schema/schema_project_maya.json | 4 + .../schemas/schema_maya_render_settings.json | 411 ++++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index cc70516c72..76a235bc12 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -51,6 +51,10 @@ "type": "schema", "name": "schema_maya_scriptsmenu" }, + { + "type": "schema", + "name": "schema_maya_render_settings" + }, { "type": "schema", "name": "schema_maya_create" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json new file mode 100644 index 0000000000..62e9c9e461 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -0,0 +1,411 @@ +{ + "type": "dict", + "collapsible": true, + "key": "RenderSettings", + "label": "Render Settings", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" + }, + { + "key": "aov_separator", + "label": "AOV Separator character", + "type": "enum", + "multiselection": false, + "default": "underscore", + "enum_items": [ + {"dash": "- (dash)"}, + {"underscore": "_ (underscore)"}, + {"dot": ". (dot)"} + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "arnold_renderer", + "label": "Arnold Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"jpeg": "jpeg"}, + {"png": "png"}, + {"deepexr": "deep exr"}, + {"tif": "tif"}, + {"exr": "exr"}, + {"maya": "maya"}, + {"mtoa_shaders": "mtoa_shaders"} + ] + }, + { + "key": "multilayer_exr", + "label": "Multilayer (exr)", + "type": "boolean" + }, + { + "key": "tiled", + "label": "Tiled (tif, exr)", + "type": "boolean" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"ID": "ID"}, + {"N": "N"}, + {"P": "P"}, + {"Pref": "Pref"}, + {"RGBA": "RGBA"}, + {"Z": "Z"}, + {"albedo": "albedo"}, + {"background": "background"}, + {"coat": "coat"}, + {"coat_albedo": "coat_albedo"}, + {"coat_direct": "coat_direct"}, + {"coat_indirect": "coat_indirect"}, + {"cputime": "cputime"}, + {"crypto_asset": "crypto_asset"}, + {"crypto_material": "cypto_material"}, + {"crypto_object": "crypto_object"}, + {"diffuse": "diffuse"}, + {"diffuse_albedo": "diffuse_albedo"}, + {"diffuse_direct": "diffuse_direct"}, + {"diffuse_indirect": "diffuse_indirect"}, + {"direct": "direct"}, + {"emission": "emission"}, + {"highlight": "highlight"}, + {"indirect": "indirect"}, + {"motionvector": "motionvector"}, + {"opacity": "opacity"}, + {"raycount": "raycount"}, + {"rim_light": "rim_light"}, + {"shadow": "shadow"}, + {"shadow_diff": "shadow_diff"}, + {"shadow_mask": "shadow_mask"}, + {"shadow_matte": "shadow_matte"}, + {"sheen": "sheen"}, + {"sheen_albedo": "sheen_albedo"}, + {"sheen_direct": "sheen_direct"}, + {"sheen_indirect": "sheen_indirect"}, + {"specular": "specular"}, + {"specular_albedo": "specular_albedo"}, + {"specular_direct": "specular_direct"}, + {"specular_indirect": "specular_indirect"}, + {"sss": "sss"}, + {"sss_albedo": "sss_albedo"}, + {"sss_direct": "sss_direct"}, + {"sss_indirect": "sss_indirect"}, + {"transmission": "transmission"}, + {"transmission_albedo": "transmission_albedo"}, + {"transmission_direct": "transmission_direct"}, + {"transmission_indirect": "transmission_indirect"}, + {"volume": "volume"}, + {"volume_Z": "volume_Z"}, + {"volume_albedo": "volume_albedo"}, + {"volume_direct": "volume_direct"}, + {"volume_indirect": "volume_indirect"}, + {"volume_opacity": "volume_opacity"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like AASamples" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "vray_renderer", + "label": "V-Ray Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "engine", + "label": "Production Engine", + "type": "enum", + "multiselection": false, + "defaults": "1", + "enum_items": [ + {"1": "V-Ray"}, + {"2": "V-Ray GPU"} + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"png": "png"}, + {"jpg": "jpg"}, + {"vrimg": "vrimg"}, + {"hdr": "hdr"}, + {"exr": "exr"}, + {"exr (multichannel)": "exr (multichannel)"}, + {"exr (deep)": "exr (deep)"}, + {"tga": "tga"}, + {"bmp": "bmp"}, + {"sgi": "sgi"} + ] + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"atmosphereChannel": "atmosphere"}, + {"backgroundChannel": "background"}, + {"bumpNormalsChannel": "bumpnormals"}, + {"causticsChannel": "caustics"}, + {"coatFilterChannel": "coat_filter"}, + {"coatGlossinessChannel": "coatGloss"}, + {"coatReflectionChannel": "coat_reflection"}, + {"vrayCoatChannel": "coat_specular"}, + {"CoverageChannel": "coverage"}, + {"cryptomatteChannel": "cryptomatte"}, + {"customColor": "custom_color"}, + {"drBucketChannel": "DR"}, + {"denoiserChannel": "denoiser"}, + {"diffuseChannel": "diffuse"}, + {"ExtraTexElement": "extraTex"}, + {"giChannel": "GI"}, + {"LightMixElement": "None"}, + {"lightingChannel": "lighting"}, + {"LightingAnalysisChannel": "LightingAnalysis"}, + {"materialIDChannel": "materialID"}, + {"MaterialSelectElement": "materialSelect"}, + {"matteShadowChannel": "matteShadow"}, + {"MultiMatteElement": "multimatte"}, + {"multimatteIDChannel": "multimatteID"}, + {"normalsChannel": "normals"}, + {"nodeIDChannel": "objectId"}, + {"objectSelectChannel": "objectSelect"}, + {"rawCoatFilterChannel": "raw_coat_filter"}, + {"rawCoatReflectionChannel": "raw_coat_reflection"}, + {"rawDiffuseFilterChannel": "rawDiffuseFilter"}, + {"rawGiChannel": "rawGI"}, + {"rawLightChannel": "rawLight"}, + {"rawReflectionChannel": "rawReflection"}, + {"rawReflectionFilterChannel": "rawReflectionFilter"}, + {"rawRefractionChannel": "rawRefraction"}, + {"rawRefractionFilterChannel": "rawRefractionFilter"}, + {"rawShadowChannel": "rawShadow"}, + {"rawSheenFilterChannel": "raw_sheen_filter"}, + {"rawSheenReflectionChannel": "raw_sheen_reflection"}, + {"rawTotalLightChannel": "rawTotalLight"}, + {"reflectIORChannel": "reflIOR"}, + {"reflectChannel": "reflect"}, + {"reflectionFilterChannel": "reflectionFilter"}, + {"reflectGlossinessChannel": "reflGloss"}, + {"refractChannel": "refract"}, + {"refractionFilterChannel": "refractionFilter"}, + {"refractGlossinessChannel": "refrGloss"}, + {"renderIDChannel": "renderId"}, + {"FastSSS2Channel": "SSS"}, + {"sampleRateChannel": "sampleRate"}, + {"samplerInfo": "samplerInfo"}, + {"selfIllumChannel": "selfIllum"}, + {"shadowChannel": "shadow"}, + {"sheenFilterChannel": "sheen_filter"}, + {"sheenGlossinessChannel": "sheenGloss"}, + {"sheenReflectionChannel": "sheen_reflection"}, + {"vraySheenChannel": "sheen_specular"}, + {"specularChannel": "specular"}, + {"Toon": "Toon"}, + {"toonLightingChannel": "toonLighting"}, + {"toonSpecularChannel": "toonSpecular"}, + {"totalLightChannel": "totalLight"}, + {"unclampedColorChannel": "unclampedColor"}, + {"VRScansPaintMaskChannel": "VRScansPaintMask"}, + {"VRScansZoneMaskChannel": "VRScansZoneMask"}, + {"velocityChannel": "velocity"}, + {"zdepthChannel": "zDepth"}, + {"LightSelectElement": "lightselect"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like aaFilterSize" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "redshift_renderer", + "label": "Redshift Renderer", + "is_group": true, + "children": [ + { + "key": "image_prefix", + "label": "Image prefix template", + "type": "text" + }, + { + "key": "primary_gi_engine", + "label": "Primary GI Engine", + "type": "enum", + "multiselection": false, + "defaults": "0", + "enum_items": [ + {"0": "None"}, + {"1": "Photon Map"}, + {"2": "Irradiance Cache"}, + {"3": "Brute Force"} + ] + }, + { + "key": "secondary_gi_engine", + "label": "Secondary GI Engine", + "type": "enum", + "multiselection": false, + "defaults": "0", + "enum_items": [ + {"0": "None"}, + {"1": "Photon Map"}, + {"2": "Irradiance Cache"}, + {"3": "Brute Force"} + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"iff": "Maya IFF"}, + {"exr": "OpenEXR"}, + {"tif": "TIFF"}, + {"png": "PNG"}, + {"tga": "Targa"}, + {"jpg": "JPEG"} + ] + }, + { + "key": "multilayer_exr", + "label": "Multilayer (exr)", + "type": "boolean" + }, + { + "key": "force_combine", + "label": "Force combine beauty and AOVs", + "type": "boolean" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< none >"}, + {"AO": "Ambient Occlusion"}, + {"Background": "Background"}, + {"Beauty": "Beauty"}, + {"BumpNormals": "Bump Normals"}, + {"Caustics": "Caustics"}, + {"CausticsRaw": "Caustics Raw"}, + {"Cryptomatte": "Cryptomatte"}, + {"Custom": "Custom"}, + {"Z": "Depth"}, + {"DiffuseFilter": "Diffuse Filter"}, + {"DiffuseLighting": "Diffuse Lighting"}, + {"DiffuseLightingRaw": "Diffuse Lighting Raw"}, + {"Emission": "Emission"}, + {"GI": "Global Illumination"}, + {"GIRaw": "Global Illumination Raw"}, + {"Matte": "Matte"}, + {"MotionVectors": "Ambient Occlusion"}, + {"N": "Normals"}, + {"ID": "ObjectID"}, + {"ObjectBumpNormal": "Object-Space Bump Normals"}, + {"ObjectPosition": "Object-Space Positions"}, + {"PuzzleMatte": "Puzzle Matte"}, + {"Reflections": "Reflections"}, + {"ReflectionsFilter": "Reflections Filter"}, + {"ReflectionsRaw": "Reflections Raw"}, + {"Refractions": "Refractions"}, + {"RefractionsFilter": "Refractions Filter"}, + {"RefractionsRaw": "Refractions Filter"}, + {"Shadows": "Shadows"}, + {"SpecularLighting": "Specular Lighting"}, + {"SSS": "Sub Surface Scatter"}, + {"SSSRaw": "Sub Surface Scatter Raw"}, + {"TotalDiffuseLightingRaw": "Total Diffuse Lighting Raw"}, + {"TotalTransLightingRaw": "Total Translucency Filter"}, + {"TransTint": "Translucency Filter"}, + {"TransGIRaw": "Translucency Lighting Raw"}, + {"VolumeFogEmission": "Volume Fog Emission"}, + {"VolumeFogTint": "Volume Fog Tint"}, + {"VolumeLighting": "Volume Lighting"}, + {"P": "World Position"} + ] + }, + { + "type": "label", + "label": "Add additional options - put attribute and value, like reflectionMaxTraceDepth" + }, + { + "type": "dict-modifiable", + "key": "additional_options", + "label": "Additional Renderer Options", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] + } + ] +} \ No newline at end of file From c853e8440f81123b089b8f329ce6b179a703d8ac Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:11:23 +0300 Subject: [PATCH 040/432] Add comment about pools --- openpype/hosts/maya/plugins/create/create_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index bdd1844b5e..b718bbfa9c 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -18,8 +18,8 @@ from openpype.api import ( ) from openpype.hosts.maya.api import ( lib, - plugin, - render_settings + lib_rendersettings, + plugin ) from openpype.modules import ModulesManager @@ -158,7 +158,7 @@ class CreateRender(plugin.Creator): collection.getSelector().setPattern('*') self.log.info("Applying default render settings..") - render_settings.RenderSettings.apply_defaults() + lib_rendersettings.RenderSettings.apply_defaults() return self.instance def _deadline_webservice_changed(self): @@ -209,7 +209,7 @@ class CreateRender(plugin.Creator): def _create_render_settings(self): """Create instance settings.""" - # get pools + # get pools (slave machines of the render farm) pool_names = [] default_priority = 50 From b5004aeaa5696a14d745d8e93c1113de2bc3b8cc Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:13:23 +0300 Subject: [PATCH 041/432] Add comment about pool_names source --- openpype/hosts/maya/plugins/create/create_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index b718bbfa9c..e431eb2bf1 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -253,7 +253,8 @@ class CreateRender(plugin.Creator): # if 'default' server is not between selected, # use first one for initial list of pools. deadline_url = next(iter(self.deadline_servers.values())) - + # Uses function to get pool machines from the assigned deadline + # url in settings pool_names = self._get_deadline_pools(deadline_url) maya_submit_dl = self._project_settings.get( "deadline", {}).get( From 05ed9c5c5396dca022785215c73db3ff8ef6452e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:19:39 +0300 Subject: [PATCH 042/432] Redshift function cleanup. --- openpype/hosts/maya/api/lib_rendersettings.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 48026e1510..887cbc775e 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -106,18 +106,14 @@ class RenderSettings(object): if renderer == "redshift": self._set_redshift_settings() - def _set_redshift_settings(self): + def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" img_ext = self.redshift_renderer.get("image_format") self._set_global_output_settings() - # Resolution - resWidth = self.attributes.get("resolutionWidth") - resHeight = self.attributes.get("resolutionHeight") - cmds.setAttr("redshiftOptions.imageFormat", img_ext) - cmds.setAttr("defaultResolution.width", resWidth) - cmds.setAttr("defaultResolution.height", resHeight) + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) def _set_vray_settings(self, aov_separator, width, height): # type: (str, int, int) -> None From 5969124fbc059f9f6d42db866c5f5a02383e2d4e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:21:08 +0300 Subject: [PATCH 043/432] Arnold function cleanup. --- openpype/hosts/maya/api/lib_rendersettings.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 887cbc775e..13ab0ae6cb 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -54,20 +54,6 @@ class RenderSettings(object): render_settings = RenderSettings(project_settings) render_settings.set_default_renderer_settings(renderer) - def _set_Arnold_settings(self): - """Sets settings for Arnold.""" - - img_ext = self.arnold_renderer.get("image_format") - self._set_global_output_settings() - # Resolution - resWidth = self.attributes.get("resolutionWidth") - resHeight = self.attributes.get("resolutionHeight") - - cmds.setAttr("defaultArnoldDriver.ai_translator", - img_ext, type="string") - cmds.setAttr("defaultResolution.width", resWidth) - cmds.setAttr("defaultResolution.height", resHeight) - def set_default_renderer_settings(self, renderer): """Set basic settings based on renderer. @@ -106,6 +92,16 @@ class RenderSettings(object): if renderer == "redshift": self._set_redshift_settings() + def _set_Arnold_settings(self, width, height): + """Sets settings for Arnold.""" + + img_ext = self.arnold_renderer.get("image_format") + self._set_global_output_settings() + cmds.setAttr("defaultArnoldDriver.ai_translator", + img_ext, type="string") + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" From 47e70d33c79787d88d912daa771a007ca0ef101d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 00:31:03 +0300 Subject: [PATCH 044/432] add comment about vray file format setting --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 13ab0ae6cb..5e0d40e6f9 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -142,7 +142,7 @@ class RenderSettings(object): type="string" ) - # set format to exr + # Set render file format to exr cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") # animType From b902b2a7e96008268d0f87493265195297b679f2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 22 Apr 2022 07:55:49 +0300 Subject: [PATCH 045/432] Remove unnecessary checkbox --- .../projects_schema/schemas/schema_maya_render_settings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 62e9c9e461..2f8b9562bf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -3,7 +3,6 @@ "collapsible": true, "key": "RenderSettings", "label": "Render Settings", - "checkbox_key": "enabled", "children": [ { "type": "boolean", From 61b59ef2c4b6f1af5fa4eedc7c1ab361cf603d70 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 09:09:38 +0300 Subject: [PATCH 046/432] add checkbox to render settings to apply render settings on creation --- .../projects_schema/schemas/schema_maya_render_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 2f8b9562bf..8a5730fbef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -6,8 +6,8 @@ "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "apply_render_settings", + "label": "Apply Render Settings on creation" }, { "type": "text", From 998eb0ee762700c80ca675cbfe3db9d6d8f0e1dd Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 09:12:55 +0300 Subject: [PATCH 047/432] remove redundant schema settings --- .../schemas/schema_maya_create_render.json | 397 ------------------ 1 file changed, 397 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json index f4a724cd5c..68ad7ad63d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json @@ -15,403 +15,6 @@ "key": "defaults", "label": "Default Subsets", "object_type": "text" - }, - { - "type": "text", - "key": "default_render_image_folder", - "label": "Default render image folder" - }, - { - "key": "aov_separator", - "label": "AOV Separator character", - "type": "enum", - "multiselection": false, - "default": "underscore", - "enum_items": [ - {"dash": "- (dash)"}, - {"underscore": "_ (underscore)"}, - {"dot": ". (dot)"} - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "arnold_renderer", - "label": "Arnold Renderer", - "is_group": true, - "children": [ - { - "key": "image_prefix", - "label": "Image prefix template", - "type": "text" - }, - { - "key": "image_format", - "label": "Output Image Format", - "type": "enum", - "multiselection": false, - "defaults": "exr", - "enum_items": [ - {"jpeg": "jpeg"}, - {"png": "png"}, - {"deepexr": "deep exr"}, - {"tif": "tif"}, - {"exr": "exr"}, - {"maya": "maya"}, - {"mtoa_shaders": "mtoa_shaders"} - ] - }, - { - "key": "multilayer_exr", - "label": "Multilayer (exr)", - "type": "boolean" - }, - { - "key": "tiled", - "label": "Tiled (tif, exr)", - "type": "boolean" - }, - { - "key": "aov_list", - "label": "AOVs to create", - "type": "enum", - "multiselection": true, - "defaults": "empty", - "enum_items": [ - {"empty": "< empty >"}, - {"ID": "ID"}, - {"N": "N"}, - {"P": "P"}, - {"Pref": "Pref"}, - {"RGBA": "RGBA"}, - {"Z": "Z"}, - {"albedo": "albedo"}, - {"background": "background"}, - {"coat": "coat"}, - {"coat_albedo": "coat_albedo"}, - {"coat_direct": "coat_direct"}, - {"coat_indirect": "coat_indirect"}, - {"cputime": "cputime"}, - {"crypto_asset": "crypto_asset"}, - {"crypto_material": "cypto_material"}, - {"crypto_object": "crypto_object"}, - {"diffuse": "diffuse"}, - {"diffuse_albedo": "diffuse_albedo"}, - {"diffuse_direct": "diffuse_direct"}, - {"diffuse_indirect": "diffuse_indirect"}, - {"direct": "direct"}, - {"emission": "emission"}, - {"highlight": "highlight"}, - {"indirect": "indirect"}, - {"motionvector": "motionvector"}, - {"opacity": "opacity"}, - {"raycount": "raycount"}, - {"rim_light": "rim_light"}, - {"shadow": "shadow"}, - {"shadow_diff": "shadow_diff"}, - {"shadow_mask": "shadow_mask"}, - {"shadow_matte": "shadow_matte"}, - {"sheen": "sheen"}, - {"sheen_albedo": "sheen_albedo"}, - {"sheen_direct": "sheen_direct"}, - {"sheen_indirect": "sheen_indirect"}, - {"specular": "specular"}, - {"specular_albedo": "specular_albedo"}, - {"specular_direct": "specular_direct"}, - {"specular_indirect": "specular_indirect"}, - {"sss": "sss"}, - {"sss_albedo": "sss_albedo"}, - {"sss_direct": "sss_direct"}, - {"sss_indirect": "sss_indirect"}, - {"transmission": "transmission"}, - {"transmission_albedo": "transmission_albedo"}, - {"transmission_direct": "transmission_direct"}, - {"transmission_indirect": "transmission_indirect"}, - {"volume": "volume"}, - {"volume_Z": "volume_Z"}, - {"volume_albedo": "volume_albedo"}, - {"volume_direct": "volume_direct"}, - {"volume_indirect": "volume_indirect"}, - {"volume_opacity": "volume_opacity"} - ] - }, - { - "type": "label", - "label": "Add additional options - put attribute and value, like AASamples" - }, - { - "type": "dict-modifiable", - "key": "additional_options", - "label": "Additional Renderer Options", - "use_label_wrap": true, - "object_type": { - "type": "text" - } - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "vray_renderer", - "label": "V-Ray Renderer", - "is_group": true, - "children": [ - { - "key": "image_prefix", - "label": "Image prefix template", - "type": "text" - }, - { - "key": "engine", - "label": "Production Engine", - "type": "enum", - "multiselection": false, - "defaults": "1", - "enum_items": [ - {"1": "V-Ray"}, - {"2": "V-Ray GPU"} - ] - }, - { - "key": "image_format", - "label": "Output Image Format", - "type": "enum", - "multiselection": false, - "defaults": "exr", - "enum_items": [ - {"png": "png"}, - {"jpg": "jpg"}, - {"vrimg": "vrimg"}, - {"hdr": "hdr"}, - {"exr": "exr"}, - {"exr (multichannel)": "exr (multichannel)"}, - {"exr (deep)": "exr (deep)"}, - {"tga": "tga"}, - {"bmp": "bmp"}, - {"sgi": "sgi"} - ] - }, - { - "key": "aov_list", - "label": "AOVs to create", - "type": "enum", - "multiselection": true, - "defaults": "empty", - "enum_items": [ - {"empty": "< empty >"}, - {"atmosphereChannel": "atmosphere"}, - {"backgroundChannel": "background"}, - {"bumpNormalsChannel": "bumpnormals"}, - {"causticsChannel": "caustics"}, - {"coatFilterChannel": "coat_filter"}, - {"coatGlossinessChannel": "coatGloss"}, - {"coatReflectionChannel": "coat_reflection"}, - {"vrayCoatChannel": "coat_specular"}, - {"CoverageChannel": "coverage"}, - {"cryptomatteChannel": "cryptomatte"}, - {"customColor": "custom_color"}, - {"drBucketChannel": "DR"}, - {"denoiserChannel": "denoiser"}, - {"diffuseChannel": "diffuse"}, - {"ExtraTexElement": "extraTex"}, - {"giChannel": "GI"}, - {"LightMixElement": "None"}, - {"lightingChannel": "lighting"}, - {"LightingAnalysisChannel": "LightingAnalysis"}, - {"materialIDChannel": "materialID"}, - {"MaterialSelectElement": "materialSelect"}, - {"matteShadowChannel": "matteShadow"}, - {"MultiMatteElement": "multimatte"}, - {"multimatteIDChannel": "multimatteID"}, - {"normalsChannel": "normals"}, - {"nodeIDChannel": "objectId"}, - {"objectSelectChannel": "objectSelect"}, - {"rawCoatFilterChannel": "raw_coat_filter"}, - {"rawCoatReflectionChannel": "raw_coat_reflection"}, - {"rawDiffuseFilterChannel": "rawDiffuseFilter"}, - {"rawGiChannel": "rawGI"}, - {"rawLightChannel": "rawLight"}, - {"rawReflectionChannel": "rawReflection"}, - {"rawReflectionFilterChannel": "rawReflectionFilter"}, - {"rawRefractionChannel": "rawRefraction"}, - {"rawRefractionFilterChannel": "rawRefractionFilter"}, - {"rawShadowChannel": "rawShadow"}, - {"rawSheenFilterChannel": "raw_sheen_filter"}, - {"rawSheenReflectionChannel": "raw_sheen_reflection"}, - {"rawTotalLightChannel": "rawTotalLight"}, - {"reflectIORChannel": "reflIOR"}, - {"reflectChannel": "reflect"}, - {"reflectionFilterChannel": "reflectionFilter"}, - {"reflectGlossinessChannel": "reflGloss"}, - {"refractChannel": "refract"}, - {"refractionFilterChannel": "refractionFilter"}, - {"refractGlossinessChannel": "refrGloss"}, - {"renderIDChannel": "renderId"}, - {"FastSSS2Channel": "SSS"}, - {"sampleRateChannel": "sampleRate"}, - {"samplerInfo": "samplerInfo"}, - {"selfIllumChannel": "selfIllum"}, - {"shadowChannel": "shadow"}, - {"sheenFilterChannel": "sheen_filter"}, - {"sheenGlossinessChannel": "sheenGloss"}, - {"sheenReflectionChannel": "sheen_reflection"}, - {"vraySheenChannel": "sheen_specular"}, - {"specularChannel": "specular"}, - {"Toon": "Toon"}, - {"toonLightingChannel": "toonLighting"}, - {"toonSpecularChannel": "toonSpecular"}, - {"totalLightChannel": "totalLight"}, - {"unclampedColorChannel": "unclampedColor"}, - {"VRScansPaintMaskChannel": "VRScansPaintMask"}, - {"VRScansZoneMaskChannel": "VRScansZoneMask"}, - {"velocityChannel": "velocity"}, - {"zdepthChannel": "zDepth"}, - {"LightSelectElement": "lightselect"} - ] - }, - { - "type": "label", - "label": "Add additional options - put attribute and value, like aaFilterSize" - }, - { - "type": "dict-modifiable", - "key": "additional_options", - "label": "Additional Renderer Options", - "use_label_wrap": true, - "object_type": { - "type": "text" - } - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "redshift_renderer", - "label": "Redshift Renderer", - "is_group": true, - "children": [ - { - "key": "image_prefix", - "label": "Image prefix template", - "type": "text" - }, - { - "key": "primary_gi_engine", - "label": "Primary GI Engine", - "type": "enum", - "multiselection": false, - "defaults": "0", - "enum_items": [ - {"0": "None"}, - {"1": "Photon Map"}, - {"2": "Irradiance Cache"}, - {"3": "Brute Force"} - ] - }, - { - "key": "secondary_gi_engine", - "label": "Secondary GI Engine", - "type": "enum", - "multiselection": false, - "defaults": "0", - "enum_items": [ - {"0": "None"}, - {"1": "Photon Map"}, - {"2": "Irradiance Cache"}, - {"3": "Brute Force"} - ] - }, - { - "key": "image_format", - "label": "Output Image Format", - "type": "enum", - "multiselection": false, - "defaults": "exr", - "enum_items": [ - {"iff": "Maya IFF"}, - {"exr": "OpenEXR"}, - {"tif": "TIFF"}, - {"png": "PNG"}, - {"tga": "Targa"}, - {"jpg": "JPEG"} - ] - }, - { - "key": "multilayer_exr", - "label": "Multilayer (exr)", - "type": "boolean" - }, - { - "key": "force_combine", - "label": "Force combine beauty and AOVs", - "type": "boolean" - }, - { - "key": "aov_list", - "label": "AOVs to create", - "type": "enum", - "multiselection": true, - "defaults": "empty", - "enum_items": [ - {"empty": "< none >"}, - {"AO": "Ambient Occlusion"}, - {"Background": "Background"}, - {"Beauty": "Beauty"}, - {"BumpNormals": "Bump Normals"}, - {"Caustics": "Caustics"}, - {"CausticsRaw": "Caustics Raw"}, - {"Cryptomatte": "Cryptomatte"}, - {"Custom": "Custom"}, - {"Z": "Depth"}, - {"DiffuseFilter": "Diffuse Filter"}, - {"DiffuseLighting": "Diffuse Lighting"}, - {"DiffuseLightingRaw": "Diffuse Lighting Raw"}, - {"Emission": "Emission"}, - {"GI": "Global Illumination"}, - {"GIRaw": "Global Illumination Raw"}, - {"Matte": "Matte"}, - {"MotionVectors": "Ambient Occlusion"}, - {"N": "Normals"}, - {"ID": "ObjectID"}, - {"ObjectBumpNormal": "Object-Space Bump Normals"}, - {"ObjectPosition": "Object-Space Positions"}, - {"PuzzleMatte": "Puzzle Matte"}, - {"Reflections": "Reflections"}, - {"ReflectionsFilter": "Reflections Filter"}, - {"ReflectionsRaw": "Reflections Raw"}, - {"Refractions": "Refractions"}, - {"RefractionsFilter": "Refractions Filter"}, - {"RefractionsRaw": "Refractions Filter"}, - {"Shadows": "Shadows"}, - {"SpecularLighting": "Specular Lighting"}, - {"SSS": "Sub Surface Scatter"}, - {"SSSRaw": "Sub Surface Scatter Raw"}, - {"TotalDiffuseLightingRaw": "Total Diffuse Lighting Raw"}, - {"TotalTransLightingRaw": "Total Translucency Filter"}, - {"TransTint": "Translucency Filter"}, - {"TransGIRaw": "Translucency Lighting Raw"}, - {"VolumeFogEmission": "Volume Fog Emission"}, - {"VolumeFogTint": "Volume Fog Tint"}, - {"VolumeLighting": "Volume Lighting"}, - {"P": "World Position"} - ] - }, - { - "type": "label", - "label": "Add additional options - put attribute and value, like reflectionMaxTraceDepth" - }, - { - "type": "dict-modifiable", - "key": "additional_options", - "label": "Additional Renderer Options", - "use_label_wrap": true, - "object_type": { - "type": "text" - } - } - ] } ] } \ No newline at end of file From 365a6b3990a2c1480eac139cee340ff0580ff58f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 09:32:12 +0300 Subject: [PATCH 048/432] add menu item to OpenPype menu --- openpype/hosts/maya/api/menu.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 5f0fc39bf3..133877a63e 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,7 +11,7 @@ import avalon.api from openpype.api import BuildWorkfile from openpype.settings import get_project_settings from openpype.tools.utils import host_tools -from openpype.hosts.maya.api import lib +from openpype.hosts.maya.api import lib, lib_rendersettings from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range @@ -99,6 +99,15 @@ def install(): cmds.menuItem(divider=True) + cmds.menuItem( + "Set Render Settings", + command=lambda *args: lib_rendersettings.set_default_renderer_settings( # noqa + parent=parent_widget + ) + ) + + cmds.menuItem(divider=True) + cmds.menuItem( "Work Files...", command=lambda *args: host_tools.show_workfiles( From a33a9057cd11706391da12dc38f031741459a811 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 09:42:13 +0300 Subject: [PATCH 049/432] modify project settings schema defaults for maya --- .../defaults/project_settings/maya.json | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index c0b85eb0eb..7dcefeff3f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -30,6 +30,36 @@ } ] }, + "RenderSettings": { + "apply_render_settings": true, + "default_render_image_folder": "", + "aov_separator": "underscore", + "arnold_renderer": { + "image_prefix": "", + "image_format": "exr", + "multilayer_exr": true, + "tiled": true, + "aov_list": [], + "additional_options": {} + }, + "vray_renderer": { + "image_prefix": "", + "engine": "1", + "image_format": "png", + "aov_list": [], + "additional_options": {} + }, + "redshift_renderer": { + "image_prefix": "", + "primary_gi_engine": "0", + "secondary_gi_engine": "0", + "image_format": "iff", + "multilayer_exr": true, + "force_combine": true, + "aov_list": [], + "additional_options": {} + } + }, "create": { "CreateLook": { "enabled": true, @@ -42,40 +72,7 @@ "enabled": true, "defaults": [ "Main" - ], - "default_render_image_folder": "renders", - "aov_separator": "underscore", - "arnold_renderer": { - "image_prefix": "maya///{aov_separator}", - "image_format": "exr", - "multilayer_exr": false, - "tiled": true, - "aov_list": [ - "empty" - ], - "additional_options": {} - }, - "vray_renderer": { - "image_prefix": "maya///", - "engine": "1", - "image_format": "exr", - "aov_list": [ - "empty" - ], - "additional_options": {} - }, - "redshift_renderer": { - "image_prefix": "'maya///{aov_separator}", - "primary_gi_engine": "0", - "secondary_gi_engine": "0", - "image_format": "exr", - "multilayer_exr": false, - "force_combine": false, - "aov_list": [ - "empty" - ], - "additional_options": {} - } + ] }, "CreateUnrealStaticMesh": { "enabled": true, From 18693cf96f3830a7376109f3015eb47894bb764c Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 12:00:26 +0300 Subject: [PATCH 050/432] fix function argument, add renderer --- openpype/hosts/maya/api/menu.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 133877a63e..4b79357f0b 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -45,6 +45,7 @@ def install(): parent="MayaWindow" ) + renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() # Create context menu context_label = "{}, {}".format( avalon.api.Session["AVALON_ASSET"], @@ -101,9 +102,7 @@ def install(): cmds.menuItem( "Set Render Settings", - command=lambda *args: lib_rendersettings.set_default_renderer_settings( # noqa - parent=parent_widget - ) + command=lambda *args: lib_rendersettings.RenderSettings.set_default_renderer_settings(renderer) # noqa ) cmds.menuItem(divider=True) From d3d27576ec01a261f01eb5e2abd05ca6925dff33 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 12:13:59 +0300 Subject: [PATCH 051/432] Fix Arnold function missing arguments. --- openpype/hosts/maya/api/lib_rendersettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 5e0d40e6f9..cdd65de209 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -8,7 +8,7 @@ from avalon.api import Session from openpype.pipeline import CreatorError -class RenderSettings(object): +class RenderSettzings(object): _image_prefix_nodes = { 'mentalray': 'defaultRenderGlobals.imageFilePrefix', @@ -84,7 +84,7 @@ class RenderSettings(object): if renderer == "arnold": # set renderer settings for Arnold from project settings - self._set_Arnold_settings() + self._set_Arnold_settings(width, height) if renderer == "vray": self._set_vray_settings(aov_separator, width, height) From 09a941acd0cbfa5270d7c8f0b3f3680ae6acc72d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 12:18:19 +0300 Subject: [PATCH 052/432] Fix Redshift function missing arguments. --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index cdd65de209..4362511fc4 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -90,7 +90,7 @@ class RenderSettzings(object): self._set_vray_settings(aov_separator, width, height) if renderer == "redshift": - self._set_redshift_settings() + self._set_redshift_settings(width, height) def _set_Arnold_settings(self, width, height): """Sets settings for Arnold.""" From 6299b01ae6532272a0f18e743aa358fe995c1c12 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 12:23:13 +0300 Subject: [PATCH 053/432] Fix accidental typo. --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 4362511fc4..33b138fa08 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -8,7 +8,7 @@ from avalon.api import Session from openpype.pipeline import CreatorError -class RenderSettzings(object): +class RenderSettings(object): _image_prefix_nodes = { 'mentalray': 'defaultRenderGlobals.imageFilePrefix', From 1e251ac064a74f3e0b4b8b3fddaf5a42213c6f88 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Apr 2022 12:25:02 +0300 Subject: [PATCH 054/432] Remove trailing space. --- openpype/hosts/maya/api/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 4b79357f0b..c1aea4da78 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -45,7 +45,7 @@ def install(): parent="MayaWindow" ) - renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() + renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() # Create context menu context_label = "{}, {}".format( avalon.api.Session["AVALON_ASSET"], From 7a63e52a3fa520aa67c6fb9c21a11671fcea4317 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Apr 2022 00:12:29 +0300 Subject: [PATCH 055/432] Append Arnold render settings from project settings. --- openpype/hosts/maya/api/lib_rendersettings.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 33b138fa08..26e2455d86 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -81,10 +81,10 @@ class RenderSettings(object): asset = get_asset() width = asset["data"].get("resolutionWidth") height = asset["data"].get("resolutionHeight") - + arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] if renderer == "arnold": # set renderer settings for Arnold from project settings - self._set_Arnold_settings(width, height) + self._set_Arnold_settings(arnold_render_presets, width, height) if renderer == "vray": self._set_vray_settings(aov_separator, width, height) @@ -92,13 +92,12 @@ class RenderSettings(object): if renderer == "redshift": self._set_redshift_settings(width, height) - def _set_Arnold_settings(self, width, height): + def _set_Arnold_settings(self, settings, width, height): """Sets settings for Arnold.""" - img_ext = self.arnold_renderer.get("image_format") + img_ext = settings["image_format"] self._set_global_output_settings() - cmds.setAttr("defaultArnoldDriver.ai_translator", - img_ext, type="string") + cmds.setAttr("defaultArnoldDriver.ai_translator", img_ext, type="string") cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) From 14a34836b710052ebc41744ec963ba8d062a03f3 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Apr 2022 02:26:38 +0300 Subject: [PATCH 056/432] Add Maya window function call to initalize render objects. --- openpype/hosts/maya/api/lib_rendersettings.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 26e2455d86..1dcea16640 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,4 +1,4 @@ -from maya import cmds +from maya import cmds, mel from openpype.api import ( get_project_settings, @@ -94,12 +94,16 @@ class RenderSettings(object): def _set_Arnold_settings(self, settings, width, height): """Sets settings for Arnold.""" + mel.eval('unifiedRenderGlobalsWindow;') + if cmds.window("unifiedRenderGlobalsWindow", exists=True): + cmds.deleteUI("unifiedRenderGlobalsWindow") + + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) img_ext = settings["image_format"] self._set_global_output_settings() cmds.setAttr("defaultArnoldDriver.ai_translator", img_ext, type="string") - cmds.setAttr("defaultResolution.width", width) - cmds.setAttr("defaultResolution.height", height) def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" From 79d770054125c6d585cdd70cf9eb03d5f5b08f39 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Apr 2022 13:30:12 +0300 Subject: [PATCH 057/432] fix asset var name, add relevant comments --- openpype/hosts/maya/api/lib_rendersettings.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 1dcea16640..70ec1ebb47 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -78,10 +78,12 @@ class RenderSettings(object): prefix, type="string") - asset = get_asset() - width = asset["data"].get("resolutionWidth") - height = asset["data"].get("resolutionHeight") + asset_doc = get_asset() + # TODO: handle not having res values in the doc + width = asset_doc["data"].get("resolutionWidth") + height = asset_doc["data"].get("resolutionHeight")# TODO: don't camelcase arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] + if renderer == "arnold": # set renderer settings for Arnold from project settings self._set_Arnold_settings(arnold_render_presets, width, height) From fb424f672609b91cec693e2c13bdf224b9c4a998 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Apr 2022 15:12:15 +0300 Subject: [PATCH 058/432] replace render settings workaround with function call --- openpype/hosts/maya/api/lib_rendersettings.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 70ec1ebb47..13317cf85e 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,4 +1,5 @@ -from maya import cmds, mel +from maya import cmds +from mtoa.core import createOptions from openpype.api import ( get_project_settings, @@ -96,11 +97,7 @@ class RenderSettings(object): def _set_Arnold_settings(self, settings, width, height): """Sets settings for Arnold.""" - mel.eval('unifiedRenderGlobalsWindow;') - - if cmds.window("unifiedRenderGlobalsWindow", exists=True): - cmds.deleteUI("unifiedRenderGlobalsWindow") - + createOptions() cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) img_ext = settings["image_format"] From 6645be26ef175de388b36096127e6696e1028947 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 2 May 2022 13:34:49 +0300 Subject: [PATCH 059/432] minor style/import fixes --- openpype/hosts/maya/api/lib_renderproducts.py | 1 + openpype/hosts/maya/plugins/create/create_render.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index f62432b2e9..1f38ef8904 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -81,6 +81,7 @@ IMAGE_PREFIXES = { RENDERMAN_IMAGE_DIR = "maya//" + def has_tokens(string, tokens): """Return whether any of tokens is in input string (case-insensitive)""" pattern = "({})".format("|".join(re.escape(token) for token in tokens)) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 4e36f922d9..6b65911cf3 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -2,11 +2,9 @@ """Create ``Render`` instance in Maya.""" import json import os -import sys import appdirs import requests -import six from maya import cmds from maya.app.renderSetup.model import renderSetup @@ -21,7 +19,6 @@ from openpype.hosts.maya.api import ( lib_rendersettings, plugin ) -from openpype.modules import ModulesManager class CreateRender(plugin.Creator): From 2cdea369dc1849243fc926301ec4780a42774fee Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 2 May 2022 13:35:40 +0300 Subject: [PATCH 060/432] Remove avalon import. --- openpype/hosts/maya/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 6b65911cf3..2bbaf1006d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -9,7 +9,6 @@ import requests from maya import cmds from maya.app.renderSetup.model import renderSetup -from avalon.api import Session from openpype.api import ( get_system_settings, get_project_settings, From e0b0e30734b8c036fbec4f65c8c2b678be59d053 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 2 May 2022 13:54:09 +0300 Subject: [PATCH 061/432] replace avalon dependency with legacy_io --- openpype/hosts/maya/api/lib_rendersettings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 13317cf85e..4e1c4f7bd2 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -5,7 +5,7 @@ from openpype.api import ( get_project_settings, get_asset) -from avalon.api import Session +from openpype.pipeline import legacy_io from openpype.pipeline import CreatorError @@ -50,7 +50,7 @@ class RenderSettings(object): renderer = 'renderman' if project_settings is None: - project_settings = get_project_settings(Session["AVALON_PROJECT"]) + project_settings = get_project_settings(legacy_io.Session["AVALON_PROJECT"]) render_settings = RenderSettings(project_settings) render_settings.set_default_renderer_settings(renderer) @@ -97,6 +97,7 @@ class RenderSettings(object): def _set_Arnold_settings(self, settings, width, height): """Sets settings for Arnold.""" + createOptions() cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) From 302e493f617a9d027243a8ca661f99b482bbbb55 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 2 May 2022 13:54:39 +0300 Subject: [PATCH 062/432] Change import position. --- openpype/hosts/maya/api/lib_rendersettings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 4e1c4f7bd2..5afcd94758 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,5 +1,4 @@ from maya import cmds -from mtoa.core import createOptions from openpype.api import ( get_project_settings, @@ -97,7 +96,7 @@ class RenderSettings(object): def _set_Arnold_settings(self, settings, width, height): """Sets settings for Arnold.""" - + from mtoa.core import createOptions createOptions() cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) From 103cd8c18093bd4af8e2be23795107b6e87c910a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 6 May 2022 12:40:18 +0300 Subject: [PATCH 063/432] Move settings getter function --- openpype/hosts/maya/api/lib_rendersettings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 5afcd94758..7b2145b7ac 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -81,8 +81,7 @@ class RenderSettings(object): asset_doc = get_asset() # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") - height = asset_doc["data"].get("resolutionHeight")# TODO: don't camelcase - arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] + height = asset_doc["data"].get("resolutionHeight") if renderer == "arnold": # set renderer settings for Arnold from project settings @@ -94,13 +93,16 @@ class RenderSettings(object): if renderer == "redshift": self._set_redshift_settings(width, height) - def _set_Arnold_settings(self, settings, width, height): + def _set_Arnold_settings(self, width, height): """Sets settings for Arnold.""" from mtoa.core import createOptions createOptions() + arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] # noqa + img_ext = arnold_render_presets["image_format"] + cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) - img_ext = settings["image_format"] + self._set_global_output_settings() cmds.setAttr("defaultArnoldDriver.ai_translator", img_ext, type="string") From 2bb96a90a49114080dc1e4bb7ad978c4b5ecaa05 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 6 May 2022 13:55:53 +0300 Subject: [PATCH 064/432] Append aov handling --- openpype/hosts/maya/api/lib_rendersettings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 7b2145b7ac..582bdc224a 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -96,9 +96,14 @@ class RenderSettings(object): def _set_Arnold_settings(self, width, height): """Sets settings for Arnold.""" from mtoa.core import createOptions + from mtoa.aovs import AOVInterface createOptions() arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] # noqa img_ext = arnold_render_presets["image_format"] + aovs = arnold_render_presets["aov_list"] + + for aov in aovs: + AOVInterface('defaultArnoldRenderOptions'.addAOV(aov)) cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) From f6d442330de7585fb1ce76389249fb6c43b6d5c4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 9 May 2022 12:48:25 +0300 Subject: [PATCH 065/432] Get renderer from within settings function --- openpype/hosts/maya/api/lib_rendersettings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 582bdc224a..64e3d07a44 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -54,13 +54,15 @@ class RenderSettings(object): render_settings = RenderSettings(project_settings) render_settings.set_default_renderer_settings(renderer) - def set_default_renderer_settings(self, renderer): + @staticmethod + def set_default_renderer_settings(self): """Set basic settings based on renderer. Args: renderer (str): Renderer name. """ + renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() # project_settings/maya/create/CreateRender/aov_separator try: aov_separator = self._aov_chars[( From ad6f562f80f1c916eae3226bae282e4c55eedacf Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 9 May 2022 13:30:40 +0300 Subject: [PATCH 066/432] Remove unused parameter --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 64e3d07a44..13be2a1e26 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -87,7 +87,7 @@ class RenderSettings(object): if renderer == "arnold": # set renderer settings for Arnold from project settings - self._set_Arnold_settings(arnold_render_presets, width, height) + self._set_Arnold_settings(width, height) if renderer == "vray": self._set_vray_settings(aov_separator, width, height) From e4324a11f7f11a5b76c48388b53b25c3d63d0efe Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 9 May 2022 13:34:11 +0300 Subject: [PATCH 067/432] Move get_asset() --- openpype/hosts/maya/api/lib_rendersettings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 13be2a1e26..73f03975bb 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -63,6 +63,7 @@ class RenderSettings(object): """ renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() + asset_doc = get_asset() # project_settings/maya/create/CreateRender/aov_separator try: aov_separator = self._aov_chars[( @@ -80,7 +81,7 @@ class RenderSettings(object): prefix, type="string") - asset_doc = get_asset() + # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") height = asset_doc["data"].get("resolutionHeight") From bf1daa4e906ef36afcbaf20b21578106b7e24f41 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 9 May 2022 13:34:28 +0300 Subject: [PATCH 068/432] style fix --- openpype/hosts/maya/api/lib_rendersettings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 73f03975bb..3ac663e38d 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -81,7 +81,6 @@ class RenderSettings(object): prefix, type="string") - # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") height = asset_doc["data"].get("resolutionHeight") From bb67065d39a092383ce1a277cb68f23d2302c225 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 10 May 2022 12:14:15 +0200 Subject: [PATCH 069/432] few style changes --- openpype/hosts/maya/api/lib_rendersettings.py | 61 +++++++++++-------- openpype/hosts/maya/api/menu.py | 2 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 3ac663e38d..03c70ee3d6 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,4 +1,8 @@ -from maya import cmds +# -*- coding: utf-8 -*- +"""Class for handling Render Settings.""" +from maya import cmds # noqa +import six +import sys from openpype.api import ( get_project_settings, @@ -36,8 +40,12 @@ class RenderSettings(object): def get_image_prefix_attr(cls, renderer): return cls._image_prefix_nodes[renderer] - def __init__(self, project_settings): + def __init__(self, project_settings=None): self._project_settings = project_settings + if not self._project_settings: + self._project_settings = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) @staticmethod def apply_defaults(renderer=None, project_settings=None): @@ -48,21 +56,15 @@ class RenderSettings(object): if renderer.startswith('renderman'): renderer = 'renderman' - if project_settings is None: - project_settings = get_project_settings(legacy_io.Session["AVALON_PROJECT"]) - render_settings = RenderSettings(project_settings) render_settings.set_default_renderer_settings(renderer) - @staticmethod - def set_default_renderer_settings(self): - """Set basic settings based on renderer. + def set_default_renderer_settings(self, renderer=None): + """Set basic settings based on renderer.""" + if not renderer: + renderer = cmds.getAttr( + 'defaultRenderGlobals.currentRenderer').lower() - Args: - renderer (str): Renderer name. - - """ - renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() asset_doc = get_asset() # project_settings/maya/create/CreateRender/aov_separator try: @@ -87,7 +89,7 @@ class RenderSettings(object): if renderer == "arnold": # set renderer settings for Arnold from project settings - self._set_Arnold_settings(width, height) + self._set_arnold_settings(width, height) if renderer == "vray": self._set_vray_settings(aov_separator, width, height) @@ -95,28 +97,34 @@ class RenderSettings(object): if renderer == "redshift": self._set_redshift_settings(width, height) - def _set_Arnold_settings(self, width, height): + def _set_arnold_settings(self, width, height): """Sets settings for Arnold.""" - from mtoa.core import createOptions - from mtoa.aovs import AOVInterface + from mtoa.core import createOptions # noqa + from mtoa.aovs import AOVInterface # noqa createOptions() arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] # noqa img_ext = arnold_render_presets["image_format"] aovs = arnold_render_presets["aov_list"] for aov in aovs: - AOVInterface('defaultArnoldRenderOptions'.addAOV(aov)) + AOVInterface('defaultArnoldRenderOptions').addAOV(aov) cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._set_global_output_settings() - cmds.setAttr("defaultArnoldDriver.ai_translator", img_ext, type="string") + cmds.setAttr( + "defaultArnoldDriver.ai_translator", img_ext, type="string") def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" - - img_ext = self.redshift_renderer.get("image_format") + redshift_render_presets = ( + self._project_settings + ["maya"] + ["RenderSettings"] + ["redshift_renderer"] + ) + img_ext = redshift_render_presets.get("image_format") self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) cmds.setAttr("defaultResolution.width", width) @@ -138,10 +146,13 @@ class RenderSettings(object): separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 try: sep_idx = separators.index(aov_separator) - except ValueError: - raise CreatorError( - "AOV character {} not in {}".format( - aov_separator, separators)) + except ValueError as e: + six.reraise( + CreatorError, + CreatorError( + "AOV character {} not in {}".format( + aov_separator, separators)), + sys.exc_info()[2]) cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 3c43c192e3..c3ce8b0227 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -101,7 +101,7 @@ def install(): cmds.menuItem( "Set Render Settings", - command=lambda *args: lib_rendersettings.RenderSettings.set_default_renderer_settings(renderer) # noqa + command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings() # noqa ) cmds.menuItem(divider=True) From ae0708b639a1981fbefc5331a2b24af50079470a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 11 May 2022 12:28:45 +0300 Subject: [PATCH 070/432] Force resetting render settings --- openpype/hosts/maya/api/lib_rendersettings.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 03c70ee3d6..3946750add 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Class for handling Render Settings.""" from maya import cmds # noqa +import maya.mel as mel import six import sys @@ -103,9 +104,17 @@ class RenderSettings(object): from mtoa.aovs import AOVInterface # noqa createOptions() arnold_render_presets = self._project_settings["maya"]["RenderSettings"]["arnold_renderer"] # noqa + # Force resetting settings and AOV list to avoid having to deal with + # AOV checking logic, for now. + # This is a work around because the standard + # function to revert render settings does not reset AOVs list in MtoA + # Fetch current aovs in case there's any. + current_aovs = AOVInterface().getAOVs() + # Remove fetched AOVs + AOVInterface().removeAOVs(current_aovs) + mel.eval("unifiedRenderGlobalsRevertToDefault") img_ext = arnold_render_presets["image_format"] aovs = arnold_render_presets["aov_list"] - for aov in aovs: AOVInterface('defaultArnoldRenderOptions').addAOV(aov) From 6809d372b8b619a676beecce153f3cd14c273279 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 12 May 2022 00:15:18 +0300 Subject: [PATCH 071/432] Propagate further attributes. --- openpype/hosts/maya/api/lib_rendersettings.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 3946750add..18e5e132d0 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -114,7 +114,10 @@ class RenderSettings(object): AOVInterface().removeAOVs(current_aovs) mel.eval("unifiedRenderGlobalsRevertToDefault") img_ext = arnold_render_presets["image_format"] + img_prefix = arnold_render_presets["image_prefix"] aovs = arnold_render_presets["aov_list"] + img_tiled = arnold_render_presets["tiled"] + multi_exr = arnold_render_presets["multilayer_exr"] for aov in aovs: AOVInterface('defaultArnoldRenderOptions').addAOV(aov) @@ -122,9 +125,22 @@ class RenderSettings(object): cmds.setAttr("defaultResolution.height", height) self._set_global_output_settings() + + cmds.setAttr( + "defaultRenderGlobals.imageFilePrefix", img_prefix, type="string") + cmds.setAttr( "defaultArnoldDriver.ai_translator", img_ext, type="string") + cmds.setAttr( + "defaultArnoldDriver.exrTiled", img_tiled, type="boolean") + + cmds.setAttr( + "defaultArnoldDriver.mergeAOVs", multi_exr, type="boolean") + + for attr in additional_options.items(): + cmds.setAttr(attr, additional_options.get(attr, None)) + def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" redshift_render_presets = ( From b1d49692e0f4e3e00c71ed23d818885042d4d0b2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 12 May 2022 00:39:23 +0300 Subject: [PATCH 072/432] Add variable for additional attributes --- openpype/hosts/maya/api/lib_rendersettings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 18e5e132d0..3d229060be 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -118,6 +118,7 @@ class RenderSettings(object): aovs = arnold_render_presets["aov_list"] img_tiled = arnold_render_presets["tiled"] multi_exr = arnold_render_presets["multilayer_exr"] + additional_options = arnold_render_presets["additional_options"] for aov in aovs: AOVInterface('defaultArnoldRenderOptions').addAOV(aov) @@ -133,10 +134,10 @@ class RenderSettings(object): "defaultArnoldDriver.ai_translator", img_ext, type="string") cmds.setAttr( - "defaultArnoldDriver.exrTiled", img_tiled, type="boolean") + "defaultArnoldDriver.exrTiled", img_tiled) cmds.setAttr( - "defaultArnoldDriver.mergeAOVs", multi_exr, type="boolean") + "defaultArnoldDriver.mergeAOVs", multi_exr) for attr in additional_options.items(): cmds.setAttr(attr, additional_options.get(attr, None)) From e9426df72d37f65e053d6959ce91d2411e86e8a5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 12 May 2022 11:16:07 +0300 Subject: [PATCH 073/432] Fix dictionary bug. --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 3d229060be..49d7d9fc72 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -139,7 +139,7 @@ class RenderSettings(object): cmds.setAttr( "defaultArnoldDriver.mergeAOVs", multi_exr) - for attr in additional_options.items(): + for attr in additional_options.keys(): cmds.setAttr(attr, additional_options.get(attr, None)) def _set_redshift_settings(self, width, height): From 4260f8a49ce3d63a0fc18741c6319f65e49122be Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 12 May 2022 11:29:36 +0300 Subject: [PATCH 074/432] Attr as list to workaround ftrack limitation --- openpype/hosts/maya/api/lib_rendersettings.py | 9 ++++++--- .../schemas/schema_maya_render_settings.json | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 49d7d9fc72..c6afbfa19c 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -138,9 +138,12 @@ class RenderSettings(object): cmds.setAttr( "defaultArnoldDriver.mergeAOVs", multi_exr) - - for attr in additional_options.keys(): - cmds.setAttr(attr, additional_options.get(attr, None)) + # Passes additional options in from the schema as a list + # but converts it to a dictionary because ftrack doesn't + # allow fullstops in custom attributes. + additional_options_dict = dict(additional_options) + for attr in additional_options_dict.keys(): + cmds.setAttr(attr, additional_options_dict.get(attr, None)) def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 8a5730fbef..96b67dc66a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -134,6 +134,7 @@ }, { "type": "dict-modifiable", + "store_as_list": true, "key": "additional_options", "label": "Additional Renderer Options", "use_label_wrap": true, From 12a1e9e520641e1e3e700d77576c5d0d036f5879 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 13 May 2022 10:53:11 +0300 Subject: [PATCH 075/432] Handle additional attributes for MtoA --- openpype/hosts/maya/api/lib_rendersettings.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index c6afbfa19c..38f493a4a8 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -140,10 +140,17 @@ class RenderSettings(object): "defaultArnoldDriver.mergeAOVs", multi_exr) # Passes additional options in from the schema as a list # but converts it to a dictionary because ftrack doesn't - # allow fullstops in custom attributes. - additional_options_dict = dict(additional_options) - for attr in additional_options_dict.keys(): - cmds.setAttr(attr, additional_options_dict.get(attr, None)) + # allow fullstops in custom attributes. Then checks for + # type of MtoA attribute passed to adjust the `setAttr` + # command accordingly. + for item in additional_options: + attribute, value = item + if (cmds.setAttr(str(attribute), type=True)) == "long": + cmds.setAttr(str(attribute), int(value)) + elif (cmds.setAttr(str(attribute), type=True)) == "bool": + cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa + elif (cmds.setAttr(str(attribute), type=True)) == "string": + cmds.setAttr(str(attribute), str(value), type = "string") # noqa def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" From dc95b5ac0e06de4e8916ad5cd5e63ca30901e6c5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 13 May 2022 12:20:27 +0300 Subject: [PATCH 076/432] Import missing library --- openpype/hosts/maya/plugins/create/create_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 2bbaf1006d..334400bb23 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -19,6 +19,8 @@ from openpype.hosts.maya.api import ( plugin ) +from openpype.pipeline import legacy_io + class CreateRender(plugin.Creator): """Create *render* instance. From 201aa692bf9ac53d065522c51b1d780f3eec175d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 13 May 2022 14:00:36 +0300 Subject: [PATCH 077/432] Fix missing deadline import/logic --- openpype/hosts/maya/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 334400bb23..e858534912 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -18,7 +18,7 @@ from openpype.hosts.maya.api import ( lib_rendersettings, plugin ) - +from openpype.modules import ModulesManager from openpype.pipeline import legacy_io @@ -79,6 +79,8 @@ class CreateRender(plugin.Creator): self._project_settings = get_project_settings( legacy_io.Session["AVALON_PROJECT"]) + manager = ModulesManager() + self.deadline_module = manager.modules_by_name["deadline"] try: default_servers = deadline_settings["deadline_urls"] project_servers = ( @@ -234,7 +236,8 @@ class CreateRender(plugin.Creator): deadline_url = next(iter(self.deadline_servers.values())) # Uses function to get pool machines from the assigned deadline # url in settings - pool_names = self._get_deadline_pools(deadline_url) + pool_names = self.deadline_module.get_deadline_pools(deadline_url, + self.log) maya_submit_dl = self._project_settings.get( "deadline", {}).get( "publish", {}).get( From a06bfc1648d242c6c8167b28deb969174380b987 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 13 May 2022 14:02:26 +0300 Subject: [PATCH 078/432] Style fix --- openpype/hosts/maya/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index e858534912..c4a8e53a0b 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -237,7 +237,7 @@ class CreateRender(plugin.Creator): # Uses function to get pool machines from the assigned deadline # url in settings pool_names = self.deadline_module.get_deadline_pools(deadline_url, - self.log) + self.log) maya_submit_dl = self._project_settings.get( "deadline", {}).get( "publish", {}).get( From 712e1c6707a9b0b57bbf1a50f108d692c6f9030b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 14 Jun 2022 16:00:51 +0100 Subject: [PATCH 079/432] Implemented extraction of JSON layout from Maya --- .../maya/plugins/publish/extract_layout.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/extract_layout.py diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py new file mode 100644 index 0000000000..4ae99f1052 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -0,0 +1,101 @@ +import os +import json + +from maya import cmds + +from bson.objectid import ObjectId + +from openpype.pipeline import legacy_io +import openpype.api + + +class ExtractLayout(openpype.api.Extractor): + """Extract a layout.""" + + label = "Extract Layout" + hosts = ["maya"] + families = ["layout"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + + # Perform extraction + self.log.info("Performing extraction..") + + if "representations" not in instance.data: + instance.data["representations"] = [] + + json_data = [] + + for asset in cmds.sets(str(instance), query=True): + # Find the container + grp_name = asset.split(':')[0] + containers = cmds.ls(f"{grp_name}*_CON") + + assert len(containers) == 1, \ + f"More than one container found for {asset}" + + container = containers[0] + + representation_id = cmds.getAttr(f"{container}.representation") + + representation = legacy_io.find_one( + { + "type": "representation", + "_id": ObjectId(representation_id) + }, projection={"parent": True, "context.family": True}) + + self.log.info(representation) + + version_id = representation.get("parent") + family = representation.get("context").get("family") + + json_element = { + "family": family, + "instance_name": cmds.getAttr(f"{container}.name"), + "representation": str(representation_id), + "version": str(version_id) + } + + loc = cmds.xform(asset, query=True, translation=True) + rot = cmds.xform(asset, query=True, rotation=True) + scl = cmds.xform(asset, query=True, relative=True, scale=True) + + json_element["transform"] = { + "translation": { + "x": loc[0], + "y": loc[1], + "z": loc[2] + }, + "rotation": { + "x": rot[0], + "y": rot[1], + "z": rot[2] + }, + "scale": { + "x": scl[0], + "y": scl[1], + "z": scl[2] + } + } + + json_data.append(json_element) + + json_filename = "{}.json".format(instance.name) + json_path = os.path.join(stagingdir, json_filename) + + with open(json_path, "w+") as file: + json.dump(json_data, fp=file, indent=2) + + json_representation = { + 'name': 'json', + 'ext': 'json', + 'files': json_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(json_representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, json_representation) From 3780b37b999f3a830fd74e9ca44f5d1d4adf4c1b Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 20 Jun 2022 10:32:25 +0300 Subject: [PATCH 080/432] Remove avalon-core. --- repos/avalon-core | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/avalon-core diff --git a/repos/avalon-core b/repos/avalon-core deleted file mode 160000 index 2fa14cea6f..0000000000 --- a/repos/avalon-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2fa14cea6f6a9d86eec70bbb96860cbe4c75c8eb From 2a78532eadc976274ee49b09cb568a06ed44ea60 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Mon, 20 Jun 2022 10:33:59 +0300 Subject: [PATCH 081/432] Update openpype/hosts/maya/plugins/publish/validate_render_single_camera.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../maya/plugins/publish/validate_render_single_camera.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py index 3f08e0cd62..1ca2ad42af 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -39,7 +39,9 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): if renderer.startswith('renderman'): renderer = 'renderman' - attr = RenderSettings.get_image_prefix_attr(renderer) + file_prefix = cmds.getAttr( + RenderSettings.get_image_prefix_attr(renderer) + ) file_prefix = cmds.getAttr(attr) if len(cameras) > 1: From 7e1015004c77d01a7c77c87e00be637c7c6d01c5 Mon Sep 17 00:00:00 2001 From: macman Date: Mon, 20 Jun 2022 11:29:19 +0300 Subject: [PATCH 082/432] Remove unnecessary var statement. --- .../hosts/maya/plugins/publish/validate_render_single_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py index 1ca2ad42af..35b87fd0ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -42,7 +42,7 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): file_prefix = cmds.getAttr( RenderSettings.get_image_prefix_attr(renderer) ) - file_prefix = cmds.getAttr(attr) + if len(cameras) > 1: if re.search(cls.R_CAMERA_TOKEN, file_prefix): From 8e8aa452402c965cf70dbb59dfaa87a419c405a4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Jun 2022 17:09:19 +0100 Subject: [PATCH 083/432] Implemented loading of JSON layout from Maya The JSON file is an updated version of the one currently in use for Blender. Compatibility is kept for existing JSON layouts. However, the transform data is different in Blender, Maya and Unreal. This commit works for Maya -> Unreal, but breaks Blender -> Unreal. Will fix in future commits. --- .../plugins/load/load_alembic_staticmesh.py | 13 +- .../hosts/unreal/plugins/load/load_layout.py | 121 ++++++++++++------ 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 5a73c72c64..691971e02f 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -24,7 +24,11 @@ class StaticMeshAlembicLoader(plugin.Loader): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() sm_settings = unreal.AbcStaticMeshSettings() - conversion_settings = unreal.AbcConversionSettings() + conversion_settings = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=False, flip_v=True, + rotation=[90.0, 0.0, 0.0], + scale=[1.0, -1.0, 1.0]) task.set_editor_property('filename', filename) task.set_editor_property('destination_path', asset_dir) @@ -40,13 +44,6 @@ class StaticMeshAlembicLoader(plugin.Loader): sm_settings.set_editor_property('merge_meshes', True) - conversion_settings.set_editor_property('flip_u', False) - conversion_settings.set_editor_property('flip_v', True) - conversion_settings.set_editor_property( - 'scale', unreal.Vector(x=100.0, y=100.0, z=100.0)) - conversion_settings.set_editor_property( - 'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0)) - options.static_mesh_settings = sm_settings options.conversion_settings = conversion_settings task.options = options diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c65cd25ac8..ee31d32811 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -12,6 +12,8 @@ from unreal import AssetToolsHelpers from unreal import FBXImportType from unreal import MathLibrary as umath +from bson.objectid import ObjectId + from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -196,12 +198,12 @@ class LayoutLoader(plugin.Loader): except Exception as e: print(e) actor.set_actor_rotation(unreal.Rotator( - umath.radians_to_degrees( + ( transform.get('rotation').get('x')), - -umath.radians_to_degrees( - transform.get('rotation').get('y')), - umath.radians_to_degrees( + ( transform.get('rotation').get('z')), + -( + transform.get('rotation').get('y')), ), False) actor.set_actor_scale3d(transform.get('scale')) @@ -354,7 +356,7 @@ class LayoutLoader(plugin.Loader): sec_params.set_editor_property('animation', animation) @staticmethod - def _generate_sequence(self, h, h_dir): + def _generate_sequence(h, h_dir): tools = unreal.AssetToolsHelpers().get_asset_tools() sequence = tools.create_asset( @@ -406,7 +408,7 @@ class LayoutLoader(plugin.Loader): return sequence, (min_frame, max_frame) - def _process(self, lib_path, asset_dir, sequence, loaded=None): + def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() with open(lib_path, "r") as fp: @@ -414,8 +416,8 @@ class LayoutLoader(plugin.Loader): all_loaders = discover_loader_plugins() - if not loaded: - loaded = [] + if not repr_loaded: + repr_loaded = [] path = Path(lib_path) @@ -426,36 +428,64 @@ class LayoutLoader(plugin.Loader): loaded_assets = [] for element in data: - reference = None - if element.get('reference_fbx'): - reference = element.get('reference_fbx') + representation = None + repr_format = None + if element.get('representation'): + # representation = element.get('representation') + + self.log.info(element.get("version")) + + valid_formats = ['fbx', 'abc'] + + repr_data = legacy_io.find_one({ + "type": "representation", + "parent": ObjectId(element.get("version")), + "name": {"$in": valid_formats} + }) + repr_format = repr_data.get('name') + + if not repr_data: + self.log.error( + f"No valid representation found for version " + f"{element.get('version')}") + continue + + representation = str(repr_data.get('_id')) + print(representation) + # This is to keep compatibility with old versions of the + # json format. + elif element.get('reference_fbx'): + representation = element.get('reference_fbx') + repr_format = 'fbx' elif element.get('reference_abc'): - reference = element.get('reference_abc') + representation = element.get('reference_abc') + repr_format = 'abc' # If reference is None, this element is skipped, as it cannot be # imported in Unreal - if not reference: + if not representation: continue instance_name = element.get('instance_name') skeleton = None - if reference not in loaded: - loaded.append(reference) + if representation not in repr_loaded: + repr_loaded.append(representation) family = element.get('family') loaders = loaders_from_representation( - all_loaders, reference) + all_loaders, representation) loader = None - if reference == element.get('reference_fbx'): + if repr_format == 'fbx': loader = self._get_fbx_loader(loaders, family) - elif reference == element.get('reference_abc'): + elif repr_format == 'abc': loader = self._get_abc_loader(loaders, family) if not loader: + self.log.error(f"No valid loader found for {representation}") continue options = { @@ -464,7 +494,7 @@ class LayoutLoader(plugin.Loader): assets = load_container( loader, - reference, + representation, namespace=instance_name, options=options ) @@ -482,8 +512,10 @@ class LayoutLoader(plugin.Loader): instances = [ item for item in data - if (item.get('reference_fbx') == reference or - item.get('reference_abc') == reference)] + if ((item.get('version') and + item.get('version') == element.get('version')) or + item.get('reference_fbx') == representation or + item.get('reference_abc') == representation)] for instance in instances: transform = instance.get('transform') @@ -501,9 +533,9 @@ class LayoutLoader(plugin.Loader): bindings_dict[inst] = bindings if skeleton: - skeleton_dict[reference] = skeleton + skeleton_dict[representation] = skeleton else: - skeleton = skeleton_dict.get(reference) + skeleton = skeleton_dict.get(representation) animation_file = element.get('animation') @@ -599,23 +631,26 @@ class LayoutLoader(plugin.Loader): # Create map for the shot, and create hierarchy of map. If the maps # already exist, we will use them. - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") + master_level = None + if hierarchy: + h_dir = hierarchy_dir_list[0] + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + if not EditorAssetLibrary.does_asset_exist(master_level): + EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") level = f"{asset_dir}/{asset}_map.{asset}_map" EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic - ) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(level) + if master_level: + EditorLevelLibrary.load_level(master_level) + EditorLevelUtils.add_level_to_world( + EditorLevelLibrary.get_editor_world(), + level, + unreal.LevelStreamingDynamic + ) + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(level) # Get all the sequences in the hierarchy. It will create them, if # they don't exist. @@ -664,11 +699,12 @@ class LayoutLoader(plugin.Loader): unreal.FrameRate(data.get("fps"), 1.0)) shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) - self._set_sequence_hierarchy( - sequences[-1], shot, - frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), - [level]) + if sequences: + self._set_sequence_hierarchy( + sequences[-1], shot, + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) EditorLevelLibrary.load_level(level) @@ -705,7 +741,8 @@ class LayoutLoader(plugin.Loader): for a in asset_content: EditorAssetLibrary.save_asset(a) - EditorLevelLibrary.load_level(master_level) + if master_level: + EditorLevelLibrary.load_level(master_level) return asset_content From b9f81b64ff81c74ef86698ce7e3ce69ec21485e3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Jun 2022 17:11:08 +0100 Subject: [PATCH 084/432] Hound fixes --- openpype/hosts/unreal/plugins/load/load_layout.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index ee31d32811..fb8f46dad1 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -485,7 +485,8 @@ class LayoutLoader(plugin.Loader): loader = self._get_abc_loader(loaders, family) if not loader: - self.log.error(f"No valid loader found for {representation}") + self.log.error( + f"No valid loader found for {representation}") continue options = { @@ -512,7 +513,7 @@ class LayoutLoader(plugin.Loader): instances = [ item for item in data - if ((item.get('version') and + if ((item.get('version') and item.get('version') == element.get('version')) or item.get('reference_fbx') == representation or item.get('reference_abc') == representation)] From 9c4791b169e3f867fb23857d4215bec00a5a4ec7 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Mon, 27 Jun 2022 11:41:56 +0300 Subject: [PATCH 085/432] Update openpype/settings/defaults/project_settings/maya.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 7dcefeff3f..ceac9ed814 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -35,7 +35,7 @@ "default_render_image_folder": "", "aov_separator": "underscore", "arnold_renderer": { - "image_prefix": "", + "image_prefix": "maya///_", "image_format": "exr", "multilayer_exr": true, "tiled": true, From f395e659d66f551822206e6fe202e3b7cf8485ee Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Mon, 27 Jun 2022 11:42:06 +0300 Subject: [PATCH 086/432] Update openpype/settings/defaults/project_settings/maya.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ceac9ed814..b76d0444f3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -43,7 +43,7 @@ "additional_options": {} }, "vray_renderer": { - "image_prefix": "", + "image_prefix": "maya///", "engine": "1", "image_format": "png", "aov_list": [], From ecdade9ff325bbe14285487c2b03cd24af7df472 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Mon, 27 Jun 2022 11:42:17 +0300 Subject: [PATCH 087/432] Update openpype/settings/defaults/project_settings/maya.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b76d0444f3..555c7c62a0 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -50,7 +50,7 @@ "additional_options": {} }, "redshift_renderer": { - "image_prefix": "", + "image_prefix": "maya///", "primary_gi_engine": "0", "secondary_gi_engine": "0", "image_format": "iff", From 9500e08a7d66646b07047b4bad6cf8e80bb99631 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 28 Jun 2022 16:49:50 +0200 Subject: [PATCH 088/432] update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7eaef69873..ea5b20eb69 100644 --- a/.gitignore +++ b/.gitignore @@ -102,5 +102,8 @@ website/.docusaurus .poetry/ .python-version +.editorconfig +.pre-commit-config.yaml +mypy.ini tools/run_eventserver.* From de5c4bffc46e5e2e93c6ea7b993e48d4b79da0a8 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 28 Jun 2022 16:50:22 +0200 Subject: [PATCH 089/432] adding shotgrid back to a realease --- .../plugins/publish/submit_maya_deadline.py | 1 + openpype/modules/shotgrid/README.md | 19 ++ openpype/modules/shotgrid/__init__.py | 5 + openpype/modules/shotgrid/lib/__init__.py | 0 openpype/modules/shotgrid/lib/const.py | 1 + openpype/modules/shotgrid/lib/credentials.py | 125 +++++++++++ openpype/modules/shotgrid/lib/record.py | 20 ++ openpype/modules/shotgrid/lib/settings.py | 18 ++ .../publish/collect_shotgrid_entities.py | 100 +++++++++ .../publish/collect_shotgrid_session.py | 123 +++++++++++ .../publish/integrate_shotgrid_publish.py | 77 +++++++ .../publish/integrate_shotgrid_version.py | 92 ++++++++ .../plugins/publish/validate_shotgrid_user.py | 38 ++++ openpype/modules/shotgrid/server/README.md | 5 + openpype/modules/shotgrid/shotgrid_module.py | 58 +++++ .../tests/shotgrid/lib/test_credentials.py | 34 +++ .../shotgrid/tray/credential_dialog.py | 201 ++++++++++++++++++ .../modules/shotgrid/tray/shotgrid_tray.py | 75 +++++++ openpype/resources/app_icons/shotgrid.png | Bin 0 -> 45744 bytes .../defaults/project_settings/shotgrid.json | 22 ++ .../defaults/system_settings/modules.json | 8 +- openpype/settings/entities/__init__.py | 2 + openpype/settings/entities/enum_entity.py | 114 ++++++---- .../schemas/projects_schema/schema_main.json | 4 + .../schema_project_shotgrid.json | 98 +++++++++ .../schemas/schema_representation_tags.json | 3 + .../schemas/system_schema/schema_modules.json | 54 +++++ poetry.lock | 16 ++ pyproject.toml | 1 + 29 files changed, 1276 insertions(+), 38 deletions(-) create mode 100644 openpype/modules/shotgrid/README.md create mode 100644 openpype/modules/shotgrid/__init__.py create mode 100644 openpype/modules/shotgrid/lib/__init__.py create mode 100644 openpype/modules/shotgrid/lib/const.py create mode 100644 openpype/modules/shotgrid/lib/credentials.py create mode 100644 openpype/modules/shotgrid/lib/record.py create mode 100644 openpype/modules/shotgrid/lib/settings.py create mode 100644 openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py create mode 100644 openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py create mode 100644 openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py create mode 100644 openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py create mode 100644 openpype/modules/shotgrid/plugins/publish/validate_shotgrid_user.py create mode 100644 openpype/modules/shotgrid/server/README.md create mode 100644 openpype/modules/shotgrid/shotgrid_module.py create mode 100644 openpype/modules/shotgrid/tests/shotgrid/lib/test_credentials.py create mode 100644 openpype/modules/shotgrid/tray/credential_dialog.py create mode 100644 openpype/modules/shotgrid/tray/shotgrid_tray.py create mode 100644 openpype/resources/app_icons/shotgrid.png create mode 100644 openpype/settings/defaults/project_settings/shotgrid.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 9964e3c646..dff80e62b9 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -519,6 +519,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "OPENPYPE_SG_USER", "AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK", diff --git a/openpype/modules/shotgrid/README.md b/openpype/modules/shotgrid/README.md new file mode 100644 index 0000000000..cbee0e9bf4 --- /dev/null +++ b/openpype/modules/shotgrid/README.md @@ -0,0 +1,19 @@ +## Shotgrid Module + +### Pre-requisites + +Install and launch a [shotgrid leecher](https://github.com/Ellipsanime/shotgrid-leecher) server + +### Quickstart + +The goal of this tutorial is to synchronize an already existing shotgrid project with OpenPype. + +- Activate the shotgrid module in the **system settings** and inform the shotgrid leecher server API url + +- Create a new OpenPype project with the **project manager** + +- Inform the shotgrid authentication infos (url, script name, api key) and the shotgrid project ID related to this OpenPype project in the **project settings** + +- Use the batch interface (Tray > shotgrid > Launch batch), select your project and click "batch" + +- You can now access your shotgrid entities within the **avalon launcher** and publish informations to shotgrid with **pyblish** diff --git a/openpype/modules/shotgrid/__init__.py b/openpype/modules/shotgrid/__init__.py new file mode 100644 index 0000000000..f1337a9492 --- /dev/null +++ b/openpype/modules/shotgrid/__init__.py @@ -0,0 +1,5 @@ +from .shotgrid_module import ( + ShotgridModule, +) + +__all__ = ("ShotgridModule",) diff --git a/openpype/modules/shotgrid/lib/__init__.py b/openpype/modules/shotgrid/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/modules/shotgrid/lib/const.py b/openpype/modules/shotgrid/lib/const.py new file mode 100644 index 0000000000..2a34800fac --- /dev/null +++ b/openpype/modules/shotgrid/lib/const.py @@ -0,0 +1 @@ +MODULE_NAME = "shotgrid" diff --git a/openpype/modules/shotgrid/lib/credentials.py b/openpype/modules/shotgrid/lib/credentials.py new file mode 100644 index 0000000000..337c4f6ecb --- /dev/null +++ b/openpype/modules/shotgrid/lib/credentials.py @@ -0,0 +1,125 @@ + +from urllib.parse import urlparse + +import shotgun_api3 +from shotgun_api3.shotgun import AuthenticationFault + +from openpype.lib import OpenPypeSecureRegistry, OpenPypeSettingsRegistry +from openpype.modules.shotgrid.lib.record import Credentials + + +def _get_shotgrid_secure_key(hostname, key): + """Secure item key for entered hostname.""" + return f"shotgrid/{hostname}/{key}" + + +def _get_secure_value_and_registry( + hostname, + name, +): + key = _get_shotgrid_secure_key(hostname, name) + registry = OpenPypeSecureRegistry(key) + return registry.get_item(name, None), registry + + +def get_shotgrid_hostname(shotgrid_url): + + if not shotgrid_url: + raise Exception("Shotgrid url cannot be a null") + valid_shotgrid_url = ( + f"//{shotgrid_url}" if "//" not in shotgrid_url else shotgrid_url + ) + return urlparse(valid_shotgrid_url).hostname + + +# Credentials storing function (using keyring) + + +def get_credentials(shotgrid_url): + hostname = get_shotgrid_hostname(shotgrid_url) + if not hostname: + return None + login_value, _ = _get_secure_value_and_registry( + hostname, + Credentials.login_key_prefix(), + ) + password_value, _ = _get_secure_value_and_registry( + hostname, + Credentials.password_key_prefix(), + ) + return Credentials(login_value, password_value) + + +def save_credentials(login, password, shotgrid_url): + hostname = get_shotgrid_hostname(shotgrid_url) + _, login_registry = _get_secure_value_and_registry( + hostname, + Credentials.login_key_prefix(), + ) + _, password_registry = _get_secure_value_and_registry( + hostname, + Credentials.password_key_prefix(), + ) + clear_credentials(shotgrid_url) + login_registry.set_item(Credentials.login_key_prefix(), login) + password_registry.set_item(Credentials.password_key_prefix(), password) + + +def clear_credentials(shotgrid_url): + hostname = get_shotgrid_hostname(shotgrid_url) + login_value, login_registry = _get_secure_value_and_registry( + hostname, + Credentials.login_key_prefix(), + ) + password_value, password_registry = _get_secure_value_and_registry( + hostname, + Credentials.password_key_prefix(), + ) + + if login_value is not None: + login_registry.delete_item(Credentials.login_key_prefix()) + + if password_value is not None: + password_registry.delete_item(Credentials.password_key_prefix()) + + +# Login storing function (using json) + + +def get_local_login(): + reg = OpenPypeSettingsRegistry() + try: + return str(reg.get_item("shotgrid_login")) + except Exception: + return None + + +def save_local_login(login): + reg = OpenPypeSettingsRegistry() + reg.set_item("shotgrid_login", login) + + +def clear_local_login(): + reg = OpenPypeSettingsRegistry() + reg.delete_item("shotgrid_login") + + +def check_credentials( + login, + password, + shotgrid_url, +): + + if not shotgrid_url or not login or not password: + return False + try: + session = shotgun_api3.Shotgun( + shotgrid_url, + login=login, + password=password, + ) + session.preferences_read() + session.close() + except AuthenticationFault: + return False + return True diff --git a/openpype/modules/shotgrid/lib/record.py b/openpype/modules/shotgrid/lib/record.py new file mode 100644 index 0000000000..f62f4855d5 --- /dev/null +++ b/openpype/modules/shotgrid/lib/record.py @@ -0,0 +1,20 @@ + +class Credentials: + login = None + password = None + + def __init__(self, login, password) -> None: + super().__init__() + self.login = login + self.password = password + + def is_empty(self): + return not (self.login and self.password) + + @staticmethod + def login_key_prefix(): + return "login" + + @staticmethod + def password_key_prefix(): + return "password" diff --git a/openpype/modules/shotgrid/lib/settings.py b/openpype/modules/shotgrid/lib/settings.py new file mode 100644 index 0000000000..924099f04b --- /dev/null +++ b/openpype/modules/shotgrid/lib/settings.py @@ -0,0 +1,18 @@ +from openpype.api import get_system_settings, get_project_settings +from openpype.modules.shotgrid.lib.const import MODULE_NAME + + +def get_shotgrid_project_settings(project): + return get_project_settings(project).get(MODULE_NAME, {}) + + +def get_shotgrid_settings(): + return get_system_settings().get("modules", {}).get(MODULE_NAME, {}) + + +def get_shotgrid_servers(): + return get_shotgrid_settings().get("shotgrid_settings", {}) + + +def get_leecher_backend_url(): + return get_shotgrid_settings().get("leecher_backend_url") diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py new file mode 100644 index 0000000000..0b03ac2e5d --- /dev/null +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_entities.py @@ -0,0 +1,100 @@ +import os + +import pyblish.api +from openpype.lib.mongo import OpenPypeMongoConnection + + +class CollectShotgridEntities(pyblish.api.ContextPlugin): + """Collect shotgrid entities according to the current context""" + + order = pyblish.api.CollectorOrder + 0.499 + label = "Shotgrid entities" + + def process(self, context): + + avalon_project = context.data.get("projectEntity") + avalon_asset = context.data.get("assetEntity") + avalon_task_name = os.getenv("AVALON_TASK") + + self.log.info(avalon_project) + self.log.info(avalon_asset) + + sg_project = _get_shotgrid_project(context) + sg_task = _get_shotgrid_task( + avalon_project, + avalon_asset, + avalon_task_name + ) + sg_entity = _get_shotgrid_entity(avalon_project, avalon_asset) + + if sg_project: + context.data["shotgridProject"] = sg_project + self.log.info( + "Collected correspondig shotgrid project : {}".format( + sg_project + ) + ) + + if sg_task: + context.data["shotgridTask"] = sg_task + self.log.info( + "Collected correspondig shotgrid task : {}".format(sg_task) + ) + + if sg_entity: + context.data["shotgridEntity"] = sg_entity + self.log.info( + "Collected correspondig shotgrid entity : {}".format(sg_entity) + ) + + def _find_existing_version(self, code, context): + + filters = [ + ["project", "is", context.data.get("shotgridProject")], + ["sg_task", "is", context.data.get("shotgridTask")], + ["entity", "is", context.data.get("shotgridEntity")], + ["code", "is", code], + ] + + sg = context.data.get("shotgridSession") + return sg.find_one("Version", filters, []) + + +def _get_shotgrid_collection(project): + client = OpenPypeMongoConnection.get_mongo_client() + return client.get_database("shotgrid_openpype").get_collection(project) + + +def _get_shotgrid_project(context): + shotgrid_project_id = context.data["project_settings"].get( + "shotgrid_project_id") + if shotgrid_project_id: + return {"type": "Project", "id": shotgrid_project_id} + return {} + + +def _get_shotgrid_task(avalon_project, avalon_asset, avalon_task): + sg_col = _get_shotgrid_collection(avalon_project["name"]) + shotgrid_task_hierarchy_row = sg_col.find_one( + { + "type": "Task", + "_id": {"$regex": "^" + avalon_task + "_[0-9]*"}, + "parent": {"$regex": ".*," + avalon_asset["name"] + ","}, + } + ) + if shotgrid_task_hierarchy_row: + return {"type": "Task", "id": shotgrid_task_hierarchy_row["src_id"]} + return {} + + +def _get_shotgrid_entity(avalon_project, avalon_asset): + sg_col = _get_shotgrid_collection(avalon_project["name"]) + shotgrid_entity_hierarchy_row = sg_col.find_one( + {"_id": avalon_asset["name"]} + ) + if shotgrid_entity_hierarchy_row: + return { + "type": shotgrid_entity_hierarchy_row["type"], + "id": shotgrid_entity_hierarchy_row["src_id"], + } + return {} diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py new file mode 100644 index 0000000000..9d5d2271bf --- /dev/null +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py @@ -0,0 +1,123 @@ +import os + +import pyblish.api +import shotgun_api3 +from shotgun_api3.shotgun import AuthenticationFault + +from openpype.lib import OpenPypeSettingsRegistry +from openpype.modules.shotgrid.lib.settings import ( + get_shotgrid_servers, + get_shotgrid_project_settings, +) + + +class CollectShotgridSession(pyblish.api.ContextPlugin): + """Collect shotgrid session using user credentials""" + + order = pyblish.api.CollectorOrder + label = "Shotgrid user session" + + def process(self, context): + + certificate_path = os.getenv("SHOTGUN_API_CACERTS") + if certificate_path is None or not os.path.exists(certificate_path): + self.log.info( + "SHOTGUN_API_CACERTS does not contains a valid \ + path: {}".format( + certificate_path + ) + ) + certificate_path = get_shotgrid_certificate() + self.log.info("Get Certificate from shotgrid_api") + + if not os.path.exists(certificate_path): + self.log.error( + "Could not find certificate in shotgun_api3: \ + {}".format( + certificate_path + ) + ) + return + + set_shotgrid_certificate(certificate_path) + self.log.info("Set Certificate: {}".format(certificate_path)) + + avalon_project = os.getenv("AVALON_PROJECT") + + shotgrid_settings = get_shotgrid_project_settings(avalon_project) + self.log.info("shotgrid settings: {}".format(shotgrid_settings)) + shotgrid_servers_settings = get_shotgrid_servers() + self.log.info( + "shotgrid_servers_settings: {}".format(shotgrid_servers_settings) + ) + + shotgrid_server = shotgrid_settings.get("shotgrid_server", "") + if not shotgrid_server: + self.log.error( + "No Shotgrid server found, please choose a credential" + "in script name and script key in OpenPype settings" + ) + + shotgrid_server_setting = shotgrid_servers_settings.get( + shotgrid_server, {} + ) + shotgrid_url = shotgrid_server_setting.get("shotgrid_url", "") + + shotgrid_script_name = shotgrid_server_setting.get( + "shotgrid_script_name", "" + ) + shotgrid_script_key = shotgrid_server_setting.get( + "shotgrid_script_key", "" + ) + if not shotgrid_script_name and not shotgrid_script_key: + self.log.error( + "No Shotgrid api credential found, please enter " + "script name and script key in OpenPype settings" + ) + + login = get_login() or os.getenv("OPENPYPE_SG_USER") + + if not login: + self.log.error( + "No Shotgrid login found, please " + "login to shotgrid withing openpype Tray" + ) + + session = shotgun_api3.Shotgun( + base_url=shotgrid_url, + script_name=shotgrid_script_name, + api_key=shotgrid_script_key, + sudo_as_login=login, + ) + + try: + session.preferences_read() + except AuthenticationFault: + raise ValueError( + "Could not connect to shotgrid {} with user {}".format( + shotgrid_url, login + ) + ) + + self.log.info( + "Logged to shotgrid {} with user {}".format(shotgrid_url, login) + ) + context.data["shotgridSession"] = session + context.data["shotgridUser"] = login + + +def get_shotgrid_certificate(): + shotgun_api_path = os.path.dirname(shotgun_api3.__file__) + return os.path.join(shotgun_api_path, "lib", "certifi", "cacert.pem") + + +def set_shotgrid_certificate(certificate): + os.environ["SHOTGUN_API_CACERTS"] = certificate + + +def get_login(): + reg = OpenPypeSettingsRegistry() + try: + return str(reg.get_item("shotgrid_login")) + except Exception: + return None diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py new file mode 100644 index 0000000000..cfd2d10fd9 --- /dev/null +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -0,0 +1,77 @@ +import os +import pyblish.api + + +class IntegrateShotgridPublish(pyblish.api.InstancePlugin): + """ + Create published Files from representations and add it to version. If + representation is tagged add shotgrid review, it will add it in + path to movie for a movie file or path to frame for an image sequence. + """ + + order = pyblish.api.IntegratorOrder + 0.499 + label = "Shotgrid Published Files" + + def process(self, instance): + + context = instance.context + + self.sg = context.data.get("shotgridSession") + + shotgrid_version = instance.data.get("shotgridVersion") + + for representation in instance.data.get("representations", []): + + local_path = representation.get("published_path") + code = os.path.basename(local_path) + + if representation.get("tags", []): + continue + + published_file = self._find_existing_publish( + code, context, shotgrid_version + ) + + published_file_data = { + "project": context.data.get("shotgridProject"), + "code": code, + "entity": context.data.get("shotgridEntity"), + "task": context.data.get("shotgridTask"), + "version": shotgrid_version, + "path": {"local_path": local_path}, + } + if not published_file: + published_file = self._create_published(published_file_data) + self.log.info( + "Create Shotgrid PublishedFile: {}".format(published_file) + ) + else: + self.sg.update( + published_file["type"], + published_file["id"], + published_file_data, + ) + self.log.info( + "Update Shotgrid PublishedFile: {}".format(published_file) + ) + + if instance.data["family"] == "image": + self.sg.upload_thumbnail( + published_file["type"], published_file["id"], local_path + ) + instance.data["shotgridPublishedFile"] = published_file + + def _find_existing_publish(self, code, context, shotgrid_version): + + filters = [ + ["project", "is", context.data.get("shotgridProject")], + ["task", "is", context.data.get("shotgridTask")], + ["entity", "is", context.data.get("shotgridEntity")], + ["version", "is", shotgrid_version], + ["code", "is", code], + ] + return self.sg.find_one("PublishedFile", filters, []) + + def _create_published(self, published_file_data): + + return self.sg.create("PublishedFile", published_file_data) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py new file mode 100644 index 0000000000..a1b7140e22 --- /dev/null +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -0,0 +1,92 @@ +import os +import pyblish.api + + +class IntegrateShotgridVersion(pyblish.api.InstancePlugin): + """Integrate Shotgrid Version""" + + order = pyblish.api.IntegratorOrder + 0.497 + label = "Shotgrid Version" + + sg = None + + def process(self, instance): + + context = instance.context + self.sg = context.data.get("shotgridSession") + + # TODO: Use path template solver to build version code from settings + anatomy = instance.data.get("anatomyData", {}) + code = "_".join( + [ + anatomy["project"]["code"], + anatomy["parent"], + anatomy["asset"], + anatomy["task"]["name"], + "v{:03}".format(int(anatomy["version"])), + ] + ) + + version = self._find_existing_version(code, context) + + if not version: + version = self._create_version(code, context) + self.log.info("Create Shotgrid version: {}".format(version)) + else: + self.log.info("Use existing Shotgrid version: {}".format(version)) + + data_to_update = {} + status = context.data.get("intent", {}).get("value") + if status: + data_to_update["sg_status_list"] = status + + for representation in instance.data.get("representations", []): + local_path = representation.get("published_path") + code = os.path.basename(local_path) + + if "shotgridreview" in representation.get("tags", []): + + if representation["ext"] in ["mov", "avi"]: + self.log.info( + "Upload review: {} for version shotgrid {}".format( + local_path, version.get("id") + ) + ) + self.sg.upload( + "Version", + version.get("id"), + local_path, + field_name="sg_uploaded_movie", + ) + + data_to_update["sg_path_to_movie"] = local_path + + elif representation["ext"] in ["jpg", "png", "exr", "tga"]: + path_to_frame = local_path.replace("0000", "#") + data_to_update["sg_path_to_frames"] = path_to_frame + + self.log.info("Update Shotgrid version with {}".format(data_to_update)) + self.sg.update("Version", version["id"], data_to_update) + + instance.data["shotgridVersion"] = version + + def _find_existing_version(self, code, context): + + filters = [ + ["project", "is", context.data.get("shotgridProject")], + ["sg_task", "is", context.data.get("shotgridTask")], + ["entity", "is", context.data.get("shotgridEntity")], + ["code", "is", code], + ] + return self.sg.find_one("Version", filters, []) + + def _create_version(self, code, context): + + version_data = { + "project": context.data.get("shotgridProject"), + "sg_task": context.data.get("shotgridTask"), + "entity": context.data.get("shotgridEntity"), + "code": code, + } + + return self.sg.create("Version", version_data) diff --git a/openpype/modules/shotgrid/plugins/publish/validate_shotgrid_user.py b/openpype/modules/shotgrid/plugins/publish/validate_shotgrid_user.py new file mode 100644 index 0000000000..c14c980e2a --- /dev/null +++ b/openpype/modules/shotgrid/plugins/publish/validate_shotgrid_user.py @@ -0,0 +1,38 @@ +import pyblish.api +import openpype.api + + +class ValidateShotgridUser(pyblish.api.ContextPlugin): + """ + Check if user is valid and have access to the project. + """ + + label = "Validate Shotgrid User" + order = openpype.api.ValidateContentsOrder + + def process(self, context): + sg = context.data.get("shotgridSession") + + login = context.data.get("shotgridUser") + self.log.info("Login shotgrid set in OpenPype is {}".format(login)) + project = context.data.get("shotgridProject") + self.log.info("Current shotgun project is {}".format(project)) + + if not (login and sg and project): + raise KeyError() + + user = sg.find_one("HumanUser", [["login", "is", login]], ["projects"]) + + self.log.info(user) + self.log.info(login) + user_projects_id = [p["id"] for p in user.get("projects", [])] + if not project.get("id") in user_projects_id: + raise PermissionError( + "Login {} don't have access to the project {}".format( + login, project + ) + ) + + self.log.info( + "Login {} have access to the project {}".format(login, project) + ) diff --git a/openpype/modules/shotgrid/server/README.md b/openpype/modules/shotgrid/server/README.md new file mode 100644 index 0000000000..15e056ff3e --- /dev/null +++ b/openpype/modules/shotgrid/server/README.md @@ -0,0 +1,5 @@ + +### Shotgrid server + +Please refer to the external project that covers Openpype/Shotgrid communication: + - https://github.com/Ellipsanime/shotgrid-leecher diff --git a/openpype/modules/shotgrid/shotgrid_module.py b/openpype/modules/shotgrid/shotgrid_module.py new file mode 100644 index 0000000000..5644f0c35f --- /dev/null +++ b/openpype/modules/shotgrid/shotgrid_module.py @@ -0,0 +1,58 @@ +import os + +from openpype_interfaces import ( + ITrayModule, + IPluginPaths, + ILaunchHookPaths, +) + +from openpype.modules import OpenPypeModule + +SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class ShotgridModule( + OpenPypeModule, ITrayModule, IPluginPaths, ILaunchHookPaths +): + leecher_manager_url = None + name = "shotgrid" + enabled = False + project_id = None + tray_wrapper = None + + def initialize(self, modules_settings): + shotgrid_settings = modules_settings.get(self.name, dict()) + self.enabled = shotgrid_settings.get("enabled", False) + self.leecher_manager_url = shotgrid_settings.get( + "leecher_manager_url", "" + ) + + def connect_with_modules(self, enabled_modules): + pass + + def get_global_environments(self): + return {"PROJECT_ID": self.project_id} + + def get_plugin_paths(self): + return { + "publish": [ + os.path.join(SHOTGRID_MODULE_DIR, "plugins", "publish") + ] + } + + def get_launch_hook_paths(self): + return os.path.join(SHOTGRID_MODULE_DIR, "hooks") + + def tray_init(self): + from .tray.shotgrid_tray import ShotgridTrayWrapper + + self.tray_wrapper = ShotgridTrayWrapper(self) + + def tray_start(self): + return self.tray_wrapper.validate() + + def tray_exit(self, *args, **kwargs): + return self.tray_wrapper + + def tray_menu(self, tray_menu): + return self.tray_wrapper.tray_menu(tray_menu) diff --git a/openpype/modules/shotgrid/tests/shotgrid/lib/test_credentials.py b/openpype/modules/shotgrid/tests/shotgrid/lib/test_credentials.py new file mode 100644 index 0000000000..1f78cf77c9 --- /dev/null +++ b/openpype/modules/shotgrid/tests/shotgrid/lib/test_credentials.py @@ -0,0 +1,34 @@ +import pytest +from assertpy import assert_that + +import openpype.modules.shotgrid.lib.credentials as sut + + +def test_missing_shotgrid_url(): + with pytest.raises(Exception) as ex: + # arrange + url = "" + # act + sut.get_shotgrid_hostname(url) + # assert + assert_that(ex).is_equal_to("Shotgrid url cannot be a null") + + +def test_full_shotgrid_url(): + # arrange + url = "https://shotgrid.com/myinstance" + # act + actual = sut.get_shotgrid_hostname(url) + # assert + assert_that(actual).is_not_empty() + assert_that(actual).is_equal_to("shotgrid.com") + + +def test_incomplete_shotgrid_url(): + # arrange + url = "shotgrid.com/myinstance" + # act + actual = sut.get_shotgrid_hostname(url) + # assert + assert_that(actual).is_not_empty() + assert_that(actual).is_equal_to("shotgrid.com") diff --git a/openpype/modules/shotgrid/tray/credential_dialog.py b/openpype/modules/shotgrid/tray/credential_dialog.py new file mode 100644 index 0000000000..9d841d98be --- /dev/null +++ b/openpype/modules/shotgrid/tray/credential_dialog.py @@ -0,0 +1,201 @@ +import os +from Qt import QtCore, QtWidgets, QtGui + +from openpype import style +from openpype import resources +from openpype.modules.shotgrid.lib import settings, credentials + + +class CredentialsDialog(QtWidgets.QDialog): + SIZE_W = 450 + SIZE_H = 200 + + _module = None + _is_logged = False + url_label = None + login_label = None + password_label = None + url_input = None + login_input = None + password_input = None + input_layout = None + login_button = None + buttons_layout = None + main_widget = None + + login_changed = QtCore.Signal() + + def __init__(self, module, parent=None): + super(CredentialsDialog, self).__init__(parent) + + self._module = module + self._is_logged = False + + self.setWindowTitle("OpenPype - Shotgrid Login") + + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowMinimizeButtonHint + ) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100)) + self.setStyleSheet(style.load_stylesheet()) + + self.ui_init() + + def ui_init(self): + self.url_label = QtWidgets.QLabel("Shotgrid server:") + self.login_label = QtWidgets.QLabel("Login:") + self.password_label = QtWidgets.QLabel("Password:") + + self.url_input = QtWidgets.QComboBox() + # self.url_input.setReadOnly(True) + + self.login_input = QtWidgets.QLineEdit() + self.login_input.setPlaceholderText("login") + + self.password_input = QtWidgets.QLineEdit() + self.password_input.setPlaceholderText("password") + self.password_input.setEchoMode(QtWidgets.QLineEdit.Password) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setStyleSheet("color: red;") + self.error_label.setWordWrap(True) + self.error_label.hide() + + self.input_layout = QtWidgets.QFormLayout() + self.input_layout.setContentsMargins(10, 15, 10, 5) + + self.input_layout.addRow(self.url_label, self.url_input) + self.input_layout.addRow(self.login_label, self.login_input) + self.input_layout.addRow(self.password_label, self.password_input) + self.input_layout.addRow(self.error_label) + + self.login_button = QtWidgets.QPushButton("Login") + self.login_button.setToolTip("Log in shotgrid instance") + self.login_button.clicked.connect(self._on_shotgrid_login_clicked) + + self.logout_button = QtWidgets.QPushButton("Logout") + self.logout_button.setToolTip("Log out shotgrid instance") + self.logout_button.clicked.connect(self._on_shotgrid_logout_clicked) + + self.buttons_layout = QtWidgets.QHBoxLayout() + self.buttons_layout.addWidget(self.logout_button) + self.buttons_layout.addWidget(self.login_button) + + self.main_widget = QtWidgets.QVBoxLayout(self) + self.main_widget.addLayout(self.input_layout) + self.main_widget.addLayout(self.buttons_layout) + self.setLayout(self.main_widget) + + def show(self, *args, **kwargs): + super(CredentialsDialog, self).show(*args, **kwargs) + self._fill_shotgrid_url() + self._fill_shotgrid_login() + + def _fill_shotgrid_url(self): + servers = settings.get_shotgrid_servers() + + if servers: + for _, v in servers.items(): + self.url_input.addItem("{}".format(v.get('shotgrid_url'))) + self._valid_input(self.url_input) + self.login_button.show() + self.logout_button.show() + enabled = True + else: + self.set_error("Ask your admin to add shotgrid server in settings") + self._invalid_input(self.url_input) + self.login_button.hide() + self.logout_button.hide() + enabled = False + + self.login_input.setEnabled(enabled) + self.password_input.setEnabled(enabled) + + def _fill_shotgrid_login(self): + login = credentials.get_local_login() + + if login: + self.login_input.setText(login) + + def _clear_shotgrid_login(self): + self.login_input.setText("") + self.password_input.setText("") + + def _on_shotgrid_login_clicked(self): + login = self.login_input.text().strip() + password = self.password_input.text().strip() + missing = [] + + if login == "": + missing.append("login") + self._invalid_input(self.login_input) + + if password == "": + missing.append("password") + self._invalid_input(self.password_input) + + url = self.url_input.currentText() + if url == "": + missing.append("url") + self._invalid_input(self.url_input) + + if len(missing) > 0: + self.set_error("You didn't enter {}".format(" and ".join(missing))) + return + + # if credentials.check_credentials( + # login=login, + # password=password, + # shotgrid_url=url, + # ): + credentials.save_local_login( + login=login + ) + os.environ['OPENPYPE_SG_USER'] = login + self._on_login() + + self.set_error("CANT LOGIN") + + def _on_shotgrid_logout_clicked(self): + credentials.clear_local_login() + del os.environ['OPENPYPE_SG_USER'] + self._clear_shotgrid_login() + self._on_logout() + + def set_error(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def _on_login(self): + self._is_logged = True + self.login_changed.emit() + self._close_widget() + + def _on_logout(self): + self._is_logged = False + self.login_changed.emit() + + def _close_widget(self): + self.hide() + + def _valid_input(self, input_widget): + input_widget.setStyleSheet("") + + def _invalid_input(self, input_widget): + input_widget.setStyleSheet("border: 1px solid red;") + + def login_with_credentials( + self, url, login, password + ): + verification = credentials.check_credentials(url, login, password) + if verification: + credentials.save_credentials(login, password, False) + self._module.set_credentials_to_env(login, password) + self.set_credentials(login, password) + self.login_changed.emit() + return verification diff --git a/openpype/modules/shotgrid/tray/shotgrid_tray.py b/openpype/modules/shotgrid/tray/shotgrid_tray.py new file mode 100644 index 0000000000..4038d77b03 --- /dev/null +++ b/openpype/modules/shotgrid/tray/shotgrid_tray.py @@ -0,0 +1,75 @@ +import os +import webbrowser + +from Qt import QtWidgets + +from openpype.modules.shotgrid.lib import credentials +from openpype.modules.shotgrid.tray.credential_dialog import ( + CredentialsDialog, +) + + +class ShotgridTrayWrapper: + module = None + credentials_dialog = None + logged_user_label = None + + def __init__(self, module): + self.module = module + self.credentials_dialog = CredentialsDialog(module) + self.credentials_dialog.login_changed.connect(self.set_login_label) + self.logged_user_label = QtWidgets.QAction("") + self.logged_user_label.setDisabled(True) + self.set_login_label() + + def show_batch_dialog(self): + if self.module.leecher_manager_url: + webbrowser.open(self.module.leecher_manager_url) + + def show_connect_dialog(self): + self.show_credential_dialog() + + def show_credential_dialog(self): + self.credentials_dialog.show() + self.credentials_dialog.activateWindow() + self.credentials_dialog.raise_() + + def set_login_label(self): + login = credentials.get_local_login() + if login: + self.logged_user_label.setText("{}".format(login)) + else: + self.logged_user_label.setText( + "No User logged in {0}".format(login) + ) + + def tray_menu(self, tray_menu): + # Add login to user menu + menu = QtWidgets.QMenu("Shotgrid", tray_menu) + show_connect_action = QtWidgets.QAction("Connect to Shotgrid", menu) + show_connect_action.triggered.connect(self.show_connect_dialog) + menu.addAction(self.logged_user_label) + menu.addSeparator() + menu.addAction(show_connect_action) + tray_menu.addMenu(menu) + + # Add manager to Admin menu + for m in tray_menu.findChildren(QtWidgets.QMenu): + if m.title() == "Admin": + shotgrid_manager_action = QtWidgets.QAction( + "Shotgrid manager", menu + ) + shotgrid_manager_action.triggered.connect( + self.show_batch_dialog + ) + m.addAction(shotgrid_manager_action) + + def validate(self): + login = credentials.get_local_login() + + if not login: + self.show_credential_dialog() + else: + os.environ["OPENPYPE_SG_USER"] = login + + return True diff --git a/openpype/resources/app_icons/shotgrid.png b/openpype/resources/app_icons/shotgrid.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0cc047f9ed86e0db45ea557404ab7edb2f5bf2 GIT binary patch literal 45744 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 N8$~sR3OTc&{{b(fTU!7C literal 0 HcmV?d00001 diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json new file mode 100644 index 0000000000..83b6f69074 --- /dev/null +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -0,0 +1,22 @@ +{ + "shotgrid_project_id": 0, + "shotgrid_server": "", + "event": { + "enabled": false + }, + "fields": { + "asset": { + "type": "sg_asset_type" + }, + "sequence": { + "episode_link": "episode" + }, + "shot": { + "episode_link": "sg_episode", + "sequence_link": "sg_sequence" + }, + "task": { + "step": "step" + } + } +} diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 8cd4114cb0..9d8910689a 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -131,6 +131,12 @@ } } }, + "shotgrid": { + "enabled": false, + "leecher_manager_url": "http://127.0.0.1:3000", + "leecher_backend_url": "http://127.0.0.1:8090", + "shotgrid_settings": {} + }, "kitsu": { "enabled": false, "server": "" @@ -203,4 +209,4 @@ "linux": "" } } -} \ No newline at end of file +} diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index a173e2454f..b2cb2204f4 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -107,6 +107,7 @@ from .enum_entity import ( TaskTypeEnumEntity, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity, + ShotgridUrlEnumEntity ) from .list_entity import ListEntity @@ -171,6 +172,7 @@ __all__ = ( "ToolsEnumEntity", "TaskTypeEnumEntity", "DeadlineUrlEnumEntity", + "ShotgridUrlEnumEntity", "AnatomyTemplatesEnumEntity", "ListEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 92a397afba..3b3dd47e61 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,10 +1,7 @@ import copy from .input_entities import InputEntity from .exceptions import EntitySchemaError -from .lib import ( - NOT_SET, - STRING_TYPE -) +from .lib import NOT_SET, STRING_TYPE class BaseEnumEntity(InputEntity): @@ -26,7 +23,7 @@ class BaseEnumEntity(InputEntity): for item in self.enum_items: key = tuple(item.keys())[0] if key in enum_keys: - reason = "Key \"{}\" is more than once in enum items.".format( + reason = 'Key "{}" is more than once in enum items.'.format( key ) raise EntitySchemaError(self, reason) @@ -34,7 +31,7 @@ class BaseEnumEntity(InputEntity): enum_keys.add(key) if not isinstance(key, STRING_TYPE): - reason = "Key \"{}\" has invalid type {}, expected {}.".format( + reason = 'Key "{}" has invalid type {}, expected {}.'.format( key, type(key), STRING_TYPE ) raise EntitySchemaError(self, reason) @@ -59,7 +56,7 @@ class BaseEnumEntity(InputEntity): for item in check_values: if item not in self.valid_keys: raise ValueError( - "{} Invalid value \"{}\". Expected one of: {}".format( + '{} Invalid value "{}". Expected one of: {}'.format( self.path, item, self.valid_keys ) ) @@ -84,7 +81,7 @@ class EnumEntity(BaseEnumEntity): self.valid_keys = set(all_keys) if self.multiselection: - self.valid_value_types = (list, ) + self.valid_value_types = (list,) value_on_not_set = [] if enum_default: if not isinstance(enum_default, list): @@ -109,7 +106,7 @@ class EnumEntity(BaseEnumEntity): self.value_on_not_set = key break - self.valid_value_types = (STRING_TYPE, ) + self.valid_value_types = (STRING_TYPE,) # GUI attribute self.placeholder = self.schema_data.get("placeholder") @@ -152,6 +149,7 @@ class HostsEnumEntity(BaseEnumEntity): Host name is not the same as application name. Host name defines implementation instead of application name. """ + schema_types = ["hosts-enum"] all_host_names = [ "aftereffects", @@ -169,7 +167,7 @@ class HostsEnumEntity(BaseEnumEntity): "tvpaint", "unreal", "standalonepublisher", - "webpublisher" + "webpublisher", ] def _item_initialization(self): @@ -210,7 +208,7 @@ class HostsEnumEntity(BaseEnumEntity): self.valid_keys = valid_keys if self.multiselection: - self.valid_value_types = (list, ) + self.valid_value_types = (list,) self.value_on_not_set = [] else: for key in valid_keys: @@ -218,7 +216,7 @@ class HostsEnumEntity(BaseEnumEntity): self.value_on_not_set = key break - self.valid_value_types = (STRING_TYPE, ) + self.valid_value_types = (STRING_TYPE,) # GUI attribute self.placeholder = self.schema_data.get("placeholder") @@ -226,14 +224,10 @@ class HostsEnumEntity(BaseEnumEntity): def schema_validations(self): if self.hosts_filter: enum_len = len(self.enum_items) - if ( - enum_len == 0 - or (enum_len == 1 and self.use_empty_value) - ): - joined_filters = ", ".join([ - '"{}"'.format(item) - for item in self.hosts_filter - ]) + if enum_len == 0 or (enum_len == 1 and self.use_empty_value): + joined_filters = ", ".join( + ['"{}"'.format(item) for item in self.hosts_filter] + ) reason = ( "All host names were removed after applying" " host filters. {}" @@ -246,24 +240,25 @@ class HostsEnumEntity(BaseEnumEntity): invalid_filters.add(item) if invalid_filters: - joined_filters = ", ".join([ - '"{}"'.format(item) - for item in self.hosts_filter - ]) - expected_hosts = ", ".join([ - '"{}"'.format(item) - for item in self.all_host_names - ]) - self.log.warning(( - "Host filters containt invalid host names:" - " \"{}\" Expected values are {}" - ).format(joined_filters, expected_hosts)) + joined_filters = ", ".join( + ['"{}"'.format(item) for item in self.hosts_filter] + ) + expected_hosts = ", ".join( + ['"{}"'.format(item) for item in self.all_host_names] + ) + self.log.warning( + ( + "Host filters containt invalid host names:" + ' "{}" Expected values are {}' + ).format(joined_filters, expected_hosts) + ) super(HostsEnumEntity, self).schema_validations() class AppsEnumEntity(BaseEnumEntity): """Enum of applications for project anatomy attributes.""" + schema_types = ["apps-enum"] def _item_initialization(self): @@ -271,7 +266,7 @@ class AppsEnumEntity(BaseEnumEntity): self.value_on_not_set = [] self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (list, ) + self.valid_value_types = (list,) self.placeholder = None def _get_enum_values(self): @@ -352,7 +347,7 @@ class ToolsEnumEntity(BaseEnumEntity): self.value_on_not_set = [] self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (list, ) + self.valid_value_types = (list,) self.placeholder = None def _get_enum_values(self): @@ -409,10 +404,10 @@ class TaskTypeEnumEntity(BaseEnumEntity): def _item_initialization(self): self.multiselection = self.schema_data.get("multiselection", True) if self.multiselection: - self.valid_value_types = (list, ) + self.valid_value_types = (list,) self.value_on_not_set = [] else: - self.valid_value_types = (STRING_TYPE, ) + self.valid_value_types = (STRING_TYPE,) self.value_on_not_set = "" self.enum_items = [] @@ -507,7 +502,8 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): enum_items_list = [] for server_name, url_entity in deadline_urls_entity.items(): enum_items_list.append( - {server_name: "{}: {}".format(server_name, url_entity.value)}) + {server_name: "{}: {}".format(server_name, url_entity.value)} + ) valid_keys.add(server_name) return enum_items_list, valid_keys @@ -530,6 +526,50 @@ class DeadlineUrlEnumEntity(BaseEnumEntity): self._current_value = tuple(self.valid_keys)[0] +class ShotgridUrlEnumEntity(BaseEnumEntity): + schema_types = ["shotgrid_url-enum"] + + def _item_initialization(self): + self.multiselection = False + + self.enum_items = [] + self.valid_keys = set() + + self.valid_value_types = (STRING_TYPE,) + self.value_on_not_set = "" + + # GUI attribute + self.placeholder = self.schema_data.get("placeholder") + + def _get_enum_values(self): + shotgrid_settings = self.get_entity_from_path( + "system_settings/modules/shotgrid/shotgrid_settings" + ) + + valid_keys = set() + enum_items_list = [] + for server_name, settings in shotgrid_settings.items(): + enum_items_list.append( + { + server_name: "{}: {}".format( + server_name, settings["shotgrid_url"].value + ) + } + ) + valid_keys.add(server_name) + return enum_items_list, valid_keys + + def set_override_state(self, *args, **kwargs): + super(ShotgridUrlEnumEntity, self).set_override_state(*args, **kwargs) + + self.enum_items, self.valid_keys = self._get_enum_values() + if not self.valid_keys: + self._current_value = "" + + elif self._current_value not in self.valid_keys: + self._current_value = tuple(self.valid_keys)[0] + + class AnatomyTemplatesEnumEntity(BaseEnumEntity): schema_types = ["anatomy-templates-enum"] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 6c07209de3..80b1baad1b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -62,6 +62,10 @@ "type": "schema", "name": "schema_project_ftrack" }, + { + "type": "schema", + "name": "schema_project_shotgrid" + }, { "type": "schema", "name": "schema_project_kitsu" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json b/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json new file mode 100644 index 0000000000..4faeca89f3 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_shotgrid.json @@ -0,0 +1,98 @@ +{ + "type": "dict", + "key": "shotgrid", + "label": "Shotgrid", + "collapsible": true, + "is_file": true, + "children": [ + { + "type": "number", + "key": "shotgrid_project_id", + "label": "Shotgrid project id" + }, + { + "type": "shotgrid_url-enum", + "key": "shotgrid_server", + "label": "Shotgrid Server" + }, + { + "type": "dict", + "key": "event", + "label": "Event Handler", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, + { + "type": "dict", + "key": "fields", + "label": "Fields Template", + "collapsible": true, + "children": [ + { + "type": "dict", + "key": "asset", + "label": "Asset", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "type", + "label": "Asset Type" + } + ] + }, + { + "type": "dict", + "key": "sequence", + "label": "Sequence", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "episode_link", + "label": "Episode link" + } + ] + }, + { + "type": "dict", + "key": "shot", + "label": "Shot", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "episode_link", + "label": "Episode link" + }, + { + "type": "text", + "key": "sequence_link", + "label": "Sequence link" + } + ] + }, + { + "type": "dict", + "key": "task", + "label": "Task", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "step", + "label": "Step link" + } + ] + } + ] + } + ] +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json index 484fbf9d07..a4b28f47bc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json @@ -13,6 +13,9 @@ { "ftrackreview": "Add review to Ftrack" }, + { + "shotgridreview": "Add review to Shotgrid" + }, { "delete": "Delete output" }, diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index d22b9016a7..952b38040c 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -48,6 +48,60 @@ "type": "schema", "name": "schema_kitsu" }, + { + "type": "dict", + "key": "shotgrid", + "label": "Shotgrid", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "leecher_manager_url", + "label": "Shotgrid Leecher Manager URL" + }, + { + "type": "text", + "key": "leecher_backend_url", + "label": "Shotgrid Leecher Backend URL" + }, + { + "type": "boolean", + "key": "filter_projects_by_login", + "label": "Filter projects by SG login" + }, + { + "type": "dict-modifiable", + "key": "shotgrid_settings", + "label": "Shotgrid Servers", + "object_type": { + "type": "dict", + "children": [ + { + "key": "shotgrid_url", + "label": "Server URL", + "type": "text" + }, + { + "key": "shotgrid_script_name", + "label": "Script Name", + "type": "text" + }, + { + "key": "shotgrid_script_key", + "label": "Script api key", + "type": "text" + } + ] + } + } + ] + }, { "type": "dict", "key": "timers_manager", diff --git a/poetry.lock b/poetry.lock index 7221e191ff..0033bc0d73 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1375,6 +1375,21 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "shotgun-api3" +version = "3.3.3" +description = "Shotgun Python API" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/shotgunsoftware/python-api.git" +reference = "v3.3.3" +resolved_reference = "b9f066c0edbea6e0733242e18f32f75489064840" + [[package]] name = "six" version = "1.16.0" @@ -2820,6 +2835,7 @@ semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] +shotgun-api3 = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index bd5d3ad89d..306c7206fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ clique = "1.6.*" Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "2.0.*" +shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} gazu = "^0.8.28" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^2.6.0" From 0353ec38f3593c9e81a9b82a735ab7868e406d9f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 1 Jul 2022 11:06:44 +0100 Subject: [PATCH 090/432] Generalization of the basis of the origin platform in the JSON layout --- .../blender/plugins/publish/extract_layout.py | 14 ++++- .../maya/plugins/publish/extract_layout.py | 50 +++++++++++++-- .../plugins/load/load_alembic_staticmesh.py | 6 +- .../hosts/unreal/plugins/load/load_layout.py | 63 ++++++++++--------- 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 8ecc78a2c6..f987dffe05 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -193,7 +193,7 @@ class ExtractLayout(openpype.api.Extractor): "rotation": { "x": asset.rotation_euler.x, "y": asset.rotation_euler.y, - "z": asset.rotation_euler.z, + "z": asset.rotation_euler.z }, "scale": { "x": asset.scale.x, @@ -202,6 +202,18 @@ class ExtractLayout(openpype.api.Extractor): } } + json_element["transform_matrix"] = [] + + for row in list(asset.matrix_world.transposed()): + json_element["transform_matrix"].append(list(row)) + + json_element["basis"] = [ + [1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + # Extract the animation as well if family == "rig": f, n = self._export_animation( diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index 4ae99f1052..7eb6a64e6d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -1,7 +1,9 @@ +import math import os import json from maya import cmds +from maya.api import OpenMaya as om from bson.objectid import ObjectId @@ -60,7 +62,7 @@ class ExtractLayout(openpype.api.Extractor): } loc = cmds.xform(asset, query=True, translation=True) - rot = cmds.xform(asset, query=True, rotation=True) + rot = cmds.xform(asset, query=True, rotation=True, euler=True) scl = cmds.xform(asset, query=True, relative=True, scale=True) json_element["transform"] = { @@ -70,9 +72,9 @@ class ExtractLayout(openpype.api.Extractor): "z": loc[2] }, "rotation": { - "x": rot[0], - "y": rot[1], - "z": rot[2] + "x": math.radians(rot[0]), + "y": math.radians(rot[1]), + "z": math.radians(rot[2]) }, "scale": { "x": scl[0], @@ -81,6 +83,46 @@ class ExtractLayout(openpype.api.Extractor): } } + row_length = 4 + t_matrix_list = cmds.xform(asset, query=True, matrix=True) + + transform_mm = om.MMatrix(t_matrix_list) + transform = om.MTransformationMatrix(transform_mm) + + transform.scaleBy([1.0, 1.0, -1.0], om.MSpace.kWorld) + transform.rotateBy( + om.MEulerRotation(math.radians(-90), 0, 0), om.MSpace.kWorld) + + t_matrix_list = list(transform.asMatrix()) + + t_matrix = [] + for i in range(0, len(t_matrix_list), row_length): + t_matrix.append(t_matrix_list[i:i + row_length]) + + json_element["transform_matrix"] = [] + for row in t_matrix: + json_element["transform_matrix"].append(list(row)) + + basis_list = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1 + ] + + basis_mm = om.MMatrix(basis_list) + basis = om.MTransformationMatrix(basis_mm) + + b_matrix_list = list(basis.asMatrix()) + b_matrix = [] + + for i in range(0, len(b_matrix_list), row_length): + b_matrix.append(b_matrix_list[i:i + row_length]) + + json_element["basis"] = [] + for row in b_matrix: + json_element["basis"].append(list(row)) + json_data.append(json_element) json_filename = "{}.json".format(instance.name) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 691971e02f..42abbda80f 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -26,9 +26,9 @@ class StaticMeshAlembicLoader(plugin.Loader): sm_settings = unreal.AbcStaticMeshSettings() conversion_settings = unreal.AbcConversionSettings( preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=True, - rotation=[90.0, 0.0, 0.0], - scale=[1.0, -1.0, 1.0]) + flip_u=False, flip_v=False, + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0]) task.set_editor_property('filename', filename) task.set_editor_property('destination_path', asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index fb8f46dad1..361c3684fa 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Loader for layouts.""" -import os import json from pathlib import Path @@ -170,9 +169,29 @@ class LayoutLoader(plugin.Loader): hid_section.set_row_index(index) hid_section.set_level_names(maps) - @staticmethod + def _transform_from_basis(self, transform, basis): + """Transform a transform from a basis to a new basis.""" + # Get the basis matrix + basis_matrix = unreal.Matrix( + basis[0], + basis[1], + basis[2], + basis[3] + ) + transform_matrix = unreal.Matrix( + transform[0], + transform[1], + transform[2], + transform[3] + ) + + new_transform = ( + basis_matrix.get_inverse() * transform_matrix * basis_matrix) + + return new_transform.transform() + def _process_family( - assets, class_name, transform, sequence, inst_name=None + self, assets, class_name, transform, basis, sequence, inst_name=None ): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -182,30 +201,12 @@ class LayoutLoader(plugin.Loader): for asset in assets: obj = ar.get_asset_by_object_path(asset).get_asset() if obj.get_class().get_name() == class_name: + t = self._transform_from_basis(transform, basis) actor = EditorLevelLibrary.spawn_actor_from_object( - obj, - transform.get('translation') + obj, t.translation ) - if inst_name: - try: - # Rename method leads to crash - # actor.rename(name=inst_name) - - # The label works, although it make it slightly more - # complicated to check for the names, as we need to - # loop through all the actors in the level - actor.set_actor_label(inst_name) - except Exception as e: - print(e) - actor.set_actor_rotation(unreal.Rotator( - ( - transform.get('rotation').get('x')), - ( - transform.get('rotation').get('z')), - -( - transform.get('rotation').get('y')), - ), False) - actor.set_actor_scale3d(transform.get('scale')) + actor.set_actor_rotation(t.rotation.rotator(), False) + actor.set_actor_scale3d(t.scale3d) if class_name == 'SkeletalMesh': skm_comp = actor.get_editor_property( @@ -519,17 +520,23 @@ class LayoutLoader(plugin.Loader): item.get('reference_abc') == representation)] for instance in instances: - transform = instance.get('transform') + # transform = instance.get('transform') + transform = instance.get('transform_matrix') + basis = instance.get('basis') inst = instance.get('instance_name') actors = [] if family == 'model': actors, _ = self._process_family( - assets, 'StaticMesh', transform, sequence, inst) + assets, 'StaticMesh', transform, basis, + sequence, inst + ) elif family == 'rig': actors, bindings = self._process_family( - assets, 'SkeletalMesh', transform, sequence, inst) + assets, 'SkeletalMesh', transform, basis, + sequence, inst + ) actors_dict[inst] = actors bindings_dict[inst] = bindings From 927fe351a33be543a9d02d11b2924240ac2c380c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Jul 2022 22:43:14 +0200 Subject: [PATCH 091/432] settings: adding editorial family --- .../project_settings/traypublisher.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 0b54cfd39e..e938384282 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -30,6 +30,24 @@ ".psb", ".aep" ] + }, + { + "family": "editorial", + "identifier": "", + "label": "Editorial", + "icon": "fa.file", + "default_variants": [ + "Main" + ], + "description": "Editorial files to generate shots.", + "detailed_description": "Supporting publishing new shots to project or updating already created. Publishing will create OTIO file.", + "allow_sequences": false, + "extensions": [ + ".edl", + ".xml", + ".aaf", + ".fcpxml" + ] } ] } \ No newline at end of file From 49fd9e6308f711ee261293081ac1c5375c669043 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Jul 2022 15:51:54 +0200 Subject: [PATCH 092/432] editorial tray publisher kick-off --- openpype/hosts/traypublisher/api/plugin.py | 94 ++++++++++++++++++- .../plugins/create/create_editorial.py | 25 +++++ .../plugins/create/create_from_settings.py | 7 +- .../project_settings/traypublisher.json | 38 +++++++- .../schema_project_traypublisher.json | 83 ++++++++++++++++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/traypublisher/plugins/create/create_editorial.py diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 202664cfc6..901f05c755 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -2,7 +2,14 @@ from openpype.pipeline import ( Creator, CreatedInstance ) -from openpype.lib import FileDef +from openpype.lib import ( + FileDef, + TextDef, + NumberDef, + EnumDef, + BoolDef, + FileDefItem +) from .pipeline import ( list_instances, @@ -95,3 +102,88 @@ class SettingsCreator(TrayPublishCreator): "default_variants": item_data["default_variants"] } ) + + +class EditorialCreator(TrayPublishCreator): + create_allow_context_change = True + + extensions = [] + + def collect_instances(self): + for instance_data in list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def create(self, subset_name, data, pre_create_data): + # Pass precreate data to creator attributes + data["creator_attributes"] = pre_create_data + data["settings_creator"] = True + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, data, self) + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + def get_instance_attr_defs(self): + if self.identifier == "editorial.simple": + return [ + FileDef( + "sequence_filepath", + folders=False, + extensions=self.sequence_extensions, + allow_sequences=self.allow_sequences, + label="Filepath", + ) + ] + else: + return [ + FileDef( + "sequence_filepath", + folders=False, + extensions=self.sequence_extensions, + allow_sequences=self.allow_sequences, + label="Sequence filepath", + ), + FileDef( + "clip_source_folder", + folders=True, + extensions=self.clip_extensions, + allow_sequences=False, + label="Clips' Source folder", + ), + TextDef("text input"), + NumberDef("number input"), + EnumDef("enum input", { + "value1": "label1", + "value2": "label2" + }), + BoolDef("bool input") + ] + + @classmethod + def from_settings(cls, item_data): + identifier = item_data["identifier"] + family = item_data["family"] + if not identifier: + identifier = "settings_{}".format(family) + return type( + "{}{}".format(cls.__name__, identifier), + (cls, ), + { + "family": family, + "identifier": identifier, + "label": item_data["label"].strip(), + "icon": item_data["icon"], + "description": item_data["description"], + "detailed_description": item_data["detailed_description"], + "sequence_extensions": item_data["sequence_extensions"], + "clip_extensions": item_data["clip_extensions"], + "allow_sequences": item_data["allow_sequences"], + "default_variants": item_data["default_variants"] + } + ) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py new file mode 100644 index 0000000000..d7fe0f952c --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -0,0 +1,25 @@ +import os +from pprint import pformat +from openpype.api import get_project_settings, Logger + +log = Logger.get_logger(__name__) + + +def CreateEditorial(): + from openpype.hosts.traypublisher.api.plugin import EditorialCreator + + project_name = os.environ["AVALON_PROJECT"] + project_settings = get_project_settings(project_name) + + simple_creators = project_settings["traypublisher"]["editorial_creators"] + + global_variables = globals() + for item in simple_creators: + + log.debug(pformat(item)) + + dynamic_plugin = EditorialCreator.from_settings(item) + global_variables[dynamic_plugin.__name__] = dynamic_plugin + + +CreateEditorial() diff --git a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py index baca274ea6..1271e03fdb 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py +++ b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py @@ -1,6 +1,8 @@ import os +from pprint import pformat +from openpype.api import get_project_settings, Logger -from openpype.api import get_project_settings +log = Logger.get_logger(__name__) def initialize(): @@ -13,6 +15,9 @@ def initialize(): global_variables = globals() for item in simple_creators: + + log.debug(pformat(item)) + dynamic_plugin = SettingsCreator.from_settings(item) global_variables[dynamic_plugin.__name__] = dynamic_plugin diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index e938384282..64cbd4a6f3 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -30,11 +30,13 @@ ".psb", ".aep" ] - }, + } + ], + "editorial_creators": [ { "family": "editorial", - "identifier": "", - "label": "Editorial", + "identifier": "editorial.simple", + "label": "Editorial Simple", "icon": "fa.file", "default_variants": [ "Main" @@ -42,11 +44,39 @@ "description": "Editorial files to generate shots.", "detailed_description": "Supporting publishing new shots to project or updating already created. Publishing will create OTIO file.", "allow_sequences": false, - "extensions": [ + "sequence_extensions": [ ".edl", ".xml", ".aaf", ".fcpxml" + ], + "clip_extensions": [ + ".mov", + ".jpg", + ".png" + ] + }, + { + "family": "editorial", + "identifier": "editorial.complex", + "label": "Editorial Complex", + "icon": "fa.file", + "default_variants": [ + "Main" + ], + "description": "Editorial files to generate shots.", + "detailed_description": "Supporting publishing new shots to project or updating already created. Publishing will create OTIO file.", + "allow_sequences": false, + "sequence_extensions": [ + ".edl", + ".xml", + ".aaf", + ".fcpxml" + ], + "clip_extensions": [ + ".mov", + ".jpg", + ".png" ] } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 55c1b7b7d7..e112a8c004 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -78,6 +78,89 @@ } ] } + }, + { + "type": "list", + "collapsible": true, + "key": "editorial_creators", + "label": "Editorial creator plugins", + "use_label_wrap": true, + "collapsible_key": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "family", + "label": "Family" + }, + { + "type": "text", + "key": "identifier", + "label": "Identifier", + "placeholder": "< Use 'Family' >", + "tooltip": "All creators must have unique identifier.\nBy default is used 'family' but if you need to have more creators with same families\nyou have to set identifier too." + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "icon", + "label": "Icon" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "description", + "label": "Description" + }, + { + "type": "text", + "key": "detailed_description", + "label": "Detailed Description", + "multiline": true + }, + { + "type": "separator" + }, + { + "key": "allow_sequences", + "label": "Allow sequences", + "type": "boolean" + }, + { + "type": "list", + "key": "sequence_extensions", + "label": "Sequence extensions", + "use_label_wrap": true, + "collapsible_key": true, + "collapsed": false, + "object_type": "text" + }, + { + "type": "list", + "key": "clip_extensions", + "label": "Clip source file extensions", + "use_label_wrap": true, + "collapsible_key": true, + "collapsed": false, + "object_type": "text" + } + ] + } } ] } From 1bf95ddce5587c25258c9a7a7b07cf3a555b745b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 6 Jul 2022 17:00:35 +0100 Subject: [PATCH 093/432] Fix Maya transform --- openpype/hosts/maya/plugins/publish/extract_layout.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index 7eb6a64e6d..991217684a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -89,9 +89,12 @@ class ExtractLayout(openpype.api.Extractor): transform_mm = om.MMatrix(t_matrix_list) transform = om.MTransformationMatrix(transform_mm) - transform.scaleBy([1.0, 1.0, -1.0], om.MSpace.kWorld) + t = transform.translation(om.MSpace.kWorld) + t = om.MVector(t.x, t.z, -t.y) + transform.setTranslation(t, om.MSpace.kWorld) transform.rotateBy( om.MEulerRotation(math.radians(-90), 0, 0), om.MSpace.kWorld) + transform.scaleBy([1.0, 1.0, -1.0], om.MSpace.kObject) t_matrix_list = list(transform.asMatrix()) From 7444c2653073ec8a3a8ff49030ec547fa51cbfc2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Jul 2022 11:52:11 +0200 Subject: [PATCH 094/432] trayp: editorial wip --- openpype/hosts/traypublisher/api/editorial.py | 41 +++++++++++++++ openpype/hosts/traypublisher/api/plugin.py | 25 ++-------- .../plugins/create/create_editorial.py | 4 +- .../publish/collect_editorial_instances.py | 50 +++++++++++++++++++ 4 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 openpype/hosts/traypublisher/api/editorial.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py new file mode 100644 index 0000000000..316163b2fa --- /dev/null +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -0,0 +1,41 @@ + +import os +import opentimelineio as otio +from openpype import lib as plib + +from openpype.pipeline import ( + Creator, + CreatedInstance +) + +from .pipeline import ( + list_instances, + update_instances, + remove_instances, + HostContext, +) + + + +class CreateEditorialInstance: + """Create Editorial OTIO timeline""" + + def __init__(self, file_path, extension=None, resources_dir=None): + self.file_path = file_path + self.video_extension = extension or ".mov" + self.resources_dir = resources_dir + + def create(self): + + # get editorial sequence file into otio timeline object + extension = os.path.splitext(self.file_path)[1] + kwargs = {} + if extension == ".edl": + # EDL has no frame rate embedded so needs explicit + # frame rate else 24 is asssumed. + kwargs["rate"] = plib.get_asset()["data"]["fps"] + + instance.data["otio_timeline"] = otio.adapters.read_from_file( + file_path, **kwargs) + + self.log.info(f"Added OTIO timeline from: `{file_path}`") diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 901f05c755..ae9e93fd60 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -41,7 +41,7 @@ class TrayPublishCreator(Creator): self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): - # Use same attributes as for instance attrobites + # Use same attributes as for instance attributes return self.get_instance_attr_defs() @@ -50,15 +50,6 @@ class SettingsCreator(TrayPublishCreator): extensions = [] - def collect_instances(self): - for instance_data in list_instances(): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) - def create(self, subset_name, data, pre_create_data): # Pass precreate data to creator attributes data["creator_attributes"] = pre_create_data @@ -109,19 +100,13 @@ class EditorialCreator(TrayPublishCreator): extensions = [] - def collect_instances(self): - for instance_data in list_instances(): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) - def create(self, subset_name, data, pre_create_data): + # TODO: create otio instance + # TODO: create clip instances + # Pass precreate data to creator attributes data["creator_attributes"] = pre_create_data - data["settings_creator"] = True + data["editorial_creator"] = True # Create new instance new_instance = CreatedInstance(self.family, subset_name, data, self) # Host implementation of storing metadata about instance diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index d7fe0f952c..8b2af8973b 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -11,10 +11,10 @@ def CreateEditorial(): project_name = os.environ["AVALON_PROJECT"] project_settings = get_project_settings(project_name) - simple_creators = project_settings["traypublisher"]["editorial_creators"] + editorial_creators = project_settings["traypublisher"]["editorial_creators"] global_variables = globals() - for item in simple_creators: + for item in editorial_creators: log.debug(pformat(item)) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py new file mode 100644 index 0000000000..874b6101c3 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py @@ -0,0 +1,50 @@ +import os +import pyblish.api + + +class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): + """Collect data for instances created by settings creators.""" + + label = "Collect Settings Simple Instances" + order = pyblish.api.CollectorOrder - 0.49 + + hosts = ["traypublisher"] + + def process(self, instance): + if not instance.data.get("ediorial_creator"): + return + + if "families" not in instance.data: + instance.data["families"] = [] + + if "representations" not in instance.data: + instance.data["representations"] = [] + repres = instance.data["representations"] + + creator_attributes = instance.data["creator_attributes"] + filepath_item = creator_attributes["filepath"] + self.log.info(filepath_item) + filepaths = [ + os.path.join(filepath_item["directory"], filename) + for filename in filepath_item["filenames"] + ] + + instance.data["sourceFilepaths"] = filepaths + instance.data["stagingDir"] = filepath_item["directory"] + + filenames = filepath_item["filenames"] + _, ext = os.path.splitext(filenames[0]) + ext = ext[1:] + if len(filenames) == 1: + filenames = filenames[0] + + repres.append({ + "ext": ext, + "name": ext, + "stagingDir": filepath_item["directory"], + "files": filenames + }) + + self.log.debug("Created Simple Settings instance {}".format( + instance.data + )) From a88b1f1a33c1dada33a67cbe488776ce0c5f0b22 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Jul 2022 22:02:15 +0200 Subject: [PATCH 095/432] trayp: editorial family wip --- openpype/hosts/traypublisher/api/editorial.py | 41 ------- openpype/hosts/traypublisher/api/plugin.py | 86 +------------ .../plugins/create/create_editorial.py | 116 ++++++++++++++++-- openpype/pipeline/create/creator_plugins.py | 6 + 4 files changed, 112 insertions(+), 137 deletions(-) delete mode 100644 openpype/hosts/traypublisher/api/editorial.py diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py deleted file mode 100644 index 316163b2fa..0000000000 --- a/openpype/hosts/traypublisher/api/editorial.py +++ /dev/null @@ -1,41 +0,0 @@ - -import os -import opentimelineio as otio -from openpype import lib as plib - -from openpype.pipeline import ( - Creator, - CreatedInstance -) - -from .pipeline import ( - list_instances, - update_instances, - remove_instances, - HostContext, -) - - - -class CreateEditorialInstance: - """Create Editorial OTIO timeline""" - - def __init__(self, file_path, extension=None, resources_dir=None): - self.file_path = file_path - self.video_extension = extension or ".mov" - self.resources_dir = resources_dir - - def create(self): - - # get editorial sequence file into otio timeline object - extension = os.path.splitext(self.file_path)[1] - kwargs = {} - if extension == ".edl": - # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is asssumed. - kwargs["rate"] = plib.get_asset()["data"]["fps"] - - instance.data["otio_timeline"] = otio.adapters.read_from_file( - file_path, **kwargs) - - self.log.info(f"Added OTIO timeline from: `{file_path}`") diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index ae9e93fd60..94f6e7487f 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -3,12 +3,7 @@ from openpype.pipeline import ( CreatedInstance ) from openpype.lib import ( - FileDef, - TextDef, - NumberDef, - EnumDef, - BoolDef, - FileDefItem + FileDef ) from .pipeline import ( @@ -93,82 +88,3 @@ class SettingsCreator(TrayPublishCreator): "default_variants": item_data["default_variants"] } ) - - -class EditorialCreator(TrayPublishCreator): - create_allow_context_change = True - - extensions = [] - - def create(self, subset_name, data, pre_create_data): - # TODO: create otio instance - # TODO: create clip instances - - # Pass precreate data to creator attributes - data["creator_attributes"] = pre_create_data - data["editorial_creator"] = True - # Create new instance - new_instance = CreatedInstance(self.family, subset_name, data, self) - # Host implementation of storing metadata about instance - HostContext.add_instance(new_instance.data_to_store()) - # Add instance to current context - self._add_instance_to_context(new_instance) - - def get_instance_attr_defs(self): - if self.identifier == "editorial.simple": - return [ - FileDef( - "sequence_filepath", - folders=False, - extensions=self.sequence_extensions, - allow_sequences=self.allow_sequences, - label="Filepath", - ) - ] - else: - return [ - FileDef( - "sequence_filepath", - folders=False, - extensions=self.sequence_extensions, - allow_sequences=self.allow_sequences, - label="Sequence filepath", - ), - FileDef( - "clip_source_folder", - folders=True, - extensions=self.clip_extensions, - allow_sequences=False, - label="Clips' Source folder", - ), - TextDef("text input"), - NumberDef("number input"), - EnumDef("enum input", { - "value1": "label1", - "value2": "label2" - }), - BoolDef("bool input") - ] - - @classmethod - def from_settings(cls, item_data): - identifier = item_data["identifier"] - family = item_data["family"] - if not identifier: - identifier = "settings_{}".format(family) - return type( - "{}{}".format(cls.__name__, identifier), - (cls, ), - { - "family": family, - "identifier": identifier, - "label": item_data["label"].strip(), - "icon": item_data["icon"], - "description": item_data["description"], - "detailed_description": item_data["detailed_description"], - "sequence_extensions": item_data["sequence_extensions"], - "clip_extensions": item_data["clip_extensions"], - "allow_sequences": item_data["allow_sequences"], - "default_variants": item_data["default_variants"] - } - ) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8b2af8973b..49fba65711 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,25 +1,119 @@ import os -from pprint import pformat -from openpype.api import get_project_settings, Logger +import opentimelineio as otio +from openpype.api import get_project_settings +from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator +from openpype.pipeline.create.creator_plugins import InvisibleCreator -log = Logger.get_logger(__name__) +from openpype.pipeline import CreatedInstance + +from openpype.lib import ( + FileDef, + TextDef, + NumberDef, + EnumDef, + BoolDef +) + +from openpype.hosts.traypublisher.api.pipeline import HostContext def CreateEditorial(): - from openpype.hosts.traypublisher.api.plugin import EditorialCreator - project_name = os.environ["AVALON_PROJECT"] project_settings = get_project_settings(project_name) editorial_creators = project_settings["traypublisher"]["editorial_creators"] - global_variables = globals() - for item in editorial_creators: - log.debug(pformat(item)) +class EditorialClipInstanceCreator(InvisibleCreator): + identifier = "editorial.clip" + family = "clip" - dynamic_plugin = EditorialCreator.from_settings(item) - global_variables[dynamic_plugin.__name__] = dynamic_plugin + def create(self, instance_data, source_data): + # instance_data > asset, task_name, variant, family + # source_data > additional data + self.log.info(f"instance_data: {instance_data}") + self.log.info(f"source_data: {source_data}") -CreateEditorial() +class EditorialSimpleCreator(TrayPublishCreator): + + label = "Editorial Simple" + family = "editorial" + identifier = "editorial.simple" + default_variants = [ + "main", + "review" + ] + description = "Editorial files to generate shots." + detailed_description = """ +Supporting publishing new shots to project +or updating already created. Publishing will create OTIO file. +""" + icon = "fa.file" + + def create(self, subset_name, data, pre_create_data): + # TODO: create otio instance + otio_timeline = self._create_otio_instance( + subset_name, data, pre_create_data) + + # TODO: create clip instances + editorial_clip_creator = self.create_context.creators["editorial.clip"] + editorial_clip_creator.create({}, {}) + + def _create_otio_instance(self, subset_name, data, pre_create_data): + # from openpype import lib as plib + + # get path of sequence + file_path_data = pre_create_data["sequence_filepath_data"] + file_path = os.path.join( + file_path_data["directory"], file_path_data["filenames"][0]) + + self.log.info(f"file_path: {file_path}") + + # get editorial sequence file into otio timeline object + extension = os.path.splitext(file_path)[1] + kwargs = {} + if extension == ".edl": + # EDL has no frame rate embedded so needs explicit + # frame rate else 24 is asssumed. + kwargs["rate"] = float(25) + # plib.get_asset()["data"]["fps"] + + self.log.info(f"kwargs: {kwargs}") + otio_timeline = otio.adapters.read_from_file( + file_path, **kwargs) + + # Pass precreate data to creator attributes + data.update({ + "creator_attributes": pre_create_data, + "editorial_creator": True + + }) + + self._create_instance(self.family, subset_name, data) + + return otio_timeline + + def _create_instance(self, family, subset_name, data): + # Create new instance + new_instance = CreatedInstance(family, subset_name, data, self) + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + def get_instance_attr_defs(self): + return [ + FileDef( + "sequence_filepath_data", + folders=False, + extensions=[ + ".edl", + ".xml", + ".aaf", + ".fcpxml" + ], + allow_sequences=False, + label="Filepath", + ) + ] diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 8006d4f4f8..778d6846b2 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -342,6 +342,12 @@ class Creator(BaseCreator): return self.pre_create_attr_defs +class InvisibleCreator(BaseCreator): + @abstractmethod + def create(self, instance_data, source_data): + pass + + class AutoCreator(BaseCreator): """Creator which is automatically triggered without user interaction. From 14acec63c2d0a3760f9ecbf41a431461f9bc459b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Jul 2022 10:49:35 +0200 Subject: [PATCH 096/432] create plugins have access to project name --- openpype/pipeline/create/context.py | 4 ++++ openpype/pipeline/create/creator_plugins.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 12cd9bbc68..c91b13e520 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -748,6 +748,10 @@ class CreateContext: def host_name(self): return os.environ["AVALON_APP"] + @property + def project_name(self): + return self.dbcon.active_project() + @property def log(self): """Dynamic access to logger.""" diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 778d6846b2..be3f3d4cbd 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -92,6 +92,12 @@ class BaseCreator: """Family that plugin represents.""" pass + @property + def project_name(self): + """Family that plugin represents.""" + + self.create_context.project_name + @property def log(self): if self._log is None: From 9eed955303f3937b0e0ddb4fbd515408a69c0e95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Jul 2022 11:02:22 +0200 Subject: [PATCH 097/432] use settings on init and query asset document --- .../plugins/create/create_editorial.py | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 49fba65711..54a52dfb75 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,6 +1,7 @@ import os +from copy import deepcopy import opentimelineio as otio -from openpype.api import get_project_settings +from openpype.client import get_asset_by_name from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator from openpype.pipeline.create.creator_plugins import InvisibleCreator @@ -17,16 +18,18 @@ from openpype.lib import ( from openpype.hosts.traypublisher.api.pipeline import HostContext -def CreateEditorial(): - project_name = os.environ["AVALON_PROJECT"] - project_settings = get_project_settings(project_name) - - editorial_creators = project_settings["traypublisher"]["editorial_creators"] - - class EditorialClipInstanceCreator(InvisibleCreator): identifier = "editorial.clip" family = "clip" + host_name = "traypublisher" + + def __init__( + self, create_context, system_settings, project_settings, + *args, **kwargs + ): + super(EditorialClipInstanceCreator, self).__init__( + create_context, system_settings, project_settings, *args, **kwargs + ) def create(self, instance_data, source_data): # instance_data > asset, task_name, variant, family @@ -51,10 +54,24 @@ or updating already created. Publishing will create OTIO file. """ icon = "fa.file" - def create(self, subset_name, data, pre_create_data): + def __init__( + self, create_context, system_settings, project_settings, + *args, **kwargs + ): + super(EditorialSimpleCreator, self).__init__( + create_context, system_settings, project_settings, *args, **kwargs + ) + editorial_creators = ( + project_settings["traypublisher"]["editorial_creators"] + ) + self._editorial_creators = deepcopy(editorial_creators) + + def create(self, subset_name, instance_data, pre_create_data): # TODO: create otio instance + asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(self.project_name, asset_name) otio_timeline = self._create_otio_instance( - subset_name, data, pre_create_data) + subset_name, instance_data, pre_create_data) # TODO: create clip instances editorial_clip_creator = self.create_context.creators["editorial.clip"] From dcc64f9eb425c94e85a66df53d1c2ddd7762b7b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 11:13:15 +0200 Subject: [PATCH 098/432] trayp: updating create_editorial --- .../hosts/traypublisher/plugins/create/create_editorial.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 54a52dfb75..a58d968e3d 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -61,10 +61,13 @@ or updating already created. Publishing will create OTIO file. super(EditorialSimpleCreator, self).__init__( create_context, system_settings, project_settings, *args, **kwargs ) - editorial_creators = ( + editorial_creators = deepcopy( project_settings["traypublisher"]["editorial_creators"] ) - self._editorial_creators = deepcopy(editorial_creators) + self._creator_settings = editorial_creators.get(self.__name__) + + if self._creator_settings.get("default_variants"): + self.default_variants = self._creator_settings["default_variants"] def create(self, subset_name, instance_data, pre_create_data): # TODO: create otio instance From c9a70d410f8de60b4171fad7f2314dda7c4d5e20 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Jul 2022 11:15:09 +0200 Subject: [PATCH 099/432] use project_name attribute --- openpype/pipeline/create/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c91b13e520..8f79110fdf 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -843,9 +843,8 @@ class CreateContext: self.plugins_with_defs = plugins_with_defs # Prepare settings - project_name = self.dbcon.Session["AVALON_PROJECT"] system_settings = get_system_settings() - project_settings = get_project_settings(project_name) + project_settings = get_project_settings(self.project_name) # Discover and prepare creators creators = {} From 76e36015dcd530c6b5c40b9a8a041821d308a1ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Jul 2022 11:20:44 +0200 Subject: [PATCH 100/432] implemented invisible tray publisher creator --- openpype/hosts/traypublisher/api/plugin.py | 25 +++++++++++++++++++++- openpype/pipeline/create/__init__.py | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 94f6e7487f..75f73e88b1 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,5 +1,6 @@ -from openpype.pipeline import ( +from openpype.pipeline.create import ( Creator, + InivisbleCreator, CreatedInstance ) from openpype.lib import ( @@ -14,6 +15,28 @@ from .pipeline import ( ) +class InvisibleTrayPublishCreator(InivisbleCreator): + create_allow_context_change = True + host_name = "traypublisher" + + def collect_instances(self): + for instance_data in list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + update_instances(update_list) + + def remove_instances(self, instances): + remove_instances(instances) + for instance in instances: + self._remove_instance_from_context(instance) + + class TrayPublishCreator(Creator): create_allow_context_change = True host_name = "traypublisher" diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 1beeb4267b..a0f2c16f75 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -7,6 +7,7 @@ from .creator_plugins import ( BaseCreator, Creator, AutoCreator, + InivisbleCreator, discover_creator_plugins, discover_legacy_creator_plugins, @@ -35,6 +36,7 @@ __all__ = ( "BaseCreator", "Creator", "AutoCreator", + "InivisbleCreator", "discover_creator_plugins", "discover_legacy_creator_plugins", From 82899b1acda57320c0faf31fcf0666a762b041d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Jul 2022 11:22:15 +0200 Subject: [PATCH 101/432] implement get_pre_create_attr_defs only for settings creator --- openpype/hosts/traypublisher/api/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 75f73e88b1..c7f2f4ec13 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -58,10 +58,6 @@ class TrayPublishCreator(Creator): for instance in instances: self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return self.get_instance_attr_defs() - class SettingsCreator(TrayPublishCreator): create_allow_context_change = True @@ -90,6 +86,10 @@ class SettingsCreator(TrayPublishCreator): ) ] + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attrobites + return self.get_instance_attr_defs() + @classmethod def from_settings(cls, item_data): identifier = item_data["identifier"] From 10aae0e98686c6cc2463b4b763778adcb25608aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 11:28:02 +0200 Subject: [PATCH 102/432] fixing invisible creator name --- .../plugins/create/create_editorial.py | 13 +++++++++---- openpype/pipeline/create/__init__.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index a58d968e3d..61f24ec60e 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -2,8 +2,11 @@ import os from copy import deepcopy import opentimelineio as otio from openpype.client import get_asset_by_name -from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator -from openpype.pipeline.create.creator_plugins import InvisibleCreator +from openpype.hosts.traypublisher.api.plugin import ( + TrayPublishCreator, + InvisibleTrayPublishCreator +) + from openpype.pipeline import CreatedInstance @@ -18,7 +21,7 @@ from openpype.lib import ( from openpype.hosts.traypublisher.api.pipeline import HostContext -class EditorialClipInstanceCreator(InvisibleCreator): +class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): identifier = "editorial.clip" family = "clip" host_name = "traypublisher" @@ -64,8 +67,10 @@ or updating already created. Publishing will create OTIO file. editorial_creators = deepcopy( project_settings["traypublisher"]["editorial_creators"] ) - self._creator_settings = editorial_creators.get(self.__name__) + # get this creator settings by identifier + self._creator_settings = editorial_creators.get(self.identifier) + # try to set main attributes from settings if self._creator_settings.get("default_variants"): self.default_variants = self._creator_settings["default_variants"] diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index a0f2c16f75..cd01c53cf5 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -7,7 +7,7 @@ from .creator_plugins import ( BaseCreator, Creator, AutoCreator, - InivisbleCreator, + InvisibleCreator, discover_creator_plugins, discover_legacy_creator_plugins, @@ -36,7 +36,7 @@ __all__ = ( "BaseCreator", "Creator", "AutoCreator", - "InivisbleCreator", + "InvisibleCreator", "discover_creator_plugins", "discover_legacy_creator_plugins", From a31ea2a24d4de68fbbb6f47d5eb224cd02e182e7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 11:28:37 +0200 Subject: [PATCH 103/432] fixing invisible creator name 2 --- openpype/hosts/traypublisher/api/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index c7f2f4ec13..0d7651e464 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,6 +1,6 @@ from openpype.pipeline.create import ( Creator, - InivisbleCreator, + InvisibleCreator, CreatedInstance ) from openpype.lib import ( @@ -15,8 +15,7 @@ from .pipeline import ( ) -class InvisibleTrayPublishCreator(InivisbleCreator): - create_allow_context_change = True +class InvisibleTrayPublishCreator(InvisibleCreator): host_name = "traypublisher" def collect_instances(self): From 2270c972906b6aac890c95016c125f4001a63f0f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 11:28:51 +0200 Subject: [PATCH 104/432] trayp: adding settings --- .../project_settings/traypublisher.json | 47 +-------- .../schema_project_traypublisher.json | 96 ++++--------------- 2 files changed, 24 insertions(+), 119 deletions(-) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 64cbd4a6f3..4a672789ed 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -32,52 +32,11 @@ ] } ], - "editorial_creators": [ - { - "family": "editorial", - "identifier": "editorial.simple", - "label": "Editorial Simple", - "icon": "fa.file", + "editorial_creators": { + "editorial.simple": { "default_variants": [ "Main" - ], - "description": "Editorial files to generate shots.", - "detailed_description": "Supporting publishing new shots to project or updating already created. Publishing will create OTIO file.", - "allow_sequences": false, - "sequence_extensions": [ - ".edl", - ".xml", - ".aaf", - ".fcpxml" - ], - "clip_extensions": [ - ".mov", - ".jpg", - ".png" - ] - }, - { - "family": "editorial", - "identifier": "editorial.complex", - "label": "Editorial Complex", - "icon": "fa.file", - "default_variants": [ - "Main" - ], - "description": "Editorial files to generate shots.", - "detailed_description": "Supporting publishing new shots to project or updating already created. Publishing will create OTIO file.", - "allow_sequences": false, - "sequence_extensions": [ - ".edl", - ".xml", - ".aaf", - ".fcpxml" - ], - "clip_extensions": [ - ".mov", - ".jpg", - ".png" ] } - ] + } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index e112a8c004..1b24fcbe93 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -80,87 +80,33 @@ } }, { - "type": "list", + "type": "dict", "collapsible": true, "key": "editorial_creators", "label": "Editorial creator plugins", "use_label_wrap": true, "collapsible_key": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "family", - "label": "Family" - }, - { - "type": "text", - "key": "identifier", - "label": "Identifier", - "placeholder": "< Use 'Family' >", - "tooltip": "All creators must have unique identifier.\nBy default is used 'family' but if you need to have more creators with same families\nyou have to set identifier too." - }, - { - "type": "text", - "key": "label", - "label": "Label" - }, - { - "type": "text", - "key": "icon", - "label": "Icon" - }, - { - "type": "list", - "key": "default_variants", - "label": "Default variants", - "object_type": { - "type": "text" + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "editorial.simple", + "label": "Editorial simple creator", + "use_label_wrap": true, + "collapsible_key": true, + "children": [ + + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } } - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "description", - "label": "Description" - }, - { - "type": "text", - "key": "detailed_description", - "label": "Detailed Description", - "multiline": true - }, - { - "type": "separator" - }, - { - "key": "allow_sequences", - "label": "Allow sequences", - "type": "boolean" - }, - { - "type": "list", - "key": "sequence_extensions", - "label": "Sequence extensions", - "use_label_wrap": true, - "collapsible_key": true, - "collapsed": false, - "object_type": "text" - }, - { - "type": "list", - "key": "clip_extensions", - "label": "Clip source file extensions", - "use_label_wrap": true, - "collapsible_key": true, - "collapsed": false, - "object_type": "text" - } - ] - } + ] + } + ] } ] } From aa79551cedf9fef3c78535fbe8f3a3b819fa3f7f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 11:34:31 +0200 Subject: [PATCH 105/432] trayp: identifier as key in settings didnt work with dot --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 +- openpype/settings/defaults/project_settings/traypublisher.json | 2 +- .../schemas/projects_schema/schema_project_traypublisher.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 61f24ec60e..442ff77130 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -45,7 +45,7 @@ class EditorialSimpleCreator(TrayPublishCreator): label = "Editorial Simple" family = "editorial" - identifier = "editorial.simple" + identifier = "editorialSimple" default_variants = [ "main", "review" diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 4a672789ed..ef6dc5fec7 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -33,7 +33,7 @@ } ], "editorial_creators": { - "editorial.simple": { + "editorialSimple": { "default_variants": [ "Main" ] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 1b24fcbe93..11ae0e65a7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -90,7 +90,7 @@ { "type": "dict", "collapsible": true, - "key": "editorial.simple", + "key": "editorialSimple", "label": "Editorial simple creator", "use_label_wrap": true, "collapsible_key": true, From ec21481c60847f35c98bbf534a1479440d8bd28f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 12:04:29 +0200 Subject: [PATCH 106/432] trayp: adding precreate properties --- .../plugins/create/create_editorial.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 442ff77130..b31072aaf1 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -15,14 +15,16 @@ from openpype.lib import ( TextDef, NumberDef, EnumDef, - BoolDef + BoolDef, + UISeparatorDef, + UILabelDef ) from openpype.hosts.traypublisher.api.pipeline import HostContext class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): - identifier = "editorial.clip" + identifier = "editorialClip" family = "clip" host_name = "traypublisher" @@ -47,8 +49,7 @@ class EditorialSimpleCreator(TrayPublishCreator): family = "editorial" identifier = "editorialSimple" default_variants = [ - "main", - "review" + "main" ] description = "Editorial files to generate shots." detailed_description = """ @@ -82,7 +83,7 @@ or updating already created. Publishing will create OTIO file. subset_name, instance_data, pre_create_data) # TODO: create clip instances - editorial_clip_creator = self.create_context.creators["editorial.clip"] + editorial_clip_creator = self.create_context.creators["editorialClip"] editorial_clip_creator.create({}, {}) def _create_otio_instance(self, subset_name, data, pre_create_data): @@ -127,7 +128,8 @@ or updating already created. Publishing will create OTIO file. # Add instance to current context self._add_instance_to_context(new_instance) - def get_instance_attr_defs(self): + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attrobites return [ FileDef( "sequence_filepath_data", @@ -140,5 +142,7 @@ or updating already created. Publishing will create OTIO file. ], allow_sequences=False, label="Filepath", - ) - ] + ), + UISeparatorDef(), + UILabelDef("Clip instance attributes") + ] \ No newline at end of file From ba4dd7cc2234b882b0e527e75d0a5bc48666f463 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 12:47:36 +0200 Subject: [PATCH 107/432] creator: fixing returning project_name --- openpype/pipeline/create/creator_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index be3f3d4cbd..e0de2baa77 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -96,7 +96,7 @@ class BaseCreator: def project_name(self): """Family that plugin represents.""" - self.create_context.project_name + return self.create_context.project_name @property def log(self): From 01e548d2ff070fe6e9058f334e097919ea18cee9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 13:52:36 +0200 Subject: [PATCH 108/432] trayp: fixing init arg --- .../traypublisher/plugins/create/create_editorial.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index b31072aaf1..560a5ae047 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -29,11 +29,10 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): host_name = "traypublisher" def __init__( - self, create_context, system_settings, project_settings, - *args, **kwargs + self, project_settings, *args, **kwargs ): super(EditorialClipInstanceCreator, self).__init__( - create_context, system_settings, project_settings, *args, **kwargs + project_settings, *args, **kwargs ) def create(self, instance_data, source_data): @@ -59,11 +58,10 @@ or updating already created. Publishing will create OTIO file. icon = "fa.file" def __init__( - self, create_context, system_settings, project_settings, - *args, **kwargs + self, project_settings, *args, **kwargs ): super(EditorialSimpleCreator, self).__init__( - create_context, system_settings, project_settings, *args, **kwargs + project_settings, *args, **kwargs ) editorial_creators = deepcopy( project_settings["traypublisher"]["editorial_creators"] From c08713c258a230ab20494e692fce9eaa488b8cd3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Jul 2022 17:19:28 +0200 Subject: [PATCH 109/432] trayp: udpating editorial creator --- .../plugins/create/create_editorial.py | 220 ++++++++++++++++-- 1 file changed, 207 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 560a5ae047..ed91f0201f 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,5 +1,6 @@ import os from copy import deepcopy +from pprint import pformat import opentimelineio as otio from openpype.client import get_asset_by_name from openpype.hosts.traypublisher.api.plugin import ( @@ -23,6 +24,31 @@ from openpype.lib import ( from openpype.hosts.traypublisher.api.pipeline import HostContext +CLIP_ATTR_DEFS = [ + NumberDef( + "timeline_offset", + default=900000, + label="Timeline offset" + ), + UISeparatorDef(), + NumberDef( + "workfile_start_frame", + default=1001, + label="Workfile start frame" + ), + NumberDef( + "handle_start", + default=0, + label="Handle start" + ), + NumberDef( + "handle_end", + default=0, + label="Handle end" + ) +] + + class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): identifier = "editorialClip" family = "clip" @@ -41,6 +67,32 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): self.log.info(f"instance_data: {instance_data}") self.log.info(f"source_data: {source_data}") + instance_name = "{}_{}".format( + instance_data["name"], + "plateMain" + ) + return self._create_instance(instance_name, instance_data) + + def _create_instance(self, subset_name, data): + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, data, self) + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + return new_instance + + def get_instance_attr_defs(self): + attr_defs = [ + TextDef( + "asset_name", + label="Asset name", + ) + ] + attr_defs.extend(CLIP_ATTR_DEFS) + return attr_defs + class EditorialSimpleCreator(TrayPublishCreator): @@ -57,6 +109,8 @@ or updating already created. Publishing will create OTIO file. """ icon = "fa.file" + + def __init__( self, project_settings, *args, **kwargs ): @@ -74,19 +128,29 @@ or updating already created. Publishing will create OTIO file. self.default_variants = self._creator_settings["default_variants"] def create(self, subset_name, instance_data, pre_create_data): + clip_instance_properties = { + k: v for k, v in pre_create_data.items() + if k != "sequence_filepath_data" + } # TODO: create otio instance asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) + fps = asset_doc["data"]["fps"] + instance_data.update({ + "fps": fps + }) otio_timeline = self._create_otio_instance( subset_name, instance_data, pre_create_data) # TODO: create clip instances - editorial_clip_creator = self.create_context.creators["editorialClip"] - editorial_clip_creator.create({}, {}) + clip_instance_properties.update({ + "fps": fps, + "asset_name": asset_name + }) + self._get_clip_instances( + asset_name, otio_timeline, clip_instance_properties) def _create_otio_instance(self, subset_name, data, pre_create_data): - # from openpype import lib as plib - # get path of sequence file_path_data = pre_create_data["sequence_filepath_data"] file_path = os.path.join( @@ -100,8 +164,7 @@ or updating already created. Publishing will create OTIO file. if extension == ".edl": # EDL has no frame rate embedded so needs explicit # frame rate else 24 is asssumed. - kwargs["rate"] = float(25) - # plib.get_asset()["data"]["fps"] + kwargs["rate"] = data["fps"] self.log.info(f"kwargs: {kwargs}") otio_timeline = otio.adapters.read_from_file( @@ -109,15 +172,144 @@ or updating already created. Publishing will create OTIO file. # Pass precreate data to creator attributes data.update({ - "creator_attributes": pre_create_data, - "editorial_creator": True - + "sequence_file_path": file_path }) self._create_instance(self.family, subset_name, data) return otio_timeline + def _get_clip_instances( + self, + asset_name, + otio_timeline, + clip_instance_properties + ): + parent_asset_name = clip_instance_properties["asset_name"] + handle_start = clip_instance_properties["handle_start"] + handle_end = clip_instance_properties["handle_end"] + timeline_offset = clip_instance_properties["timeline_offset"] + workfile_start_frame = clip_instance_properties["workfile_start_frame"] + fps = clip_instance_properties["fps"] + + assets_shared = {} + self.asset_name_check = [] + + editorial_clip_creator = self.create_context.creators["editorialClip"] + + tracks = otio_timeline.each_child( + descended_from_type=otio.schema.Track + ) + + for track in tracks: + self.log.debug(f"track.name: {track.name}") + try: + track_start_frame = ( + abs(track.source_range.start_time.value) + ) + self.log.debug(f"track_start_frame: {track_start_frame}") + track_start_frame -= self.timeline_frame_start + except AttributeError: + track_start_frame = 0 + + self.log.debug(f"track_start_frame: {track_start_frame}") + + for clip in track.each_child(): + + if not self._validate_clip_for_processing(clip): + continue + + # basic unique asset name + clip_name = os.path.splitext(clip.name)[0].lower() + name = f"{asset_name.split('_')[0]}_{clip_name}" + + # make sure the name is unique + self._validate_name_uniqueness(name) + + # frame ranges data + clip_in = clip.range_in_parent().start_time.value + clip_in += track_start_frame + clip_out = clip.range_in_parent().end_time_inclusive().value + clip_out += track_start_frame + self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") + + # add offset in case there is any + if timeline_offset: + clip_in += timeline_offset + clip_out += timeline_offset + + clip_duration = clip.duration().value + self.log.info(f"clip duration: {clip_duration}") + + source_in = clip.trimmed_range().start_time.value + source_out = source_in + clip_duration + + # define starting frame for future shot + frame_start = ( + clip_in if workfile_start_frame is None + else workfile_start_frame + ) + frame_end = frame_start + (clip_duration - 1) + + # create shared new instance data + instance_data = { + "variant": "Main", + "families": ["plate"], + + # shared attributes + "asset": parent_asset_name, + "name": clip_name, + "task": "Compositing", + + # parent time properties + "trackStartFrame": track_start_frame, + + # creator_attributes + "creator_attributes": { + "asset_name": clip_name, + "timeline_offset": timeline_offset, + "workfile_start_frame": workfile_start_frame, + "frameStart": frame_start, + "frameEnd": frame_end, + "fps": fps, + "handle_start": handle_start, + "handle_end": handle_end, + "clipIn": clip_in, + "clipOut": clip_out, + "sourceIn": source_in, + "sourceOut": source_out, + } + } + + c_instance = editorial_clip_creator.create(instance_data, {}) + self.log.debug(f"{pformat(dict(c_instance.data))}") + + def _validate_clip_for_processing(self, clip): + if clip.name is None: + return False + + if isinstance(clip, otio.schema.Gap): + return False + + # skip all generators like black empty + if isinstance( + clip.media_reference, + otio.schema.GeneratorReference): + return False + + # Transitions are ignored, because Clips have the full frame + # range. + if isinstance(clip, otio.schema.Transition): + return False + + return True + + def _validate_name_uniqueness(self, name): + if name not in self.asset_name_check: + self.asset_name_check.append(name) + else: + self.log.warning(f"duplicate shot name: {name}") + def _create_instance(self, family, subset_name, data): # Create new instance new_instance = CreatedInstance(family, subset_name, data, self) @@ -128,7 +320,7 @@ or updating already created. Publishing will create OTIO file. def get_pre_create_attr_defs(self): # Use same attributes as for instance attrobites - return [ + attr_defs = [ FileDef( "sequence_filepath_data", folders=False, @@ -141,6 +333,8 @@ or updating already created. Publishing will create OTIO file. allow_sequences=False, label="Filepath", ), - UISeparatorDef(), - UILabelDef("Clip instance attributes") - ] \ No newline at end of file + UILabelDef("Clip instance attributes"), + UISeparatorDef() + ] + attr_defs.extend(CLIP_ATTR_DEFS) + return attr_defs From c60c0ff2d9abe9ecb312a3f553b9523c4bbae8f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Jul 2022 09:58:07 +0200 Subject: [PATCH 110/432] trayp: updating create editorial --- .../plugins/create/create_editorial.py | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index ed91f0201f..3164e4aa99 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -25,12 +25,6 @@ from openpype.hosts.traypublisher.api.pipeline import HostContext CLIP_ATTR_DEFS = [ - NumberDef( - "timeline_offset", - default=900000, - label="Timeline offset" - ), - UISeparatorDef(), NumberDef( "workfile_start_frame", default=1001, @@ -62,20 +56,20 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): ) def create(self, instance_data, source_data): - # instance_data > asset, task_name, variant, family - # source_data > additional data self.log.info(f"instance_data: {instance_data}") - self.log.info(f"source_data: {source_data}") + subset_name = instance_data["subset"] + family = instance_data["family"] instance_name = "{}_{}".format( instance_data["name"], - "plateMain" + subset_name ) - return self._create_instance(instance_name, instance_data) + return self._create_instance(instance_name, family, instance_data) + + def _create_instance(self, subset_name, family, data): - def _create_instance(self, subset_name, data): # Create new instance - new_instance = CreatedInstance(self.family, subset_name, data, self) + new_instance = CreatedInstance(family, subset_name, data, self) # Host implementation of storing metadata about instance HostContext.add_instance(new_instance.data_to_store()) # Add instance to current context @@ -109,8 +103,6 @@ or updating already created. Publishing will create OTIO file. """ icon = "fa.file" - - def __init__( self, project_settings, *args, **kwargs ): @@ -132,23 +124,27 @@ or updating already created. Publishing will create OTIO file. k: v for k, v in pre_create_data.items() if k != "sequence_filepath_data" } - # TODO: create otio instance + # Create otio editorial instance asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) + + # get asset doc data attributes fps = asset_doc["data"]["fps"] instance_data.update({ "fps": fps }) + + # get otio timeline otio_timeline = self._create_otio_instance( subset_name, instance_data, pre_create_data) - # TODO: create clip instances + # Create all clip instances clip_instance_properties.update({ "fps": fps, - "asset_name": asset_name + "parent_asset_name": asset_name }) self._get_clip_instances( - asset_name, otio_timeline, clip_instance_properties) + otio_timeline, clip_instance_properties) def _create_otio_instance(self, subset_name, data, pre_create_data): # get path of sequence @@ -181,18 +177,19 @@ or updating already created. Publishing will create OTIO file. def _get_clip_instances( self, - asset_name, otio_timeline, clip_instance_properties ): - parent_asset_name = clip_instance_properties["asset_name"] + family = "plate" + + # get clip instance properties + parent_asset_name = clip_instance_properties["parent_asset_name"] handle_start = clip_instance_properties["handle_start"] handle_end = clip_instance_properties["handle_end"] timeline_offset = clip_instance_properties["timeline_offset"] workfile_start_frame = clip_instance_properties["workfile_start_frame"] fps = clip_instance_properties["fps"] - assets_shared = {} self.asset_name_check = [] editorial_clip_creator = self.create_context.creators["editorialClip"] @@ -221,7 +218,7 @@ or updating already created. Publishing will create OTIO file. # basic unique asset name clip_name = os.path.splitext(clip.name)[0].lower() - name = f"{asset_name.split('_')[0]}_{clip_name}" + name = f"{parent_asset_name.split('_')[0]}_{clip_name}" # make sure the name is unique self._validate_name_uniqueness(name) @@ -251,14 +248,24 @@ or updating already created. Publishing will create OTIO file. ) frame_end = frame_start + (clip_duration - 1) + # subset name + variant = self.variant + subset_name = "{}{}".format( + family, variant.capitalize() + ) + # create shared new instance data instance_data = { - "variant": "Main", - "families": ["plate"], + "variant": variant, + "family": family, + "families": ["clip"], + "subset": subset_name, - # shared attributes + # HACK: just for temporal bug workaround + # TODO: should loockup shot name for update "asset": parent_asset_name, "name": clip_name, + # HACK: just for temporal bug workaround "task": "Compositing", # parent time properties @@ -334,6 +341,13 @@ or updating already created. Publishing will create OTIO file. label="Filepath", ), UILabelDef("Clip instance attributes"), + UISeparatorDef(), + # TODO: perhpas better would be timecode and fps input + NumberDef( + "timeline_offset", + default=900000, + label="Timeline offset" + ), UISeparatorDef() ] attr_defs.extend(CLIP_ATTR_DEFS) From 370ee0c254a1a3d6eab1e4a27f8cbf4bc5676986 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Jul 2022 15:21:37 +0200 Subject: [PATCH 111/432] trayp: added `fps` enumerator for rate override --- .../plugins/create/create_editorial.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 3164e4aa99..406a7bc3b3 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -25,6 +25,18 @@ from openpype.hosts.traypublisher.api.pipeline import HostContext CLIP_ATTR_DEFS = [ + EnumDef( + "fps", + items={ + "from_project": "From project", + 23.997: "23.976", + 24: "24", + 25: "25", + 29.97: "29.97", + 30: "30" + }, + label="FPS" + ), NumberDef( "workfile_start_frame", default=1001, @@ -128,8 +140,14 @@ or updating already created. Publishing will create OTIO file. asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) - # get asset doc data attributes - fps = asset_doc["data"]["fps"] + self.log.info(pre_create_data["fps"]) + + if pre_create_data["fps"] == "from_project": + # get asset doc data attributes + fps = asset_doc["data"]["fps"] + else: + fps = float(pre_create_data["fps"]) + instance_data.update({ "fps": fps }) @@ -149,6 +167,10 @@ or updating already created. Publishing will create OTIO file. def _create_otio_instance(self, subset_name, data, pre_create_data): # get path of sequence file_path_data = pre_create_data["sequence_filepath_data"] + + if len(file_path_data["filenames"]) == 0: + raise FileExistsError("File path was not added") + file_path = os.path.join( file_path_data["directory"], file_path_data["filenames"][0]) From 3f7dfb6579394237dfbf18c488c0586b06129fa2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Jul 2022 17:43:00 +0200 Subject: [PATCH 112/432] trayp: removing task --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 406a7bc3b3..6c8c1abdae 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -287,8 +287,6 @@ or updating already created. Publishing will create OTIO file. # TODO: should loockup shot name for update "asset": parent_asset_name, "name": clip_name, - # HACK: just for temporal bug workaround - "task": "Compositing", # parent time properties "trackStartFrame": track_start_frame, From ccffaa38bac0b4f7a6b4bbd500864dfa99cc87be Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Jul 2022 17:43:46 +0200 Subject: [PATCH 113/432] trayp: adding label to created instance --- .../plugins/create/create_editorial.py | 19 ++++++++++++------- .../publish/collect_editorial_instances.py | 9 +++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 6c8c1abdae..e47d28447b 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -72,16 +72,14 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): subset_name = instance_data["subset"] family = instance_data["family"] - instance_name = "{}_{}".format( - instance_data["name"], - subset_name - ) - return self._create_instance(instance_name, family, instance_data) + return self._create_instance(subset_name, family, instance_data) def _create_instance(self, subset_name, family, data): # Create new instance new_instance = CreatedInstance(family, subset_name, data, self) + self.log.info(f"instance_data: {pformat(new_instance.data)}") + # Host implementation of storing metadata about instance HostContext.add_instance(new_instance.data_to_store()) # Add instance to current context @@ -271,13 +269,20 @@ or updating already created. Publishing will create OTIO file. frame_end = frame_start + (clip_duration - 1) # subset name - variant = self.variant + variant = self.get_variant() + self.log.info( + f"__ variant: {variant}") + subset_name = "{}{}".format( family, variant.capitalize() ) - + label = "{}_{}".format( + clip_name, + subset_name + ) # create shared new instance data instance_data = { + "label": label, "variant": variant, "family": family, "families": ["clip"], diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py index 874b6101c3..6521c97774 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py @@ -1,18 +1,17 @@ import os +from pprint import pformat import pyblish.api class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): """Collect data for instances created by settings creators.""" - label = "Collect Settings Simple Instances" + label = "Collect Editorial Instances" order = pyblish.api.CollectorOrder - 0.49 hosts = ["traypublisher"] def process(self, instance): - if not instance.data.get("ediorial_creator"): - return if "families" not in instance.data: instance.data["families"] = [] @@ -20,7 +19,9 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): if "representations" not in instance.data: instance.data["representations"] = [] repres = instance.data["representations"] - + self.log.debug( + pformat(dict(instance.data)) + ) creator_attributes = instance.data["creator_attributes"] filepath_item = creator_attributes["filepath"] self.log.info(filepath_item) From e77d4a11d82c212930337609158767bf0de2a142 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Jul 2022 18:03:20 +0200 Subject: [PATCH 114/432] trayp: variant rework and timecode offset default to 0 --- .../plugins/create/create_editorial.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index e47d28447b..643c8a2a84 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -160,7 +160,10 @@ or updating already created. Publishing will create OTIO file. "parent_asset_name": asset_name }) self._get_clip_instances( - otio_timeline, clip_instance_properties) + otio_timeline, + clip_instance_properties, + variant=instance_data["variant"] + ) def _create_otio_instance(self, subset_name, data, pre_create_data): # get path of sequence @@ -198,7 +201,8 @@ or updating already created. Publishing will create OTIO file. def _get_clip_instances( self, otio_timeline, - clip_instance_properties + clip_instance_properties, + variant ): family = "plate" @@ -251,6 +255,7 @@ or updating already created. Publishing will create OTIO file. self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") # add offset in case there is any + self.log.debug(f"__ timeline_offset: {timeline_offset}") if timeline_offset: clip_in += timeline_offset clip_out += timeline_offset @@ -269,7 +274,6 @@ or updating already created. Publishing will create OTIO file. frame_end = frame_start + (clip_duration - 1) # subset name - variant = self.get_variant() self.log.info( f"__ variant: {variant}") @@ -292,6 +296,7 @@ or updating already created. Publishing will create OTIO file. # TODO: should loockup shot name for update "asset": parent_asset_name, "name": clip_name, + "task": "", # parent time properties "trackStartFrame": track_start_frame, @@ -370,7 +375,7 @@ or updating already created. Publishing will create OTIO file. # TODO: perhpas better would be timecode and fps input NumberDef( "timeline_offset", - default=900000, + default=0, label="Timeline offset" ), UISeparatorDef() From 3555143f06e61dd68e072f192f76f23bff40e5cc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Jul 2022 17:15:42 +0100 Subject: [PATCH 115/432] Load layouts without sequences. --- .../hosts/unreal/plugins/load/load_layout.py | 431 +++++++++--------- 1 file changed, 227 insertions(+), 204 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 361c3684fa..0dbaf0880a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -215,16 +215,17 @@ class LayoutLoader(plugin.Loader): actors.append(actor) - binding = None - for p in sequence.get_possessables(): - if p.get_name() == actor.get_name(): - binding = p - break + if sequence: + binding = None + for p in sequence.get_possessables(): + if p.get_name() == actor.get_name(): + binding = p + break - if not binding: - binding = sequence.add_possessable(actor) + if not binding: + binding = sequence.add_possessable(actor) - bindings.append(binding) + bindings.append(binding) return actors, bindings @@ -312,49 +313,50 @@ class LayoutLoader(plugin.Loader): actor.skeletal_mesh_component.animation_data.set_editor_property( 'anim_to_play', animation) - # Add animation to the sequencer - bindings = bindings_dict.get(instance_name) + if sequence: + # Add animation to the sequencer + bindings = bindings_dict.get(instance_name) - ar = unreal.AssetRegistryHelpers.get_asset_registry() + ar = unreal.AssetRegistryHelpers.get_asset_registry() - for binding in bindings: - tracks = binding.get_tracks() - track = None - track = tracks[0] if tracks else binding.add_track( - unreal.MovieSceneSkeletalAnimationTrack) + for binding in bindings: + tracks = binding.get_tracks() + track = None + track = tracks[0] if tracks else binding.add_track( + unreal.MovieSceneSkeletalAnimationTrack) - sections = track.get_sections() - section = None - if not sections: - section = track.add_section() - else: - section = sections[0] + sections = track.get_sections() + section = None + if not sections: + section = track.add_section() + else: + section = sections[0] + sec_params = section.get_editor_property('params') + curr_anim = sec_params.get_editor_property('animation') + + if curr_anim: + # Checks if the animation path has a container. + # If it does, it means that the animation is + # already in the sequencer. + anim_path = str(Path( + curr_anim.get_path_name()).parent + ).replace('\\', '/') + + _filter = unreal.ARFilter( + class_names=["AssetContainer"], + package_paths=[anim_path], + recursive_paths=False) + containers = ar.get_assets(_filter) + + if len(containers) > 0: + return + + section.set_range( + sequence.get_playback_start(), + sequence.get_playback_end()) sec_params = section.get_editor_property('params') - curr_anim = sec_params.get_editor_property('animation') - - if curr_anim: - # Checks if the animation path has a container. - # If it does, it means that the animation is already - # in the sequencer. - anim_path = str(Path( - curr_anim.get_path_name()).parent - ).replace('\\', '/') - - _filter = unreal.ARFilter( - class_names=["AssetContainer"], - package_paths=[anim_path], - recursive_paths=False) - containers = ar.get_assets(_filter) - - if len(containers) > 0: - return - - section.set_range( - sequence.get_playback_start(), - sequence.get_playback_end()) - sec_params = section.get_editor_property('params') - sec_params.set_editor_property('animation', animation) + sec_params.set_editor_property('animation', animation) @staticmethod def _generate_sequence(h, h_dir): @@ -617,6 +619,9 @@ class LayoutLoader(plugin.Loader): Returns: list(str): list of container content """ + # TODO: get option from OpenPype settings + create_sequences = False + # Create directory for asset and avalon container hierarchy = context.get('asset').get('data').get('parents') root = self.ASSET_ROOT @@ -637,85 +642,88 @@ class LayoutLoader(plugin.Loader): EditorAssetLibrary.make_directory(asset_dir) - # Create map for the shot, and create hierarchy of map. If the maps - # already exist, we will use them. master_level = None - if hierarchy: - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") + shot = None + sequences = [] level = f"{asset_dir}/{asset}_map.{asset}_map" EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") - if master_level: - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic + if create_sequences: + # Create map for the shot, and create hierarchy of map. If the maps + # already exist, we will use them. + if hierarchy: + h_dir = hierarchy_dir_list[0] + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + if not EditorAssetLibrary.does_asset_exist(master_level): + EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") + + if master_level: + EditorLevelLibrary.load_level(master_level) + EditorLevelUtils.add_level_to_world( + EditorLevelLibrary.get_editor_world(), + level, + unreal.LevelStreamingDynamic + ) + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(level) + + # Get all the sequences in the hierarchy. It will create them, if + # they don't exist. + frame_ranges = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): + root_content = EditorAssetLibrary.list_assets( + h_dir, recursive=False, include_folder=False) + + existing_sequences = [ + EditorAssetLibrary.find_asset_data(asset) + for asset in root_content + if EditorAssetLibrary.find_asset_data( + asset).get_class().get_name() == 'LevelSequence' + ] + + if not existing_sequences: + sequence, frame_range = self._generate_sequence(h, h_dir) + + sequences.append(sequence) + frame_ranges.append(frame_range) + else: + for e in existing_sequences: + sequences.append(e.get_asset()) + frame_ranges.append(( + e.get_asset().get_playback_start(), + e.get_asset().get_playback_end())) + + shot = tools.create_asset( + asset_name=asset, + package_path=asset_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() ) - EditorLevelLibrary.save_all_dirty_levels() + + # sequences and frame_ranges have the same length + for i in range(0, len(sequences) - 1): + self._set_sequence_hierarchy( + sequences[i], sequences[i + 1], + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + [level]) + + data = self._get_data(asset) + shot.set_display_rate( + unreal.FrameRate(data.get("fps"), 1.0)) + shot.set_playback_start(0) + shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) + if sequences: + self._set_sequence_hierarchy( + sequences[-1], shot, + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) + EditorLevelLibrary.load_level(level) - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. - sequences = [] - frame_ranges = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): - root_content = EditorAssetLibrary.list_assets( - h_dir, recursive=False, include_folder=False) - - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] - - if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) - - sequences.append(sequence) - frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) - - shot = tools.create_asset( - asset_name=asset, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - # sequences and frame_ranges have the same length - for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) - - data = self._get_data(asset) - shot.set_display_rate( - unreal.FrameRate(data.get("fps"), 1.0)) - shot.set_playback_start(0) - shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) - if sequences: - self._set_sequence_hierarchy( - sequences[-1], shot, - frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), - [level]) - - EditorLevelLibrary.load_level(level) - loaded_assets = self._process(self.fname, asset_dir, shot) for s in sequences: @@ -755,27 +763,31 @@ class LayoutLoader(plugin.Loader): return asset_content def update(self, container, representation): + # TODO: get option from OpenPype settings + create_sequences = False + ar = unreal.AssetRegistryHelpers.get_asset_registry() root = "/Game/OpenPype" asset_dir = container.get('namespace') - context = representation.get("context") - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + sequence = None + master_level = None - # # Create a temporary level to delete the layout level. - # EditorLevelLibrary.save_all_dirty_levels() - # EditorAssetLibrary.make_directory(f"{root}/tmp") - # tmp_level = f"{root}/tmp/temp_map" - # if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): - # EditorLevelLibrary.new_level(tmp_level) - # else: - # EditorLevelLibrary.load_level(tmp_level) + if create_sequences: + hierarchy = context.get('hierarchy').split("/") + h_dir = f"{root}/{hierarchy[0]}" + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[asset_dir], + recursive_paths=False) + sequences = ar.get_assets(filter) + sequence = sequences[0].get_asset() # Get layout level filter = unreal.ARFilter( @@ -783,11 +795,6 @@ class LayoutLoader(plugin.Loader): package_paths=[asset_dir], recursive_paths=False) levels = ar.get_assets(filter) - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(filter) layout_level = levels[0].get_editor_property('object_path') @@ -799,14 +806,14 @@ class LayoutLoader(plugin.Loader): for actor in actors: unreal.EditorLevelLibrary.destroy_actor(actor) - EditorLevelLibrary.save_current_level() + if create_sequences: + EditorLevelLibrary.save_current_level() EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") source_path = get_representation_path(representation) - loaded_assets = self._process( - source_path, asset_dir, sequences[0].get_asset()) + loaded_assets = self._process(source_path, asset_dir, sequence) data = { "representation": str(representation["_id"]), @@ -824,13 +831,18 @@ class LayoutLoader(plugin.Loader): for a in asset_content: EditorAssetLibrary.save_asset(a) - EditorLevelLibrary.load_level(master_level) + if master_level: + EditorLevelLibrary.load_level(master_level) def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout are used by other layouts. If not, delete the assets. """ + # TODO: get option from OpenPype settings + create_sequences = False + + root = "/Game/OpenPype" path = Path(container.get("namespace")) containers = unreal_pipeline.ls() @@ -841,7 +853,7 @@ class LayoutLoader(plugin.Loader): # Check if the assets have been loaded by other layouts, and deletes # them if they haven't. - for asset in container.get('loaded_assets'): + for asset in eval(container.get('loaded_assets')): layouts = [ lc for lc in layout_containers if asset in lc.get('loaded_assets')] @@ -849,71 +861,83 @@ class LayoutLoader(plugin.Loader): if not layouts: EditorAssetLibrary.delete_directory(str(Path(asset).parent)) - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - root = "/Game/OpenPype" - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - ar = unreal.AssetRegistryHelpers.get_asset_registry() - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_editor_property('object_path') + # Delete the parent folder if there aren't any more layouts in it. + asset_content = EditorAssetLibrary.list_assets( + str(Path(asset).parent.parent), recursive=False, include_folder=True + ) - sequences = [master_sequence] + if len(asset_content) == 0: + EditorAssetLibrary.delete_directory(str(Path(asset).parent.parent)) - parent = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if ss.get_sequence().get_name() == container.get('asset'): - parent = s - subscene_track.remove_section(ss) - break - sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: - ss.set_row_index(i) - i += 1 + master_sequence = None + master_level = None + sequences = [] - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): + if create_sequences: + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + namespace = container.get('namespace').replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + ar = unreal.AssetRegistryHelpers.get_asset_registry() + _filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + sequences = ar.get_assets(_filter) + master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_editor_property('object_path') + + sequences = [master_sequence] + + parent = None + for s in sequences: + tracks = s.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + if subscene_track: + sections = subscene_track.get_sections() + for ss in sections: + if ss.get_sequence().get_name() == container.get('asset'): + parent = s + subscene_track.remove_section(ss) + break + sequences.append(ss.get_sequence()) + # Update subscenes indexes. + i = 0 + for ss in sections: + ss.set_row_index(i) i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() - if parent: - break - assert parent, "Could not find the parent sequence" + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() + if parent: + break + + assert parent, "Could not find the parent sequence" # Create a temporary level to delete the layout level. EditorLevelLibrary.save_all_dirty_levels() @@ -927,10 +951,9 @@ class LayoutLoader(plugin.Loader): # Delete the layout directory. EditorAssetLibrary.delete_directory(str(path)) - EditorLevelLibrary.load_level(master_level) - EditorAssetLibrary.delete_directory(f"{root}/tmp") - - EditorLevelLibrary.save_current_level() + if create_sequences: + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") # Delete the parent folder if there aren't any more layouts in it. asset_content = EditorAssetLibrary.list_assets( From ef35c17ba484de54ce43998a437e7775e1d5b557 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Jul 2022 17:19:54 +0100 Subject: [PATCH 116/432] Hound fixes --- .../hosts/unreal/plugins/load/load_layout.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0dbaf0880a..7c8f78bd9a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -9,7 +9,8 @@ from unreal import EditorLevelLibrary from unreal import EditorLevelUtils from unreal import AssetToolsHelpers from unreal import FBXImportType -from unreal import MathLibrary as umath +from unreal import MovieSceneLevelVisibilityTrack +from unreal import MovieSceneSubTrack from bson.objectid import ObjectId @@ -650,8 +651,8 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") if create_sequences: - # Create map for the shot, and create hierarchy of map. If the maps - # already exist, we will use them. + # Create map for the shot, and create hierarchy of map. If the + # maps already exist, we will use them. if hierarchy: h_dir = hierarchy_dir_list[0] h_asset = hierarchy[0] @@ -861,13 +862,16 @@ class LayoutLoader(plugin.Loader): if not layouts: EditorAssetLibrary.delete_directory(str(Path(asset).parent)) - # Delete the parent folder if there aren't any more layouts in it. + # Delete the parent folder if there aren't any more + # layouts in it. asset_content = EditorAssetLibrary.list_assets( - str(Path(asset).parent.parent), recursive=False, include_folder=True + str(Path(asset).parent.parent), recursive=False, + include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(str(Path(asset).parent.parent)) + EditorAssetLibrary.delete_directory( + str(Path(asset).parent.parent)) master_sequence = None master_level = None @@ -875,8 +879,8 @@ class LayoutLoader(plugin.Loader): if create_sequences: # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. + # We need to traverse the hierarchy from the master sequence to + # find the level sequence. namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -901,15 +905,16 @@ class LayoutLoader(plugin.Loader): subscene_track = None visibility_track = None for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + if t.get_class() == MovieSceneSubTrack.static_class(): subscene_track = t if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): + MovieSceneLevelVisibilityTrack.static_class()): visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if ss.get_sequence().get_name() == container.get('asset'): + if (ss.get_sequence().get_name() == + container.get('asset')): parent = s subscene_track.remove_section(ss) break From 5de5a37475f4fbab6d02d7a83776adb8646dbfda Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Jul 2022 17:21:10 +0100 Subject: [PATCH 117/432] More hound fixes --- openpype/hosts/unreal/plugins/load/load_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 7c8f78bd9a..f600a131c5 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -879,7 +879,7 @@ class LayoutLoader(plugin.Loader): if create_sequences: # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to + # We need to traverse the hierarchy from the master sequence to # find the level sequence. namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] From a2361d8283f55e91e7e5c2a9c30a4d8cea4cd7fc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Jul 2022 17:27:37 +0100 Subject: [PATCH 118/432] Set conversion settings for abc Skeletal Meshes --- .../plugins/load/load_alembic_skeletalmesh.py | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py index b2c3889f68..d51a3ae0af 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -20,6 +20,34 @@ class SkeletalMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + def get_task(self, filename, asset_dir, asset_name, replace): + task = unreal.AssetImportTask() + options = unreal.AbcImportSettings() + sm_settings = unreal.AbcStaticMeshSettings() + conversion_settings = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=False, flip_v=False, + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0]) + + task.set_editor_property('filename', filename) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', replace) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + # set import options here + # Unreal 4.24 ignores the settings. It works with Unreal 4.26 + options.set_editor_property( + 'import_type', unreal.AlembicImportType.SKELETAL) + + options.static_mesh_settings = sm_settings + options.conversion_settings = conversion_settings + task.options = options + + return task + def load(self, context, name, namespace, data): """Load and containerise representation into Content Browser. @@ -59,22 +87,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = unreal.AssetImportTask() + task = self.get_task(self.fname, asset_dir, asset_name, False) - task.set_editor_property('filename', self.fname) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options = unreal.AbcImportSettings() - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - task.options = options unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 # Create Asset Container @@ -110,23 +124,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): source_path = get_representation_path(representation) destination_path = container["namespace"] - task = unreal.AssetImportTask() + task = self.get_task(source_path, destination_path, name, True) - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - # strip suffix - task.set_editor_property('destination_name', name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options = unreal.AbcImportSettings() - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - task.options = options # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) container_path = "{}/{}".format(container["namespace"], From 46e2f629299a66b388d449d3ec29b2e296307348 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Jul 2022 17:34:56 +0100 Subject: [PATCH 119/432] Avoid overwriting and reloading of loaded assets --- .../plugins/load/load_alembic_skeletalmesh.py | 16 +++++++++------- .../plugins/load/load_alembic_staticmesh.py | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py index d51a3ae0af..9fe5f3ab4b 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -78,22 +78,24 @@ class SkeletalMeshAlembicLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version').get('name') tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}_v{version:03d}", suffix="") container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + task = self.get_task(self.fname, asset_dir, asset_name, False) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) data = { "schema": "openpype:container-2.0", diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 42abbda80f..50e498dbb0 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -80,22 +80,24 @@ class StaticMeshAlembicLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version').get('name') tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}_v{version:03d}", suffix="") container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + task = self.get_task(self.fname, asset_dir, asset_name, False) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) data = { "schema": "openpype:container-2.0", From 08afb46ab4f8cbfcbed481a2a03665c47b29a49f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Jul 2022 12:19:04 +0200 Subject: [PATCH 120/432] trayp: implementing variants from settings --- .../plugins/create/create_editorial.py | 125 +++++++++++------- .../project_settings/traypublisher.json | 24 +++- .../schema_project_traypublisher.json | 26 ++++ 3 files changed, 127 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 643c8a2a84..fdcdd74c88 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -130,9 +130,12 @@ or updating already created. Publishing will create OTIO file. self.default_variants = self._creator_settings["default_variants"] def create(self, subset_name, instance_data, pre_create_data): + allowed_variants = self._get_allowed_variants(pre_create_data) + clip_instance_properties = { k: v for k, v in pre_create_data.items() if k != "sequence_filepath_data" + if k not in self._creator_settings["variants"] } # Create otio editorial instance asset_name = instance_data["asset"] @@ -162,7 +165,9 @@ or updating already created. Publishing will create OTIO file. self._get_clip_instances( otio_timeline, clip_instance_properties, - variant=instance_data["variant"] + variant_name=instance_data["variant"], + variants=allowed_variants + ) def _create_otio_instance(self, subset_name, data, pre_create_data): @@ -202,10 +207,9 @@ or updating already created. Publishing will create OTIO file. self, otio_timeline, clip_instance_properties, - variant + variant_name, + variants ): - family = "plate" - # get clip instance properties parent_asset_name = clip_instance_properties["parent_asset_name"] handle_start = clip_instance_properties["handle_start"] @@ -273,53 +277,73 @@ or updating already created. Publishing will create OTIO file. ) frame_end = frame_start + (clip_duration - 1) - # subset name - self.log.info( - f"__ variant: {variant}") + for family, _vconf in variants.items(): + self.log.debug(f"__ family: {family}") + self.log.debug(f"__ _vconf: {_vconf}") - subset_name = "{}{}".format( - family, variant.capitalize() - ) - label = "{}_{}".format( - clip_name, - subset_name - ) - # create shared new instance data - instance_data = { - "label": label, - "variant": variant, - "family": family, - "families": ["clip"], - "subset": subset_name, + families = ["clip"] - # HACK: just for temporal bug workaround - # TODO: should loockup shot name for update - "asset": parent_asset_name, - "name": clip_name, - "task": "", + # add review family if defined + if _vconf.get("review"): + families.append("review") - # parent time properties - "trackStartFrame": track_start_frame, + # subset name + subset_name = "{}{}".format( + family, variant_name.capitalize() + ) + label = "{}_{}".format( + clip_name, + subset_name + ) - # creator_attributes - "creator_attributes": { - "asset_name": clip_name, - "timeline_offset": timeline_offset, - "workfile_start_frame": workfile_start_frame, - "frameStart": frame_start, - "frameEnd": frame_end, - "fps": fps, - "handle_start": handle_start, - "handle_end": handle_end, - "clipIn": clip_in, - "clipOut": clip_out, - "sourceIn": source_in, - "sourceOut": source_out, + # create shared new instance data + instance_data = { + "label": label, + "variant": variant_name, + "family": family, + "families": families, + "subset": subset_name, + + # HACK: just for temporal bug workaround + # TODO: should loockup shot name for update + "asset": parent_asset_name, + "name": clip_name, + "task": "", + + # parent time properties + "trackStartFrame": track_start_frame, + + # allowed file ext from settings + "filterExt": _vconf["filter_ext"], + + # creator_attributes + "creator_attributes": { + "asset_name": clip_name, + "timeline_offset": timeline_offset, + "workfile_start_frame": workfile_start_frame, + "frameStart": frame_start, + "frameEnd": frame_end, + "fps": fps, + "handle_start": handle_start, + "handle_end": handle_end, + "clipIn": clip_in, + "clipOut": clip_out, + "sourceIn": source_in, + "sourceOut": source_out, + } } - } - c_instance = editorial_clip_creator.create(instance_data, {}) - self.log.debug(f"{pformat(dict(c_instance.data))}") + c_instance = editorial_clip_creator.create( + instance_data, {}) + self.log.debug(f"{pformat(dict(c_instance.data))}") + + def _get_allowed_variants(self, pre_create_data): + self.log.debug(f"__ pre_create_data: {pre_create_data}") + return { + key: value + for key, value in self._creator_settings["variants"].items() + if pre_create_data[key] + } def _validate_clip_for_processing(self, clip): if clip.name is None: @@ -370,15 +394,22 @@ or updating already created. Publishing will create OTIO file. allow_sequences=False, label="Filepath", ), - UILabelDef("Clip instance attributes"), - UISeparatorDef(), # TODO: perhpas better would be timecode and fps input NumberDef( "timeline_offset", default=0, label="Timeline offset" ), + UISeparatorDef(), + UILabelDef("Clip instance attributes"), UISeparatorDef() ] + # add variants swithers + attr_defs.extend( + BoolDef(_var, label=_var) + for _var in self._creator_settings["variants"] + ) + attr_defs.append(UISeparatorDef()) + attr_defs.extend(CLIP_ATTR_DEFS) return attr_defs diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index ef6dc5fec7..7f572cf1fb 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -36,7 +36,29 @@ "editorialSimple": { "default_variants": [ "Main" - ] + ], + "variants": { + "reference": { + "review": true, + "filter_ext": [ + "mov", + "mp4" + ] + }, + "plate": { + "review": false, + "filter_ext": [ + "mov", + "mp4" + ] + }, + "audio": { + "review": false, + "filter_ext": [ + "wav" + ] + } + } } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 11ae0e65a7..38597eeb97 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -103,6 +103,32 @@ "object_type": { "type": "text" } + }, + { + "type": "splitter" + }, + { + "key": "variants", + "label": "Variants", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "review", + "label": "Review", + "default": true + }, + { + "type": "list", + "key": "filter_ext", + "label": "Allowed input file types", + "object_type": "text" + } + ] + } } ] } From 75285652ff7cacd999b4aa87213e7f0b52955c05 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Jul 2022 15:24:00 +0200 Subject: [PATCH 121/432] trayp: reworking settings presets --- .../plugins/create/create_editorial.py | 25 +++++++++++-------- .../project_settings/traypublisher.json | 15 ++++++----- .../schema_project_traypublisher.json | 19 ++++++++++---- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index fdcdd74c88..c68c094218 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -56,9 +56,10 @@ CLIP_ATTR_DEFS = [ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): - identifier = "editorialClip" + identifier = "editorial_clip" family = "clip" host_name = "traypublisher" + label = "Editorial Clip" def __init__( self, project_settings, *args, **kwargs @@ -102,7 +103,7 @@ class EditorialSimpleCreator(TrayPublishCreator): label = "Editorial Simple" family = "editorial" - identifier = "editorialSimple" + identifier = "editorial_simple" default_variants = [ "main" ] @@ -130,12 +131,14 @@ or updating already created. Publishing will create OTIO file. self.default_variants = self._creator_settings["default_variants"] def create(self, subset_name, instance_data, pre_create_data): - allowed_variants = self._get_allowed_variants(pre_create_data) + allowed_variants = self._get_allowed_family_presets(pre_create_data) clip_instance_properties = { k: v for k, v in pre_create_data.items() if k != "sequence_filepath_data" - if k not in self._creator_settings["variants"] + if k not in [ + i["family"] for i in self._creator_settings["family_presets"] + ] } # Create otio editorial instance asset_name = instance_data["asset"] @@ -220,7 +223,7 @@ or updating already created. Publishing will create OTIO file. self.asset_name_check = [] - editorial_clip_creator = self.create_context.creators["editorialClip"] + editorial_clip_creator = self.create_context.creators["editorial_clip"] tracks = otio_timeline.each_child( descended_from_type=otio.schema.Track @@ -337,12 +340,12 @@ or updating already created. Publishing will create OTIO file. instance_data, {}) self.log.debug(f"{pformat(dict(c_instance.data))}") - def _get_allowed_variants(self, pre_create_data): + def _get_allowed_family_presets(self, pre_create_data): self.log.debug(f"__ pre_create_data: {pre_create_data}") return { - key: value - for key, value in self._creator_settings["variants"].items() - if pre_create_data[key] + preset["family"]: preset + for preset in self._creator_settings["family_presets"] + if pre_create_data[preset["family"]] } def _validate_clip_for_processing(self, clip): @@ -406,8 +409,8 @@ or updating already created. Publishing will create OTIO file. ] # add variants swithers attr_defs.extend( - BoolDef(_var, label=_var) - for _var in self._creator_settings["variants"] + BoolDef(_var["family"], label=_var["family"]) + for _var in self._creator_settings["family_presets"] ) attr_defs.append(UISeparatorDef()) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 7f572cf1fb..2717ab6869 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -33,32 +33,35 @@ } ], "editorial_creators": { - "editorialSimple": { + "editorial_simple": { "default_variants": [ "Main" ], - "variants": { - "reference": { + "family_presets": [ + { + "family": "reference", "review": true, "filter_ext": [ "mov", "mp4" ] }, - "plate": { + { + "family": "plate", "review": false, "filter_ext": [ "mov", "mp4" ] }, - "audio": { + { + "family": "audio", "review": false, "filter_ext": [ "wav" ] } - } + ] } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 38597eeb97..4c0aaf41e7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -90,7 +90,7 @@ { "type": "dict", "collapsible": true, - "key": "editorialSimple", + "key": "editorial_simple", "label": "Editorial simple creator", "use_label_wrap": true, "collapsible_key": true, @@ -108,13 +108,22 @@ "type": "splitter" }, { - "key": "variants", - "label": "Variants", - "type": "dict-modifiable", - "highlight_content": true, + "type": "list", + "key": "family_presets", + "label": "Family presets", "object_type": { "type": "dict", "children": [ + { + "type": "enum", + "key": "family", + "label": "Family", + "enum_items": [ + {"reference": "reference"}, + {"plate": "plate"}, + {"audio": "audio"} + ] + }, { "type": "boolean", "key": "review", From 4b42e66c211a86d7e2e93fffe03b585e75103571 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Jul 2022 15:55:30 +0200 Subject: [PATCH 122/432] trayp: adding `shot` instance --- .../plugins/create/create_editorial.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index c68c094218..6dbcf694cb 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -284,12 +284,6 @@ or updating already created. Publishing will create OTIO file. self.log.debug(f"__ family: {family}") self.log.debug(f"__ _vconf: {_vconf}") - families = ["clip"] - - # add review family if defined - if _vconf.get("review"): - families.append("review") - # subset name subset_name = "{}{}".format( family, variant_name.capitalize() @@ -304,7 +298,7 @@ or updating already created. Publishing will create OTIO file. "label": label, "variant": variant_name, "family": family, - "families": families, + "families": [], "subset": subset_name, # HACK: just for temporal bug workaround @@ -316,9 +310,6 @@ or updating already created. Publishing will create OTIO file. # parent time properties "trackStartFrame": track_start_frame, - # allowed file ext from settings - "filterExt": _vconf["filter_ext"], - # creator_attributes "creator_attributes": { "asset_name": clip_name, @@ -335,6 +326,16 @@ or updating already created. Publishing will create OTIO file. "sourceOut": source_out, } } + # add file extension filter only if it is not shot family + if family != "shot": + families = ["clip"] + # add review family if defined + if _vconf.get("review"): + families.append("review") + instance_data.update({ + "filterExt": _vconf["filter_ext"], + "families": families + }) c_instance = editorial_clip_creator.create( instance_data, {}) @@ -342,11 +343,13 @@ or updating already created. Publishing will create OTIO file. def _get_allowed_family_presets(self, pre_create_data): self.log.debug(f"__ pre_create_data: {pre_create_data}") - return { + return_dict = { preset["family"]: preset for preset in self._creator_settings["family_presets"] if pre_create_data[preset["family"]] } + return_dict["shot"] = {} + return return_dict def _validate_clip_for_processing(self, clip): if clip.name is None: From 2e8e4a0ee9724f0345038f1b9a2d78954ecf476d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Jul 2022 16:31:03 +0100 Subject: [PATCH 123/432] Added setting to generate sequences for layouts --- openpype/hosts/unreal/plugins/load/load_layout.py | 13 +++++++------ .../settings/defaults/project_settings/unreal.json | 1 + .../projects_schema/schema_project_unreal.json | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index f600a131c5..727488ee66 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -23,6 +23,7 @@ from openpype.pipeline import ( legacy_io, ) from openpype.api import get_asset +from openpype.api import get_current_project_settings from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -620,8 +621,8 @@ class LayoutLoader(plugin.Loader): Returns: list(str): list of container content """ - # TODO: get option from OpenPype settings - create_sequences = False + data = get_current_project_settings() + create_sequences = data["unreal"]["level_sequences_for_layouts"] # Create directory for asset and avalon container hierarchy = context.get('asset').get('data').get('parents') @@ -764,8 +765,8 @@ class LayoutLoader(plugin.Loader): return asset_content def update(self, container, representation): - # TODO: get option from OpenPype settings - create_sequences = False + data = get_current_project_settings() + create_sequences = data["unreal"]["level_sequences_for_layouts"] ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -840,8 +841,8 @@ class LayoutLoader(plugin.Loader): Delete the layout. First, check if the assets loaded with the layout are used by other layouts. If not, delete the assets. """ - # TODO: get option from OpenPype settings - create_sequences = False + data = get_current_project_settings() + create_sequences = data["unreal"]["level_sequences_for_layouts"] root = "/Game/OpenPype" path = Path(container.get("namespace")) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index dad61cd1f0..c5f5cdf719 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,4 +1,5 @@ { + "level_sequences_for_layouts": false, "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 4e197e9fc8..d26b5c1ccf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -5,6 +5,11 @@ "label": "Unreal Engine", "is_file": true, "children": [ + { + "type": "boolean", + "key": "level_sequences_for_layouts", + "label": "Generate level sequences when loading layouts" + }, { "type": "dict", "collapsible": true, From a3e48b558e9a82334454ae4678a04b860bc9e6ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Jul 2022 18:05:42 +0200 Subject: [PATCH 124/432] trayp: has parent on instance data --- .../plugins/create/create_editorial.py | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 6dbcf694cb..d0ce7fa452 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -61,6 +61,8 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): host_name = "traypublisher" label = "Editorial Clip" + has_parent = False + def __init__( self, project_settings, *args, **kwargs ): @@ -69,6 +71,8 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): ) def create(self, instance_data, source_data): + self.has_parent = source_data.get("has_parent") + self.log.info(f"instance_data: {instance_data}") subset_name = instance_data["subset"] family = instance_data["family"] @@ -95,7 +99,8 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): label="Asset name", ) ] - attr_defs.extend(CLIP_ATTR_DEFS) + if not self.has_parent: + attr_defs.extend(CLIP_ATTR_DEFS) return attr_defs @@ -131,7 +136,8 @@ or updating already created. Publishing will create OTIO file. self.default_variants = self._creator_settings["default_variants"] def create(self, subset_name, instance_data, pre_create_data): - allowed_variants = self._get_allowed_family_presets(pre_create_data) + allowed_family_presets = self._get_allowed_family_presets( + pre_create_data) clip_instance_properties = { k: v for k, v in pre_create_data.items() @@ -169,7 +175,7 @@ or updating already created. Publishing will create OTIO file. otio_timeline, clip_instance_properties, variant_name=instance_data["variant"], - variants=allowed_variants + family_presets=allowed_family_presets ) @@ -211,7 +217,7 @@ or updating already created. Publishing will create OTIO file. otio_timeline, clip_instance_properties, variant_name, - variants + family_presets ): # get clip instance properties parent_asset_name = clip_instance_properties["parent_asset_name"] @@ -280,9 +286,12 @@ or updating already created. Publishing will create OTIO file. ) frame_end = frame_start + (clip_duration - 1) - for family, _vconf in variants.items(): + parent_instance_label = None + for _fpreset in family_presets: + source_data = {} + family = _fpreset["family"] self.log.debug(f"__ family: {family}") - self.log.debug(f"__ _vconf: {_vconf}") + self.log.debug(f"__ _fpreset: {_fpreset}") # subset name subset_name = "{}{}".format( @@ -299,6 +308,7 @@ or updating already created. Publishing will create OTIO file. "variant": variant_name, "family": family, "families": [], + "group": family.capitalize(), "subset": subset_name, # HACK: just for temporal bug workaround @@ -327,29 +337,37 @@ or updating already created. Publishing will create OTIO file. } } # add file extension filter only if it is not shot family - if family != "shot": + if family == "shot": + parent_instance_label = label + source_data + else: families = ["clip"] # add review family if defined - if _vconf.get("review"): + if _fpreset.get("review"): families.append("review") instance_data.update({ - "filterExt": _vconf["filter_ext"], - "families": families + "filterExt": _fpreset["filter_ext"], + "families": families, + "creator_attributes": { + "asset_name": clip_name, + "parent_instance": parent_instance_label + } }) + source_data["has_parent"] = True c_instance = editorial_clip_creator.create( - instance_data, {}) + instance_data, source_data) self.log.debug(f"{pformat(dict(c_instance.data))}") def _get_allowed_family_presets(self, pre_create_data): self.log.debug(f"__ pre_create_data: {pre_create_data}") - return_dict = { - preset["family"]: preset - for preset in self._creator_settings["family_presets"] - if pre_create_data[preset["family"]] - } - return_dict["shot"] = {} - return return_dict + return [ + {"family": "shot"}, + *[ + preset for preset in self._creator_settings["family_presets"] + if pre_create_data[preset["family"]] + ] + ] def _validate_clip_for_processing(self, clip): if clip.name is None: From eae292edc856be2e81ff78787929835c5f7fd91c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Jul 2022 10:10:31 +0200 Subject: [PATCH 125/432] trayp: adding selection rather then project --- .../hosts/traypublisher/plugins/create/create_editorial.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index d0ce7fa452..afb1368bef 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -28,7 +28,7 @@ CLIP_ATTR_DEFS = [ EnumDef( "fps", items={ - "from_project": "From project", + "from_selection": "From selection", 23.997: "23.976", 24: "24", 25: "25", @@ -152,7 +152,7 @@ or updating already created. Publishing will create OTIO file. self.log.info(pre_create_data["fps"]) - if pre_create_data["fps"] == "from_project": + if pre_create_data["fps"] == "from_selection": # get asset doc data attributes fps = asset_doc["data"]["fps"] else: @@ -339,7 +339,6 @@ or updating already created. Publishing will create OTIO file. # add file extension filter only if it is not shot family if family == "shot": parent_instance_label = label - source_data else: families = ["clip"] # add review family if defined From feeee29660b33d343459aa4f835b48c0c306a670 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Jul 2022 12:24:56 +0200 Subject: [PATCH 126/432] trayp: adding variant to presets, also renaming `reference` family to `review` --- .../settings/defaults/project_settings/traypublisher.json | 5 ++++- .../projects_schema/schema_project_traypublisher.json | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 2717ab6869..13939a87bc 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -39,7 +39,8 @@ ], "family_presets": [ { - "family": "reference", + "family": "review", + "variant": "Reference", "review": true, "filter_ext": [ "mov", @@ -48,6 +49,7 @@ }, { "family": "plate", + "variant": "", "review": false, "filter_ext": [ "mov", @@ -56,6 +58,7 @@ }, { "family": "audio", + "variant": "", "review": false, "filter_ext": [ "wav" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 4c0aaf41e7..8f1caceb49 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -119,11 +119,17 @@ "key": "family", "label": "Family", "enum_items": [ - {"reference": "reference"}, + {"review": "review"}, {"plate": "plate"}, {"audio": "audio"} ] }, + { + "type": "text", + "key": "variant", + "label": "Variant", + "placeholder": "< Inherited >" + }, { "type": "boolean", "key": "review", From 43fa5f55cb904def429fa87170814cb86738f908 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Jul 2022 12:25:22 +0200 Subject: [PATCH 127/432] trayp: adding audio to review families --- .../traypublisher/plugins/publish/collect_review_family.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py b/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py index 965e251527..54ba12c66c 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py @@ -16,7 +16,8 @@ class CollectReviewFamily( "image", "render", "plate", - "review" + "review", + "audio" ] def process(self, instance): From f94b8e9e3db90ded2d09cade25c2665fe9a4c255 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Jul 2022 12:25:50 +0200 Subject: [PATCH 128/432] trayp: editorial creators swarming --- .../plugins/create/create_editorial.py | 131 ++++++++++++------ 1 file changed, 90 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index afb1368bef..f373d2ac7a 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -55,34 +55,26 @@ CLIP_ATTR_DEFS = [ ] -class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): - identifier = "editorial_clip" - family = "clip" +class EditorialClipInstanceCreatorBase(InvisibleTrayPublishCreator): host_name = "traypublisher" - label = "Editorial Clip" - - has_parent = False def __init__( self, project_settings, *args, **kwargs ): - super(EditorialClipInstanceCreator, self).__init__( + super(EditorialClipInstanceCreatorBase, self).__init__( project_settings, *args, **kwargs ) - def create(self, instance_data, source_data): - self.has_parent = source_data.get("has_parent") - + def create(self, instance_data, source_data=None): self.log.info(f"instance_data: {instance_data}") subset_name = instance_data["subset"] - family = instance_data["family"] - return self._create_instance(subset_name, family, instance_data) + return self._create_instance(subset_name, instance_data) - def _create_instance(self, subset_name, family, data): + def _create_instance(self, subset_name, data): # Create new instance - new_instance = CreatedInstance(family, subset_name, data, self) + new_instance = CreatedInstance(self.family, subset_name, data, self) self.log.info(f"instance_data: {pformat(new_instance.data)}") # Host implementation of storing metadata about instance @@ -92,6 +84,19 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): return new_instance + +class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): + identifier = "editorial_shot" + family = "shot" + label = "Editorial Shot" + + def __init__( + self, project_settings, *args, **kwargs + ): + super(EditorialShotInstanceCreator, self).__init__( + project_settings, *args, **kwargs + ) + def get_instance_attr_defs(self): attr_defs = [ TextDef( @@ -99,11 +104,49 @@ class EditorialClipInstanceCreator(InvisibleTrayPublishCreator): label="Asset name", ) ] - if not self.has_parent: - attr_defs.extend(CLIP_ATTR_DEFS) + attr_defs.extend(CLIP_ATTR_DEFS) return attr_defs +class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): + identifier = "editorial_plate" + family = "plate" + label = "Editorial Plate" + + def __init__( + self, project_settings, *args, **kwargs + ): + super(EditorialPlateInstanceCreator, self).__init__( + project_settings, *args, **kwargs + ) + + +class EditorialAudioInstanceCreator(EditorialClipInstanceCreatorBase): + identifier = "editorial_audio" + family = "audio" + label = "Editorial Audio" + + def __init__( + self, project_settings, *args, **kwargs + ): + super(EditorialAudioInstanceCreator, self).__init__( + project_settings, *args, **kwargs + ) + + +class EditorialReviewInstanceCreator(EditorialClipInstanceCreatorBase): + identifier = "editorial_review" + family = "review" + label = "Editorial Review" + + def __init__( + self, project_settings, *args, **kwargs + ): + super(EditorialReviewInstanceCreator, self).__init__( + project_settings, *args, **kwargs + ) + + class EditorialSimpleCreator(TrayPublishCreator): label = "Editorial Simple" @@ -229,8 +272,6 @@ or updating already created. Publishing will create OTIO file. self.asset_name_check = [] - editorial_clip_creator = self.create_context.creators["editorial_clip"] - tracks = otio_timeline.each_child( descended_from_type=otio.schema.Track ) @@ -287,15 +328,17 @@ or updating already created. Publishing will create OTIO file. frame_end = frame_start + (clip_duration - 1) parent_instance_label = None + parent_instance_id = None for _fpreset in family_presets: - source_data = {} + # get variant name from preset or from inharitance + _variant_name = _fpreset.get("variant") or variant_name family = _fpreset["family"] self.log.debug(f"__ family: {family}") self.log.debug(f"__ _fpreset: {_fpreset}") # subset name subset_name = "{}{}".format( - family, variant_name.capitalize() + family, _variant_name.capitalize() ) label = "{}_{}".format( clip_name, @@ -305,10 +348,8 @@ or updating already created. Publishing will create OTIO file. # create shared new instance data instance_data = { "label": label, - "variant": variant_name, + "variant": _variant_name, "family": family, - "families": [], - "group": family.capitalize(), "subset": subset_name, # HACK: just for temporal bug workaround @@ -319,43 +360,51 @@ or updating already created. Publishing will create OTIO file. # parent time properties "trackStartFrame": track_start_frame, + "timelineOffset": timeline_offset, # creator_attributes "creator_attributes": { "asset_name": clip_name, - "timeline_offset": timeline_offset, "workfile_start_frame": workfile_start_frame, - "frameStart": frame_start, - "frameEnd": frame_end, + "frameStart": int(frame_start), + "frameEnd": int(frame_end), "fps": fps, - "handle_start": handle_start, - "handle_end": handle_end, - "clipIn": clip_in, - "clipOut": clip_out, - "sourceIn": source_in, - "sourceOut": source_out, + "handle_start": int(handle_start), + "handle_end": int(handle_end), + "clipIn": int(clip_in), + "clipOut": int(clip_out), + "sourceIn": int(source_in), + "sourceOut": int(source_out), } } # add file extension filter only if it is not shot family if family == "shot": + c_instance = self.create_context.creators[ + "editorial_shot"].create( + instance_data) parent_instance_label = label + parent_instance_id = c_instance.data["instance_id"] else: - families = ["clip"] # add review family if defined - if _fpreset.get("review"): - families.append("review") instance_data.update({ "filterExt": _fpreset["filter_ext"], - "families": families, + "parent_instance_id": parent_instance_id, "creator_attributes": { - "asset_name": clip_name, "parent_instance": parent_instance_label + }, + "publish_attributes": { + "CollectReviewFamily": { + "add_review_family": _fpreset.get("review") + } } }) - source_data["has_parent"] = True - c_instance = editorial_clip_creator.create( - instance_data, source_data) + creator_identifier = f"editorial_{family}" + editorial_clip_creator = self.create_context.creators[ + creator_identifier] + c_instance = editorial_clip_creator.create( + instance_data) + self.log.debug(f"{pformat(dict(c_instance.data))}") def _get_allowed_family_presets(self, pre_create_data): @@ -435,4 +484,4 @@ or updating already created. Publishing will create OTIO file. attr_defs.append(UISeparatorDef()) attr_defs.extend(CLIP_ATTR_DEFS) - return attr_defs + return attr_defs \ No newline at end of file From 3cb9748613ef8cf8fc9a563e8df93c11b275a7d6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Jul 2022 13:23:40 +0200 Subject: [PATCH 129/432] trayp: editorial settings for shot metadata --- .../project_settings/traypublisher.json | 30 +++ .../schema_project_traypublisher.json | 185 ++++++++++++++---- 2 files changed, 180 insertions(+), 35 deletions(-) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index ee8f90df7f..93f6420c21 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -232,6 +232,36 @@ "default_variants": [ "Main" ], + "clip_name_tokenizer": { + "_sequence_": "(sc\\d{3})", + "_shot_": "(sh\\d{3})" + }, + "shot_rename": { + "enabled": true, + "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}" + }, + "shot_hierarchy": { + "enabled": true, + "parents_path": "{project}/{folder}/{sequence}", + "parents": [ + { + "type": "project", + "name": "projekt", + "value": "{projekt[name]}" + }, + { + "type": "folder", + "name": "folder", + "value": "shots" + }, + { + "type": "sequence", + "name": "sequence", + "value": "{_sequence_}" + } + ] + }, + "shot_add_tasks": {}, "family_presets": [ { "family": "review", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 8f1caceb49..8d95cb19a9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -108,42 +108,157 @@ "type": "splitter" }, { - "type": "list", - "key": "family_presets", - "label": "Family presets", - "object_type": { - "type": "dict", - "children": [ - { - "type": "enum", - "key": "family", - "label": "Family", - "enum_items": [ - {"review": "review"}, - {"plate": "plate"}, - {"audio": "audio"} - ] - }, - { - "type": "text", - "key": "variant", - "label": "Variant", - "placeholder": "< Inherited >" - }, - { - "type": "boolean", - "key": "review", - "label": "Review", - "default": true - }, - { - "type": "list", - "key": "filter_ext", - "label": "Allowed input file types", - "object_type": "text" + "type": "collapsible-wrap", + "label": "Shot metadata creator", + "collapsible": true, + "collapsed": true, + "children": [ + { + "key": "clip_name_tokenizer", + "label": "Clip name tokenizer", + "type": "dict-modifiable", + "highlight_content": true, + "tooltip": "Using Regex expression to create tokens. \nThose can be used later in \"Shot rename\" creator \nor \"Shot hierarchy\". \n\nTokens should be decorated with \"_\" on each side", + "object_type": { + "type": "text" } - ] - } + }, + { + "type": "dict", + "key": "shot_rename", + "label": "Shot rename", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "shot_rename_template", + "label": "Shot rename template", + "tooltip":"Template only supports Anatomy keys and Tokens \nfrom \"Clip name tokenizer\"" + } + ] + }, + { + "type": "dict", + "key": "shot_hierarchy", + "label": "Shot hierarchy", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "parents_path", + "label": "Parents path template", + "tooltip": "Using keys from \"Token to parent convertor\" or tokens directly" + }, + { + "key": "parents", + "label": "Token to parent convertor", + "type": "list", + "highlight_content": true, + "tooltip": "The left side is key to be used in template. \nThe right is value build from Tokens comming from \n\"Clip name tokenizer\"", + "object_type": { + "type": "dict", + "children": [ + { + "type": "enum", + "key": "type", + "label": "Parent type", + "enum_items": [ + {"project": "Project"}, + {"folder": "Folder"}, + {"episode": "Episode"}, + {"sequence": "Sequence"} + ] + }, + { + "type": "text", + "key": "name", + "label": "Parent token name", + "tooltip": "Unique name used in \"Parent path template\"" + }, + { + "type": "text", + "key": "value", + "label": "Parent name value", + "tooltip": "Template where any text, Anatomy keys and Tokens could be used" + } + ] + } + } + ] + }, + { + "key": "shot_add_tasks", + "label": "Add tasks to shot", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "task-types-enum", + "key": "type", + "label": "Task type" + } + ] + } + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Shot's subset creator", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "list", + "key": "family_presets", + "label": "Family presets", + "object_type": { + "type": "dict", + "children": [ + { + "type": "enum", + "key": "family", + "label": "Family", + "enum_items": [ + {"review": "review"}, + {"plate": "plate"}, + {"audio": "audio"} + ] + }, + { + "type": "text", + "key": "variant", + "label": "Variant", + "placeholder": "< Inherited >" + }, + { + "type": "boolean", + "key": "review", + "label": "Review", + "default": true + }, + { + "type": "list", + "key": "filter_ext", + "label": "Allowed input file types", + "object_type": "text" + } + ] + } + } + ] } ] } From 72e07dd0717f958f9ade887bc3aef8715d8343ea Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Jul 2022 15:22:03 +0200 Subject: [PATCH 130/432] trayp: editorial refactory code --- .../plugins/create/create_editorial.py | 306 +++++++++++------- 1 file changed, 193 insertions(+), 113 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index f373d2ac7a..d591256f8c 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -212,12 +212,12 @@ or updating already created. Publishing will create OTIO file. # Create all clip instances clip_instance_properties.update({ "fps": fps, - "parent_asset_name": asset_name + "parent_asset_name": asset_name, + "variant": instance_data["variant"] }) self._get_clip_instances( otio_timeline, clip_instance_properties, - variant_name=instance_data["variant"], family_presets=allowed_family_presets ) @@ -259,17 +259,8 @@ or updating already created. Publishing will create OTIO file. self, otio_timeline, clip_instance_properties, - variant_name, family_presets ): - # get clip instance properties - parent_asset_name = clip_instance_properties["parent_asset_name"] - handle_start = clip_instance_properties["handle_start"] - handle_end = clip_instance_properties["handle_end"] - timeline_offset = clip_instance_properties["timeline_offset"] - workfile_start_frame = clip_instance_properties["workfile_start_frame"] - fps = clip_instance_properties["fps"] - self.asset_name_check = [] tracks = otio_timeline.each_child( @@ -294,118 +285,207 @@ or updating already created. Publishing will create OTIO file. if not self._validate_clip_for_processing(clip): continue - # basic unique asset name - clip_name = os.path.splitext(clip.name)[0].lower() - name = f"{parent_asset_name.split('_')[0]}_{clip_name}" - - # make sure the name is unique - self._validate_name_uniqueness(name) - - # frame ranges data - clip_in = clip.range_in_parent().start_time.value - clip_in += track_start_frame - clip_out = clip.range_in_parent().end_time_inclusive().value - clip_out += track_start_frame - self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") - - # add offset in case there is any - self.log.debug(f"__ timeline_offset: {timeline_offset}") - if timeline_offset: - clip_in += timeline_offset - clip_out += timeline_offset - - clip_duration = clip.duration().value - self.log.info(f"clip duration: {clip_duration}") - - source_in = clip.trimmed_range().start_time.value - source_out = source_in + clip_duration - - # define starting frame for future shot - frame_start = ( - clip_in if workfile_start_frame is None - else workfile_start_frame + base_instance_data = self._get_base_instance_data( + clip, + clip_instance_properties, + track_start_frame ) - frame_end = frame_start + (clip_duration - 1) - parent_instance_label = None - parent_instance_id = None + parenting_data = { + "instance_label": None, + "instance_id": None + } for _fpreset in family_presets: - # get variant name from preset or from inharitance - _variant_name = _fpreset.get("variant") or variant_name - family = _fpreset["family"] - self.log.debug(f"__ family: {family}") - self.log.debug(f"__ _fpreset: {_fpreset}") - - # subset name - subset_name = "{}{}".format( - family, _variant_name.capitalize() - ) - label = "{}_{}".format( - clip_name, - subset_name + instance = self._make_subset_instance( + _fpreset["family"], + _fpreset, + deepcopy(base_instance_data), + parenting_data ) + self.log.debug(f"{pformat(dict(instance.data))}") - # create shared new instance data - instance_data = { - "label": label, - "variant": _variant_name, - "family": family, - "subset": subset_name, + def _make_subset_instance( + self, + _fpreset, + family, + future_instance_data, + parenting_data + ): + label = self._make_subset_naming( + _fpreset["family"], + _fpreset, + future_instance_data + ) - # HACK: just for temporal bug workaround - # TODO: should loockup shot name for update - "asset": parent_asset_name, - "name": clip_name, - "task": "", + # add file extension filter only if it is not shot family + if family == "shot": + c_instance = self.create_context.creators[ + "editorial_shot"].create( + future_instance_data) + parenting_data = { + "instance_label": label, + "instance_id": c_instance.data["instance_id"] + } - # parent time properties - "trackStartFrame": track_start_frame, - "timelineOffset": timeline_offset, - - # creator_attributes - "creator_attributes": { - "asset_name": clip_name, - "workfile_start_frame": workfile_start_frame, - "frameStart": int(frame_start), - "frameEnd": int(frame_end), - "fps": fps, - "handle_start": int(handle_start), - "handle_end": int(handle_end), - "clipIn": int(clip_in), - "clipOut": int(clip_out), - "sourceIn": int(source_in), - "sourceOut": int(source_out), - } + else: + # add review family if defined + future_instance_data.update({ + "filterExt": _fpreset["filter_ext"], + "parent_instance_id": parenting_data["instance_id"], + "creator_attributes": { + "parent_instance": parenting_data["instance_label"] + }, + "publish_attributes": { + "CollectReviewFamily": { + "add_review_family": _fpreset.get("review") } - # add file extension filter only if it is not shot family - if family == "shot": - c_instance = self.create_context.creators[ - "editorial_shot"].create( - instance_data) - parent_instance_label = label - parent_instance_id = c_instance.data["instance_id"] - else: - # add review family if defined - instance_data.update({ - "filterExt": _fpreset["filter_ext"], - "parent_instance_id": parent_instance_id, - "creator_attributes": { - "parent_instance": parent_instance_label - }, - "publish_attributes": { - "CollectReviewFamily": { - "add_review_family": _fpreset.get("review") - } - } - }) + } + }) - creator_identifier = f"editorial_{family}" - editorial_clip_creator = self.create_context.creators[ - creator_identifier] - c_instance = editorial_clip_creator.create( - instance_data) + creator_identifier = f"editorial_{family}" + editorial_clip_creator = self.create_context.creators[ + creator_identifier] + c_instance = editorial_clip_creator.create( + future_instance_data) - self.log.debug(f"{pformat(dict(c_instance.data))}") + return c_instance + + def _make_subset_naming( + self, + family, + _fpreset, + future_instance_data + ): + shot_name = future_instance_data["shotName"] + variant_name = future_instance_data["variant"] + + # get variant name from preset or from inharitance + _variant_name = _fpreset.get("variant") or variant_name + + self.log.debug(f"__ family: {family}") + self.log.debug(f"__ _fpreset: {_fpreset}") + + # subset name + subset_name = "{}{}".format( + family, _variant_name.capitalize() + ) + label = "{}_{}".format( + shot_name, + subset_name + ) + + future_instance_data.update({ + "family": family, + "label": label, + "variant": _variant_name, + "subset": subset_name, + }) + + return label + + def _get_base_instance_data( + self, + clip, + clip_instance_properties, + track_start_frame, + ): + # get clip instance properties + parent_asset_name = clip_instance_properties["parent_asset_name"] + handle_start = clip_instance_properties["handle_start"] + handle_end = clip_instance_properties["handle_end"] + timeline_offset = clip_instance_properties["timeline_offset"] + workfile_start_frame = clip_instance_properties["workfile_start_frame"] + fps = clip_instance_properties["fps"] + variant_name = clip_instance_properties["variant"] + + shot_name = self._get_clip_name(clip, parent_asset_name) + + timing_data = self._get_timing_data( + clip, + timeline_offset, + track_start_frame, + workfile_start_frame + ) + + # create creator attributes + creator_attributes = { + "asset_name": shot_name, + "workfile_start_frame": workfile_start_frame, + "fps": fps, + "handle_start": int(handle_start), + "handle_end": int(handle_end) + } + creator_attributes.update(timing_data) + + # create shared new instance data + base_instance_data = { + "shotName": shot_name, + "variant": variant_name, + + # HACK: just for temporal bug workaround + # TODO: should loockup shot name for update + "asset": parent_asset_name, + "task": "", + # parent time properties + "trackStartFrame": track_start_frame, + "timelineOffset": timeline_offset, + # creator_attributes + "creator_attributes": creator_attributes + } + + return base_instance_data + + def _get_timing_data( + self, + clip, + timeline_offset, + track_start_frame, + workfile_start_frame + ): + # frame ranges data + clip_in = clip.range_in_parent().start_time.value + clip_in += track_start_frame + clip_out = clip.range_in_parent().end_time_inclusive().value + clip_out += track_start_frame + self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") + + # add offset in case there is any + self.log.debug(f"__ timeline_offset: {timeline_offset}") + if timeline_offset: + clip_in += timeline_offset + clip_out += timeline_offset + + clip_duration = clip.duration().value + self.log.info(f"clip duration: {clip_duration}") + + source_in = clip.trimmed_range().start_time.value + source_out = source_in + clip_duration + + # define starting frame for future shot + frame_start = ( + clip_in if workfile_start_frame is None + else workfile_start_frame + ) + frame_end = frame_start + (clip_duration - 1) + + return { + "frameStart": int(frame_start), + "frameEnd": int(frame_end), + "clipIn": int(clip_in), + "clipOut": int(clip_out), + "sourceIn": int(source_in), + "sourceOut": int(source_out) + } + + def _get_clip_name(self, clip, selected_asset_name): + # basic unique asset name + clip_name = os.path.splitext(clip.name)[0].lower() + name = f"{selected_asset_name.split('_')[0]}_{clip_name}" + + # make sure the name is unique + self._validate_name_uniqueness(name) + + return clip_name def _get_allowed_family_presets(self, pre_create_data): self.log.debug(f"__ pre_create_data: {pre_create_data}") From a5477a15c80e5cbdd3a29541beed2c0e2eb03f8d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Jul 2022 15:39:02 +0200 Subject: [PATCH 131/432] trayp: debugging after refactory --- .../plugins/create/create_editorial.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index d591256f8c..1ff729cf65 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -295,9 +295,11 @@ or updating already created. Publishing will create OTIO file. "instance_label": None, "instance_id": None } + self.log.info( + f"Creating subsets from presets: \n{pformat(family_presets)}") + for _fpreset in family_presets: instance = self._make_subset_instance( - _fpreset["family"], _fpreset, deepcopy(base_instance_data), parenting_data @@ -307,12 +309,11 @@ or updating already created. Publishing will create OTIO file. def _make_subset_instance( self, _fpreset, - family, future_instance_data, parenting_data ): + family = _fpreset["family"] label = self._make_subset_naming( - _fpreset["family"], _fpreset, future_instance_data ) @@ -322,10 +323,10 @@ or updating already created. Publishing will create OTIO file. c_instance = self.create_context.creators[ "editorial_shot"].create( future_instance_data) - parenting_data = { + parenting_data.update({ "instance_label": label, "instance_id": c_instance.data["instance_id"] - } + }) else: # add review family if defined @@ -352,12 +353,12 @@ or updating already created. Publishing will create OTIO file. def _make_subset_naming( self, - family, _fpreset, future_instance_data ): shot_name = future_instance_data["shotName"] variant_name = future_instance_data["variant"] + family = _fpreset["family"] # get variant name from preset or from inharitance _variant_name = _fpreset.get("variant") or variant_name From 8bfcbad8eb6f741e2d38203e63af3e9c53a1e26f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Jul 2022 17:39:33 +0200 Subject: [PATCH 132/432] trayp: wip hierarchical data and rename --- openpype/hosts/traypublisher/api/editorial.py | 178 ++++++++++++++++++ .../plugins/create/create_editorial.py | 34 ++-- .../schema_project_traypublisher.json | 3 +- 3 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 openpype/hosts/traypublisher/api/editorial.py diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py new file mode 100644 index 0000000000..4637d6d1df --- /dev/null +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -0,0 +1,178 @@ +import re +from copy import deepcopy + +from openpype.client import get_asset_by_id + + +class ShotMetadataSover: + """Collecting hierarchy context from `parents` and `hierarchy` data + present in `clip` family instances coming from the request json data file + + It will add `hierarchical_context` into each instance for integrate + plugins to be able to create needed parents for the context if they + don't exist yet + """ + # presets + clip_name_tokenizer = None + shot_rename = True + shot_hierarchy = None + shot_add_tasks = None + + def __init__(self, creator_settings): + self.clip_name_tokenizer = creator_settings["clip_name_tokenizer"] + self.shot_rename = creator_settings["shot_rename"] + self.shot_hierarchy = creator_settings["shot_hierarchy"] + self.shot_add_tasks = creator_settings["shot_add_tasks"] + + def convert_to_entity(self, key, value): + # ftrack compatible entity types + types = {"shot": "Shot", + "folder": "Folder", + "episode": "Episode", + "sequence": "Sequence", + "track": "Sequence", + } + # convert to entity type + entity_type = types.get(key, None) + + # return if any + if entity_type: + return {"entity_type": entity_type, "entity_name": value} + + def _rename_template(self, clip_name, source_data): + if self.clip_name_tokenizer: + search_text = "" + parent_name = source_data["assetEntity"]["name"] + + search_text += parent_name + clip_name + source_data["anatomy_data"].update({"clip_name": clip_name}) + for type, pattern in self.clip_name_tokenizer.items(): + p = re.compile(pattern) + match = p.findall(search_text) + if not match: + continue + source_data["anatomy_data"][type] = match[-1] + + # format to new shot name + return self.shot_rename[ + "shot_rename_template"].format( + **source_data["anatomy_data"]) + + def _create_hierarchy(self, source_data): + asset_doc = source_data["selected_asset_doc"] + project_doc = source_data["project_doc"] + + project_name = project_doc["name"] + visual_hierarchy = [asset_doc] + current_doc = asset_doc + + # TODO: refactory withou the while + while True: + visual_parent_id = current_doc["data"]["visualParent"] + visual_parent = None + if visual_parent_id: + visual_parent = get_asset_by_id(project_name, visual_parent_id) + + if not visual_parent: + visual_hierarchy.append(project_doc) + break + visual_hierarchy.append(visual_parent) + current_doc = visual_parent + + # add current selection context hierarchy from standalonepublisher + parents = [] + parents.extend( + { + "entity_type": entity["data"]["entityType"], + "entity_name": entity["name"] + } + for entity in reversed(visual_hierarchy) + ) + + _hierarchy = [] + if self.shot_hierarchy.get("enabled"): + parent_template_patern = re.compile(r"\{([a-z]*?)\}") + # fill the parents parts from presets + shot_hierarchy = deepcopy(self.shot_hierarchy) + hierarchy_parents = shot_hierarchy["parents"] + + # fill parent keys data template from anatomy data + for parent_key in hierarchy_parents: + hierarchy_parents[parent_key] = hierarchy_parents[ + parent_key].format(**source_data["anatomy_data"]) + + for _index, _parent in enumerate( + shot_hierarchy["parents_path"].split("/")): + parent_filled = _parent.format(**hierarchy_parents) + parent_key = parent_template_patern.findall(_parent).pop() + + # in case SP context is set to the same folder + if (_index == 0) and ("folder" in parent_key) \ + and (parents[-1]["entity_name"] == parent_filled): + self.log.debug(f" skipping : {parent_filled}") + continue + + # in case first parent is project then start parents from start + if (_index == 0) and ("project" in parent_key): + self.log.debug("rebuilding parents from scratch") + project_parent = parents[0] + parents = [project_parent] + self.log.debug(f"project_parent: {project_parent}") + self.log.debug(f"parents: {parents}") + continue + + prnt = self.convert_to_entity( + parent_key, parent_filled) + parents.append(prnt) + _hierarchy.append(parent_filled) + + # convert hierarchy to string + hierarchy_path = "/".join(_hierarchy) + + output_data = { + "hierarchy": hierarchy_path, + "parents": parents + } + # print + self.log.debug(f"__ hierarchy_path: {hierarchy_path}") + self.log.debug(f"__ parents: {parents}") + + output_data["tasks"] = self._generate_tasks_from_settings(project_doc) + + return output_data + + def _generate_tasks_from_settings(self, project_doc): + tasks_to_add = {} + if self.shot_add_tasks: + project_tasks = project_doc["config"]["tasks"] + for task_name, task_data in self.shot_add_tasks.items(): + _task_data = deepcopy(task_data) + + # check if task type in project task types + if _task_data["type"] in project_tasks.keys(): + tasks_to_add[task_name] = _task_data + else: + raise KeyError( + "Missing task type `{}` for `{}` is not" + " existing in `{}``".format( + _task_data["type"], + task_name, + list(project_tasks.keys()) + ) + ) + + return tasks_to_add + + def generate_data(self, clip_name, source_data): + self.log.info(f"_ source_data: {source_data}") + + # match clip to shot name at start + shot_name = clip_name + + if self.shot_rename["enabled"]: + shot_name = self._rename_template(clip_name, source_data) + self.log.info(f"Renamed shot name: {shot_name}") + + hierarchy_data = self._create_hierarchy(source_data) + + return shot_name, hierarchy_data diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 1ff729cf65..7672bb6222 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -2,12 +2,17 @@ import os from copy import deepcopy from pprint import pformat import opentimelineio as otio -from openpype.client import get_asset_by_name +from openpype.client import ( + get_asset_by_name, + get_project +) from openpype.hosts.traypublisher.api.plugin import ( TrayPublishCreator, InvisibleTrayPublishCreator ) - +from openpype.hosts.traypublisher.api.editorial import ( + ShotMetadataSover +) from openpype.pipeline import CreatedInstance @@ -173,6 +178,7 @@ or updating already created. Publishing will create OTIO file. ) # get this creator settings by identifier self._creator_settings = editorial_creators.get(self.identifier) + self._shot_metadata_solver = ShotMetadataSover(self._creator_settings) # try to set main attributes from settings if self._creator_settings.get("default_variants"): @@ -399,7 +405,19 @@ or updating already created. Publishing will create OTIO file. fps = clip_instance_properties["fps"] variant_name = clip_instance_properties["variant"] - shot_name = self._get_clip_name(clip, parent_asset_name) + # basic unique asset name + clip_name = os.path.splitext(clip.name)[0].lower() + + shot_name, shot_metadata = self._shot_metadata_solver.generate_data( + clip_name, + { + "anatomy_data": anatomy_data, + "selected_asset_doc": get_asset_by_name(parent_asset_name), + "project_doc": get_project(self.project_name) + } + ) + + self._validate_name_uniqueness(shot_name) timing_data = self._get_timing_data( clip, @@ -478,16 +496,6 @@ or updating already created. Publishing will create OTIO file. "sourceOut": int(source_out) } - def _get_clip_name(self, clip, selected_asset_name): - # basic unique asset name - clip_name = os.path.splitext(clip.name)[0].lower() - name = f"{selected_asset_name.split('_')[0]}_{clip_name}" - - # make sure the name is unique - self._validate_name_uniqueness(name) - - return clip_name - def _get_allowed_family_presets(self, pre_create_data): self.log.debug(f"__ pre_create_data: {pre_create_data}") return [ diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 8d95cb19a9..3af3839c6f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -207,7 +207,8 @@ { "type": "task-types-enum", "key": "type", - "label": "Task type" + "label": "Task type", + "multiselection": false } ] } From bf82969c1422cfc87379b75d859cb93e5cf983c6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 09:38:51 +0200 Subject: [PATCH 133/432] trayp: shot metadata solver final --- openpype/hosts/traypublisher/api/editorial.py | 240 ++++++++++-------- 1 file changed, 133 insertions(+), 107 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 4637d6d1df..d6cc99f87c 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -12,6 +12,9 @@ class ShotMetadataSover: plugins to be able to create needed parents for the context if they don't exist yet """ + + NO_DECOR_PATERN = re.compile(r"\{([a-z]*?)\}") + # presets clip_name_tokenizer = None shot_rename = True @@ -24,49 +27,106 @@ class ShotMetadataSover: self.shot_hierarchy = creator_settings["shot_hierarchy"] self.shot_add_tasks = creator_settings["shot_add_tasks"] - def convert_to_entity(self, key, value): - # ftrack compatible entity types - types = {"shot": "Shot", - "folder": "Folder", - "episode": "Episode", - "sequence": "Sequence", - "track": "Sequence", - } - # convert to entity type - entity_type = types.get(key, None) + def _rename_template(self, data): + # format to new shot name + return self.shot_rename[ + "shot_rename_template"].format(**data) - # return if any - if entity_type: - return {"entity_type": entity_type, "entity_name": value} + def _generate_tokens(self, clip_name, source_data): + output_data = deepcopy(source_data["anatomy_data"]) + output_data["clip_name"] = clip_name - def _rename_template(self, clip_name, source_data): - if self.clip_name_tokenizer: - search_text = "" - parent_name = source_data["assetEntity"]["name"] + if not self.clip_name_tokenizer: + return output_data - search_text += parent_name + clip_name - source_data["anatomy_data"].update({"clip_name": clip_name}) - for type, pattern in self.clip_name_tokenizer.items(): - p = re.compile(pattern) - match = p.findall(search_text) - if not match: - continue - source_data["anatomy_data"][type] = match[-1] + parent_name = source_data["selected_asset_doc"]["name"] - # format to new shot name - return self.shot_rename[ - "shot_rename_template"].format( - **source_data["anatomy_data"]) + search_text = parent_name + clip_name - def _create_hierarchy(self, source_data): - asset_doc = source_data["selected_asset_doc"] - project_doc = source_data["project_doc"] + for token_key, pattern in self.clip_name_tokenizer.items(): + p = re.compile(pattern) + match = p.findall(search_text) + if not match: + continue + # QUESTION:how to refactory `match[-1]` to some better way? + output_data[token_key] = match[-1] + return output_data + + def _create_parents_from_settings(self, parents, data): + + # fill the parents parts from presets + shot_hierarchy = deepcopy(self.shot_hierarchy) + hierarchy_parents = shot_hierarchy["parents"] + + # fill parent keys data template from anatomy data + _parent_tokens_formating_data = { + parent_token["name"]: parent_token["value"].format(**data) + for parent_token in hierarchy_parents + } + _parent_tokens_type = { + parent_token["name"]: parent_token["type"] + for parent_token in hierarchy_parents + } + for _index, _parent in enumerate( + shot_hierarchy["parents_path"].split("/") + ): + # format parent token with value which is formated + parent_name = _parent.format( + **_parent_tokens_formating_data) + parent_token_name = ( + self.NO_DECOR_PATERN.findall(_parent).pop()) + + if not parent_token_name: + raise KeyError( + f"Parent token is not found in: `{_parent}`") + + # find parent type + parent_token_type = _parent_tokens_type[parent_token_name] + + # in case selected context is set to the same asset + if ( + _index == 0 + and parents[-1]["entity_name"] == parent_name + ): + self.log.debug(f" skipping : {parent_name}") + continue + + # in case first parent is project then start parents from start + if ( + _index == 0 + and parent_token_type == "project" + ): + self.log.debug("rebuilding parents from scratch") + project_parent = parents[0] + parents = [project_parent] + continue + + parents.append({ + "entity_type": parent_token_type, + "entity_name": parent_name + }) + + self.log.debug(f"__ parents: {parents}") + + return parents + + def _create_hierarchy_path(self, parents): + return "/".join( + [p for p in parents if p["entity_type"] != "project"] + ) if parents else "" + + def _get_parents_from_selected_asset( + self, + asset_doc, + project_doc + ): project_name = project_doc["name"] visual_hierarchy = [asset_doc] current_doc = asset_doc - # TODO: refactory withou the while + # looping trought all available visual parents + # if they are not available anymore than it breaks while True: visual_parent_id = current_doc["data"]["visualParent"] visual_parent = None @@ -79,100 +139,66 @@ class ShotMetadataSover: visual_hierarchy.append(visual_parent) current_doc = visual_parent - # add current selection context hierarchy from standalonepublisher - parents = [] - parents.extend( + # add current selection context hierarchy + return [ { "entity_type": entity["data"]["entityType"], "entity_name": entity["name"] } for entity in reversed(visual_hierarchy) - ) - - _hierarchy = [] - if self.shot_hierarchy.get("enabled"): - parent_template_patern = re.compile(r"\{([a-z]*?)\}") - # fill the parents parts from presets - shot_hierarchy = deepcopy(self.shot_hierarchy) - hierarchy_parents = shot_hierarchy["parents"] - - # fill parent keys data template from anatomy data - for parent_key in hierarchy_parents: - hierarchy_parents[parent_key] = hierarchy_parents[ - parent_key].format(**source_data["anatomy_data"]) - - for _index, _parent in enumerate( - shot_hierarchy["parents_path"].split("/")): - parent_filled = _parent.format(**hierarchy_parents) - parent_key = parent_template_patern.findall(_parent).pop() - - # in case SP context is set to the same folder - if (_index == 0) and ("folder" in parent_key) \ - and (parents[-1]["entity_name"] == parent_filled): - self.log.debug(f" skipping : {parent_filled}") - continue - - # in case first parent is project then start parents from start - if (_index == 0) and ("project" in parent_key): - self.log.debug("rebuilding parents from scratch") - project_parent = parents[0] - parents = [project_parent] - self.log.debug(f"project_parent: {project_parent}") - self.log.debug(f"parents: {parents}") - continue - - prnt = self.convert_to_entity( - parent_key, parent_filled) - parents.append(prnt) - _hierarchy.append(parent_filled) - - # convert hierarchy to string - hierarchy_path = "/".join(_hierarchy) - - output_data = { - "hierarchy": hierarchy_path, - "parents": parents - } - # print - self.log.debug(f"__ hierarchy_path: {hierarchy_path}") - self.log.debug(f"__ parents: {parents}") - - output_data["tasks"] = self._generate_tasks_from_settings(project_doc) - - return output_data + ] def _generate_tasks_from_settings(self, project_doc): tasks_to_add = {} - if self.shot_add_tasks: - project_tasks = project_doc["config"]["tasks"] - for task_name, task_data in self.shot_add_tasks.items(): - _task_data = deepcopy(task_data) - # check if task type in project task types - if _task_data["type"] in project_tasks.keys(): - tasks_to_add[task_name] = _task_data - else: - raise KeyError( - "Missing task type `{}` for `{}` is not" - " existing in `{}``".format( - _task_data["type"], - task_name, - list(project_tasks.keys()) - ) + project_tasks = project_doc["config"]["tasks"] + for task_name, task_data in self.shot_add_tasks.items(): + _task_data = deepcopy(task_data) + + # check if task type in project task types + if _task_data["type"] in project_tasks.keys(): + tasks_to_add[task_name] = _task_data + else: + raise KeyError( + "Missing task type `{}` for `{}` is not" + " existing in `{}``".format( + _task_data["type"], + task_name, + list(project_tasks.keys()) ) + ) return tasks_to_add def generate_data(self, clip_name, source_data): self.log.info(f"_ source_data: {source_data}") + tasks = {} + asset_doc = source_data["selected_asset_doc"] + project_doc = source_data["project_doc"] + # match clip to shot name at start shot_name = clip_name + # parse all tokens and generate formating data + formating_data = self._generate_tokens(shot_name, source_data) + + # generate parents from selected asset + parents = self._get_parents_from_selected_asset(asset_doc, project_doc) + if self.shot_rename["enabled"]: - shot_name = self._rename_template(clip_name, source_data) + shot_name = self._rename_template(clip_name, formating_data) self.log.info(f"Renamed shot name: {shot_name}") - hierarchy_data = self._create_hierarchy(source_data) + if self.shot_hierarchy["enabled"]: + parents = self._create_parents_from_settings(formating_data) - return shot_name, hierarchy_data + if self.shot_add_tasks: + tasks = self._generate_tasks_from_settings( + project_doc) + + return shot_name, { + "hierarchy": self._create_hierarchy_path(parents), + "parents": parents, + "tasks": tasks + } From 7f9cdaaa0e90b8c00dcdc91a523d09e1f8d32459 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 11:43:19 +0200 Subject: [PATCH 134/432] trayp: updating settings --- .../defaults/project_settings/traypublisher.json | 10 +++++----- .../projects_schema/schema_project_traypublisher.json | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 93f6420c21..82c82c79e9 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -245,17 +245,17 @@ "parents_path": "{project}/{folder}/{sequence}", "parents": [ { - "type": "project", - "name": "projekt", - "value": "{projekt[name]}" + "type": "Project", + "name": "project", + "value": "{project[name]}" }, { - "type": "folder", + "type": "Folder", "name": "folder", "value": "shots" }, { - "type": "sequence", + "type": "Sequence", "name": "sequence", "value": "{_sequence_}" } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 3af3839c6f..909ee02b04 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -173,10 +173,10 @@ "key": "type", "label": "Parent type", "enum_items": [ - {"project": "Project"}, - {"folder": "Folder"}, - {"episode": "Episode"}, - {"sequence": "Sequence"} + {"Project": "Project"}, + {"Folder": "Folder"}, + {"Episode": "Episode"}, + {"Sequence": "Sequence"} ] }, { From a8e4fdba5fa11f1c0a66e90f9e6ed9429212e9d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 11:43:39 +0200 Subject: [PATCH 135/432] trayp: editorial with hierarchy and parents --- openpype/hosts/traypublisher/api/editorial.py | 69 ++++++++++++++----- .../plugins/create/create_editorial.py | 21 ++++-- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index d6cc99f87c..713f1b5c6c 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -2,7 +2,7 @@ import re from copy import deepcopy from openpype.client import get_asset_by_id - +from openpype.pipeline.create import CreatorError class ShotMetadataSover: """Collecting hierarchy context from `parents` and `hierarchy` data @@ -21,16 +21,27 @@ class ShotMetadataSover: shot_hierarchy = None shot_add_tasks = None - def __init__(self, creator_settings): + def __init__(self, creator_settings, logger): self.clip_name_tokenizer = creator_settings["clip_name_tokenizer"] self.shot_rename = creator_settings["shot_rename"] self.shot_hierarchy = creator_settings["shot_hierarchy"] self.shot_add_tasks = creator_settings["shot_add_tasks"] + self.log = logger + def _rename_template(self, data): - # format to new shot name - return self.shot_rename[ - "shot_rename_template"].format(**data) + shot_rename_template = self.shot_rename[ + "shot_rename_template"] + try: + # format to new shot name + return shot_rename_template.format(**data) + except KeyError as _E: + raise CreatorError(( + "Make sure all keys are correct in settings: \n\n" + f"From template string {shot_rename_template} > " + f"`{_E}` has no equivalent in \n" + f"{list(data.keys())} input formating keys!" + )) def _generate_tokens(self, clip_name, source_data): output_data = deepcopy(source_data["anatomy_data"]) @@ -47,7 +58,13 @@ class ShotMetadataSover: p = re.compile(pattern) match = p.findall(search_text) if not match: - continue + raise CreatorError(( + "Make sure regex expression is correct: \n\n" + f"From settings '{token_key}' key " + f"with '{pattern}' expression, \n" + f"is not able to find anything in '{search_text}'!" + )) + # QUESTION:how to refactory `match[-1]` to some better way? output_data[token_key] = match[-1] @@ -60,10 +77,17 @@ class ShotMetadataSover: hierarchy_parents = shot_hierarchy["parents"] # fill parent keys data template from anatomy data - _parent_tokens_formating_data = { - parent_token["name"]: parent_token["value"].format(**data) - for parent_token in hierarchy_parents - } + try: + _parent_tokens_formating_data = { + parent_token["name"]: parent_token["value"].format(**data) + for parent_token in hierarchy_parents + } + except KeyError as _E: + raise CreatorError(( + "Make sure all keys are correct in settings: \n" + f"`{_E}` has no equivalent in \n{list(data.keys())}" + )) + _parent_tokens_type = { parent_token["name"]: parent_token["type"] for parent_token in hierarchy_parents @@ -72,8 +96,17 @@ class ShotMetadataSover: shot_hierarchy["parents_path"].split("/") ): # format parent token with value which is formated - parent_name = _parent.format( - **_parent_tokens_formating_data) + try: + parent_name = _parent.format( + **_parent_tokens_formating_data) + except KeyError as _E: + raise CreatorError(( + "Make sure all keys are correct in settings: \n\n" + f"From template string {shot_hierarchy['parents_path']} > " + f"`{_E}` has no equivalent in \n" + f"{list(_parent_tokens_formating_data.keys())} parents" + )) + parent_token_name = ( self.NO_DECOR_PATERN.findall(_parent).pop()) @@ -95,7 +128,7 @@ class ShotMetadataSover: # in case first parent is project then start parents from start if ( _index == 0 - and parent_token_type == "project" + and parent_token_type == "Project" ): self.log.debug("rebuilding parents from scratch") project_parent = parents[0] @@ -113,7 +146,10 @@ class ShotMetadataSover: def _create_hierarchy_path(self, parents): return "/".join( - [p for p in parents if p["entity_type"] != "project"] + [ + p["entity_name"] for p in parents + if p["entity_type"] != "Project" + ] ) if parents else "" def _get_parents_from_selected_asset( @@ -187,11 +223,12 @@ class ShotMetadataSover: parents = self._get_parents_from_selected_asset(asset_doc, project_doc) if self.shot_rename["enabled"]: - shot_name = self._rename_template(clip_name, formating_data) + shot_name = self._rename_template(formating_data) self.log.info(f"Renamed shot name: {shot_name}") if self.shot_hierarchy["enabled"]: - parents = self._create_parents_from_settings(formating_data) + parents = self._create_parents_from_settings( + parents, formating_data) if self.shot_add_tasks: tasks = self._generate_tasks_from_settings( diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 7672bb6222..6bcc692240 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -178,7 +178,8 @@ or updating already created. Publishing will create OTIO file. ) # get this creator settings by identifier self._creator_settings = editorial_creators.get(self.identifier) - self._shot_metadata_solver = ShotMetadataSover(self._creator_settings) + self._shot_metadata_solver = ShotMetadataSover( + self._creator_settings, self.log) # try to set main attributes from settings if self._creator_settings.get("default_variants"): @@ -407,13 +408,22 @@ or updating already created. Publishing will create OTIO file. # basic unique asset name clip_name = os.path.splitext(clip.name)[0].lower() + project_doc = get_project(self.project_name) shot_name, shot_metadata = self._shot_metadata_solver.generate_data( clip_name, { - "anatomy_data": anatomy_data, - "selected_asset_doc": get_asset_by_name(parent_asset_name), - "project_doc": get_project(self.project_name) + "anatomy_data": { + "project": { + "name": self.project_name, + "code": project_doc["data"]["code"] + }, + "parent": parent_asset_name, + "app": self.host_name + }, + "selected_asset_doc": get_asset_by_name( + self.project_name, parent_asset_name), + "project_doc": project_doc } ) @@ -429,6 +439,7 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { "asset_name": shot_name, + "Parent hierarchy path": shot_metadata["hierarchy"], "workfile_start_frame": workfile_start_frame, "fps": fps, "handle_start": int(handle_start), @@ -451,6 +462,8 @@ or updating already created. Publishing will create OTIO file. # creator_attributes "creator_attributes": creator_attributes } + # add hierarchy shot metadata + base_instance_data.update(shot_metadata) return base_instance_data From fe68a07a90d057526b5c65854528d67ce637c629 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 14:16:43 +0200 Subject: [PATCH 136/432] trayp: update editorial creator --- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 6bcc692240..ffff5de70a 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -255,7 +255,8 @@ or updating already created. Publishing will create OTIO file. # Pass precreate data to creator attributes data.update({ - "sequence_file_path": file_path + "sequenceFilePath": file_path, + "otioTimeline": otio.adapters.write_to_string(otio_timeline) }) self._create_instance(self.family, subset_name, data) @@ -324,6 +325,7 @@ or updating already created. Publishing will create OTIO file. _fpreset, future_instance_data ) + future_instance_data["label"] = label # add file extension filter only if it is not shot family if family == "shot": From b22b28edbc3a6af4e9e68f801a5a5c4a5c13be27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 14:17:02 +0200 Subject: [PATCH 137/432] trayp: publishing editorial --- .../publish/collect_editorial_instances.py | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py index 6521c97774..c088709a61 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py @@ -1,15 +1,17 @@ import os from pprint import pformat import pyblish.api +import opentimelineio as otio class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): """Collect data for instances created by settings creators.""" label = "Collect Editorial Instances" - order = pyblish.api.CollectorOrder - 0.49 + order = pyblish.api.CollectorOrder hosts = ["traypublisher"] + families = ["editorial"] def process(self, instance): @@ -18,34 +20,27 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): if "representations" not in instance.data: instance.data["representations"] = [] - repres = instance.data["representations"] - self.log.debug( - pformat(dict(instance.data)) - ) - creator_attributes = instance.data["creator_attributes"] - filepath_item = creator_attributes["filepath"] - self.log.info(filepath_item) - filepaths = [ - os.path.join(filepath_item["directory"], filename) - for filename in filepath_item["filenames"] - ] - instance.data["sourceFilepaths"] = filepaths - instance.data["stagingDir"] = filepath_item["directory"] + fpath = instance.data["sequenceFilePath"] + otio_timeline_string = instance.data.pop("otioTimeline") + otio_timeline = otio.adapters.read_from_string( + otio_timeline_string) - filenames = filepath_item["filenames"] - _, ext = os.path.splitext(filenames[0]) - ext = ext[1:] - if len(filenames) == 1: - filenames = filenames[0] + instance.context.data["otioTimeline"] = otio_timeline - repres.append({ - "ext": ext, - "name": ext, - "stagingDir": filepath_item["directory"], - "files": filenames + self.log.info(fpath) + + instance.data["stagingDir"] = os.path.dirname(fpath) + + _, ext = os.path.splitext(fpath) + + instance.data["representations"].append({ + "ext": ext[1:], + "name": ext[1:], + "stagingDir": instance.data["stagingDir"], + "files": os.path.basename(fpath) }) self.log.debug("Created Simple Settings instance {}".format( - instance.data + pformat(instance.data) )) From fb586feaf3dec0eeabe370f512426c03df8d7289 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 14:17:31 +0200 Subject: [PATCH 138/432] general: label could be set from instance data --- openpype/plugins/publish/collect_from_create_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index d2be633cbe..e070cc411d 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -44,7 +44,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): "subset": subset, "asset": in_data["asset"], "task": in_data["task"], - "label": subset, + "label": in_data.get("label") or subset, "name": subset, "family": in_data["family"], "families": instance_families, From 3cc78c2f98d3fd652dbe9d865d54df86bf6cd688 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 17:56:30 +0200 Subject: [PATCH 139/432] trayp: rename `invisible` to `hidden` --- openpype/hosts/traypublisher/api/plugin.py | 4 ++-- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 ++-- openpype/pipeline/create/__init__.py | 4 ++-- openpype/pipeline/create/creator_plugins.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index cb2f86eed7..3a268be55d 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,6 +1,6 @@ from openpype.pipeline.create import ( Creator, - InvisibleCreator, + HiddenCreator, CreatedInstance ) from openpype.lib import ( @@ -15,7 +15,7 @@ from .pipeline import ( ) -class InvisibleTrayPublishCreator(InvisibleCreator): +class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index ffff5de70a..8f7101385c 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -8,7 +8,7 @@ from openpype.client import ( ) from openpype.hosts.traypublisher.api.plugin import ( TrayPublishCreator, - InvisibleTrayPublishCreator + HiddenTrayPublishCreator ) from openpype.hosts.traypublisher.api.editorial import ( ShotMetadataSover @@ -60,7 +60,7 @@ CLIP_ATTR_DEFS = [ ] -class EditorialClipInstanceCreatorBase(InvisibleTrayPublishCreator): +class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): host_name = "traypublisher" def __init__( diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index cd01c53cf5..bd196ccfd1 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -7,7 +7,7 @@ from .creator_plugins import ( BaseCreator, Creator, AutoCreator, - InvisibleCreator, + HiddenCreator, discover_creator_plugins, discover_legacy_creator_plugins, @@ -36,7 +36,7 @@ __all__ = ( "BaseCreator", "Creator", "AutoCreator", - "InvisibleCreator", + "HiddenCreator", "discover_creator_plugins", "discover_legacy_creator_plugins", diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 4d953a0605..8cb161de20 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -416,7 +416,7 @@ class Creator(BaseCreator): return self.pre_create_attr_defs -class InvisibleCreator(BaseCreator): +class HiddenCreator(BaseCreator): @abstractmethod def create(self, instance_data, source_data): pass From 29de28cb5371ced19fdf35368ce8e4a9f4f8b074 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 17:57:05 +0200 Subject: [PATCH 140/432] trayp: editorial publishing wip --- openpype/hosts/traypublisher/api/editorial.py | 1 + .../plugins/create/create_editorial.py | 49 +++- .../plugins/publish/collect_clip_instances.py | 32 +++ .../publish/collect_editorial_instances.py | 8 +- .../publish/collect_editorial_resources.py | 271 ++++++++++++++++++ .../plugins/publish/collect_shot_instances.py | 163 +++++++++++ .../publish/extract_trim_video_audio.py | 2 +- .../plugins/publish/validate_asset_docs.py | 4 + 8 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py rename openpype/{hosts/standalonepublisher => }/plugins/publish/extract_trim_video_audio.py (98%) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 713f1b5c6c..948e05ec61 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -4,6 +4,7 @@ from copy import deepcopy from openpype.client import get_asset_by_id from openpype.pipeline.create import CreatorError + class ShotMetadataSover: """Collecting hierarchy context from `parents` and `hierarchy` data present in `clip` family instances coming from the request json data file diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8f7101385c..b87253a705 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -232,14 +232,10 @@ or updating already created. Publishing will create OTIO file. def _create_otio_instance(self, subset_name, data, pre_create_data): # get path of sequence file_path_data = pre_create_data["sequence_filepath_data"] + media_path_data = pre_create_data["media_filepaths_data"] - if len(file_path_data["filenames"]) == 0: - raise FileExistsError("File path was not added") - - file_path = os.path.join( - file_path_data["directory"], file_path_data["filenames"][0]) - - self.log.info(f"file_path: {file_path}") + file_path = self._get_path_from_file_data(file_path_data) + media_path = self._get_path_from_file_data(media_path_data) # get editorial sequence file into otio timeline object extension = os.path.splitext(file_path)[1] @@ -256,6 +252,7 @@ or updating already created. Publishing will create OTIO file. # Pass precreate data to creator attributes data.update({ "sequenceFilePath": file_path, + "editorialSourcePath": media_path, "otioTimeline": otio.adapters.write_to_string(otio_timeline) }) @@ -263,6 +260,18 @@ or updating already created. Publishing will create OTIO file. return otio_timeline + def _get_path_from_file_data(self, file_path_data): + # TODO: just temporarly solving only one media file + if isinstance(file_path_data, list): + file_path_data = file_path_data.pop() + + if len(file_path_data["filenames"]) == 0: + raise FileExistsError( + f"File path was not added: {file_path_data}") + + return os.path.join( + file_path_data["directory"], file_path_data["filenames"][0]) + def _get_clip_instances( self, otio_timeline, @@ -303,11 +312,14 @@ or updating already created. Publishing will create OTIO file. "instance_label": None, "instance_id": None } - self.log.info( - f"Creating subsets from presets: \n{pformat(family_presets)}") + self.log.info(( + "Creating subsets from presets: \n" + f"{pformat(family_presets)}" + )) for _fpreset in family_presets: instance = self._make_subset_instance( + clip, _fpreset, deepcopy(base_instance_data), parenting_data @@ -316,6 +328,7 @@ or updating already created. Publishing will create OTIO file. def _make_subset_instance( self, + clip, _fpreset, future_instance_data, parenting_data @@ -329,6 +342,8 @@ or updating already created. Publishing will create OTIO file. # add file extension filter only if it is not shot family if family == "shot": + future_instance_data["otioClip"] = ( + otio.adapters.write_to_string(clip)) c_instance = self.create_context.creators[ "editorial_shot"].create( future_instance_data) @@ -458,6 +473,7 @@ or updating already created. Publishing will create OTIO file. # TODO: should loockup shot name for update "asset": parent_asset_name, "task": "", + # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, @@ -568,7 +584,20 @@ or updating already created. Publishing will create OTIO file. ".fcpxml" ], allow_sequences=False, - label="Filepath", + single_item=True, + label="Sequence file", + ), + FileDef( + "media_filepaths_data", + folders=False, + extensions=[ + ".mov", + ".mp4", + ".wav" + ], + allow_sequences=False, + single_item=False, + label="Media files", ), # TODO: perhpas better would be timecode and fps input NumberDef( diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py new file mode 100644 index 0000000000..e3dfb1512a --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py @@ -0,0 +1,32 @@ +from pprint import pformat +import pyblish.api + + +class CollectClipInstance(pyblish.api.InstancePlugin): + """Collect clip instances and resolve its parent""" + + label = "Collect Clip Instances" + order = pyblish.api.CollectorOrder + + hosts = ["traypublisher"] + families = ["plate", "review", "audio"] + + def process(self, instance): + creator_identifier = instance.data["creator_identifier"] + if "editorial" not in creator_identifier: + return + + instance.data["families"].append("clip") + + parent_instance_id = instance.data["parent_instance_id"] + edit_shared_data = instance.context.data["editorialSharedData"] + instance.data.update( + edit_shared_data[parent_instance_id] + ) + + if "editorialSourcePath" in instance.context.data.keys(): + instance.data["editorialSourcePath"] = ( + instance.context.data["editorialSourcePath"]) + instance.data["families"].append("trimming") + + self.log.debug(pformat(instance.data)) \ No newline at end of file diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py index c088709a61..e181d0abe5 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_instances.py @@ -4,11 +4,11 @@ import pyblish.api import opentimelineio as otio -class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): +class CollectEditorialInstance(pyblish.api.InstancePlugin): """Collect data for instances created by settings creators.""" label = "Collect Editorial Instances" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.1 hosts = ["traypublisher"] families = ["editorial"] @@ -27,6 +27,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): otio_timeline_string) instance.context.data["otioTimeline"] = otio_timeline + instance.context.data["editorialSourcePath"] = ( + instance.data["editorialSourcePath"]) self.log.info(fpath) @@ -41,6 +43,6 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): "files": os.path.basename(fpath) }) - self.log.debug("Created Simple Settings instance {}".format( + self.log.debug("Created Editorial Instance {}".format( pformat(instance.data) )) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py new file mode 100644 index 0000000000..33a852e7a5 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py @@ -0,0 +1,271 @@ +import os +import re +import tempfile +import pyblish.api +from copy import deepcopy +import clique + + +class CollectInstanceResources(pyblish.api.InstancePlugin): + """Collect instance's resources""" + + # must be after `CollectInstances` + order = pyblish.api.CollectorOrder + label = "Collect Editorial Resources" + hosts = ["standalonepublisher"] + families = ["clip"] + + def process(self, instance): + self.context = instance.context + self.log.info(f"Processing instance: {instance}") + self.new_instances = [] + subset_files = dict() + subset_dirs = list() + anatomy = self.context.data["anatomy"] + anatomy_data = deepcopy(self.context.data["anatomyData"]) + anatomy_data.update({"root": anatomy.roots}) + + subset = instance.data["subset"] + clip_name = instance.data["clipName"] + + editorial_source_root = instance.data["editorialSourceRoot"] + editorial_source_path = instance.data["editorialSourcePath"] + + # if `editorial_source_path` then loop through + if editorial_source_path: + # add family if mov or mp4 found which is longer for + # cutting `trimming` to enable `ExtractTrimmingVideoAudio` plugin + staging_dir = os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + instance.data["stagingDir"] = staging_dir + instance.data["families"] += ["trimming"] + return + + # if template pattern in path then fill it with `anatomy_data` + if "{" in editorial_source_root: + editorial_source_root = editorial_source_root.format( + **anatomy_data) + + self.log.debug(f"root: {editorial_source_root}") + # loop `editorial_source_root` and find clip name in folders + # and look for any subset name alternatives + for root, dirs, _files in os.walk(editorial_source_root): + # search only for directories related to clip name + correct_clip_dir = None + for _d_search in dirs: + # avoid all non clip dirs + if _d_search not in clip_name: + continue + # found correct dir for clip + correct_clip_dir = _d_search + + # continue if clip dir was not found + if not correct_clip_dir: + continue + + clip_dir_path = os.path.join(root, correct_clip_dir) + subset_files_items = list() + # list content of clip dir and search for subset items + for subset_item in os.listdir(clip_dir_path): + # avoid all items which are not defined as subsets by name + if subset not in subset_item: + continue + + subset_item_path = os.path.join( + clip_dir_path, subset_item) + # if it is dir store it to `subset_dirs` list + if os.path.isdir(subset_item_path): + subset_dirs.append(subset_item_path) + + # if it is file then store it to `subset_files` list + if os.path.isfile(subset_item_path): + subset_files_items.append(subset_item_path) + + if subset_files_items: + subset_files.update({clip_dir_path: subset_files_items}) + + # break the loop if correct_clip_dir was captured + # no need to cary on if correct folder was found + if correct_clip_dir: + break + + if subset_dirs: + # look all dirs and check for subset name alternatives + for _dir in subset_dirs: + instance_data = deepcopy( + {k: v for k, v in instance.data.items()}) + sub_dir = os.path.basename(_dir) + # if subset name is only alternative then create new instance + if sub_dir != subset: + instance_data = self.duplicate_instance( + instance_data, subset, sub_dir) + + # create all representations + self.create_representations( + os.listdir(_dir), instance_data, _dir) + + if sub_dir == subset: + self.new_instances.append(instance_data) + # instance.data.update(instance_data) + + if subset_files: + unique_subset_names = list() + root_dir = list(subset_files.keys()).pop() + files_list = subset_files[root_dir] + search_pattern = f"({subset}[A-Za-z0-9]+)(?=[\\._\\s])" + for _file in files_list: + pattern = re.compile(search_pattern) + match = pattern.findall(_file) + if not match: + continue + match_subset = match.pop() + if match_subset in unique_subset_names: + continue + unique_subset_names.append(match_subset) + + self.log.debug(f"unique_subset_names: {unique_subset_names}") + + for _un_subs in unique_subset_names: + instance_data = self.duplicate_instance( + instance.data, subset, _un_subs) + + # create all representations + self.create_representations( + [os.path.basename(f) for f in files_list + if _un_subs in f], + instance_data, root_dir) + + # remove the original instance as it had been used only + # as template and is duplicated + self.context.remove(instance) + + # create all instances in self.new_instances into context + for new_instance in self.new_instances: + _new_instance = self.context.create_instance( + new_instance["name"]) + _new_instance.data.update(new_instance) + + def duplicate_instance(self, instance_data, subset, new_subset): + + new_instance_data = dict() + for _key, _value in instance_data.items(): + new_instance_data[_key] = _value + if not isinstance(_value, str): + continue + if subset in _value: + new_instance_data[_key] = _value.replace( + subset, new_subset) + + self.log.info(f"Creating new instance: {new_instance_data['name']}") + self.new_instances.append(new_instance_data) + return new_instance_data + + def create_representations( + self, files_list, instance_data, staging_dir): + """ Create representations from Collection object + """ + # collecting frames for later frame start/end reset + frames = list() + # break down Collection object to collections and reminders + collections, remainder = clique.assemble(files_list) + # add staging_dir to instance_data + instance_data["stagingDir"] = staging_dir + # add representations to instance_data + instance_data["representations"] = list() + + collection_head_name = None + # loop through collections and create representations + for _collection in collections: + ext = _collection.tail[1:] + collection_head_name = _collection.head + frame_start = list(_collection.indexes)[0] + frame_end = list(_collection.indexes)[-1] + repre_data = { + "frameStart": frame_start, + "frameEnd": frame_end, + "name": ext, + "ext": ext, + "files": [item for item in _collection], + "stagingDir": staging_dir + } + + if instance_data.get("keepSequence"): + repre_data_keep = deepcopy(repre_data) + instance_data["representations"].append(repre_data_keep) + + if "review" in instance_data["families"]: + repre_data.update({ + "thumbnail": True, + "frameStartFtrack": frame_start, + "frameEndFtrack": frame_end, + "step": 1, + "fps": self.context.data.get("fps"), + "name": "review", + "tags": ["review", "ftrackreview", "delete"], + }) + instance_data["representations"].append(repre_data) + + # add to frames for frame range reset + frames.append(frame_start) + frames.append(frame_end) + + # loop through reminders and create representations + for _reminding_file in remainder: + ext = os.path.splitext(_reminding_file)[-1][1:] + if ext not in instance_data["extensions"]: + continue + if collection_head_name and ( + (collection_head_name + ext) not in _reminding_file + ) and (ext in ["mp4", "mov"]): + self.log.info(f"Skipping file: {_reminding_file}") + continue + frame_start = 1 + frame_end = 1 + + repre_data = { + "name": ext, + "ext": ext, + "files": _reminding_file, + "stagingDir": staging_dir + } + + # exception for thumbnail + if "thumb" in _reminding_file: + repre_data.update({ + 'name': "thumbnail", + 'thumbnail': True + }) + + # exception for mp4 preview + if ext in ["mp4", "mov"]: + frame_start = 0 + frame_end = ( + (instance_data["frameEnd"] - instance_data["frameStart"]) + + 1) + # add review ftrack family into families + for _family in ["review", "ftrack"]: + if _family not in instance_data["families"]: + instance_data["families"].append(_family) + repre_data.update({ + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartFtrack": frame_start, + "frameEndFtrack": frame_end, + "step": 1, + "fps": self.context.data.get("fps"), + "name": "review", + "thumbnail": True, + "tags": ["review", "ftrackreview", "delete"], + }) + + # add to frames for frame range reset only if no collection + if not collections: + frames.append(frame_start) + frames.append(frame_end) + + instance_data["representations"].append(repre_data) + + # reset frame start / end + instance_data["frameStart"] = min(frames) + instance_data["frameEnd"] = max(frames) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py new file mode 100644 index 0000000000..5abafa498d --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -0,0 +1,163 @@ +from pprint import pformat +import pyblish.api +import opentimelineio as otio + + +class CollectShotInstance(pyblish.api.InstancePlugin): + """Collect shot instances and resolve its parent""" + + label = "Collect Shot Instances" + order = pyblish.api.CollectorOrder - 0.09 + + hosts = ["traypublisher"] + families = ["shot"] + + SHARED_KEYS = [ + "asset", + "fps", + "frameStart", + "frameEnd", + "clipIn", + "clipOut", + "sourceIn", + "sourceOut" + ] + + def process(self, instance): + self.log.debug(pformat(instance.data)) + + creator_identifier = instance.data["creator_identifier"] + if "editorial" not in creator_identifier: + return + + # get otio clip object + otio_clip = self._get_otio_clip(instance) + instance.data["otioClip"] = otio_clip + + # first solve the inputs from creator attr + data = self._solve_inputs_to_data(instance) + instance.data.update(data) + + # distribute all shared keys to clips instances + self._distribute_shared_data(instance) + self._solve_hierarchy_context(instance) + + self.log.debug(pformat(instance.data)) + + def _get_otio_clip(self, instance): + context = instance.context + # convert otio clip from string to object + otio_clip_string = instance.data.pop("otioClip") + otio_clip = otio.adapters.read_from_string( + otio_clip_string) + + otio_timeline = context.data["otioTimeline"] + + clips = [ + clip for clip in otio_timeline.each_child( + descended_from_type=otio.schema.Clip) + if clip.name == otio_clip.name + ] + self.log.debug(otio_timeline.each_child( + descended_from_type=otio.schema.Clip)) + + otio_clip = clips.pop() + self.log.debug(f"__ otioclip.parent: {otio_clip.parent}") + + return otio_clip + + def _distribute_shared_data(self, instance): + context = instance.context + + instance_id = instance.data["instance_id"] + + if not context.data.get("editorialSharedData"): + context.data["editorialSharedData"] = {} + + context.data["editorialSharedData"][instance_id] = { + _k: _v for _k, _v in instance.data.items() + if _k in self.SHARED_KEYS + } + + def _solve_inputs_to_data(self, instance): + _cr_attrs = instance.data["creator_attributes"] + workfile_start_frame = _cr_attrs["workfile_start_frame"] + frame_start = _cr_attrs["frameStart"] + frame_end = _cr_attrs["frameEnd"] + frame_dur = frame_end - frame_start + + return { + "asset": _cr_attrs["asset_name"], + "fps": float(_cr_attrs["fps"]), + "handleStart": _cr_attrs["handle_start"], + "handleEnd": _cr_attrs["handle_end"], + "frameStart": workfile_start_frame, + "frameEnd": workfile_start_frame + frame_dur, + "clipIn": _cr_attrs["clipIn"], + "clipOut": _cr_attrs["clipOut"], + "sourceIn": _cr_attrs["sourceIn"], + "sourceOut": _cr_attrs["sourceOut"], + "workfileFrameStart": workfile_start_frame + } + + def _solve_hierarchy_context(self, instance): + context = instance.context + + final_context = ( + context.data["hierarchyContext"] + if context.data.get("hierarchyContext") + else {} + ) + + name = instance.data["asset"] + + # get handles + handle_start = int(instance.data["handleStart"]) + handle_end = int(instance.data["handleEnd"]) + + in_info = { + "entity_type": "Shot", + "custom_attributes": { + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "clipIn": instance.data["clipIn"], + "clipOut": instance.data["clipOut"], + "fps": instance.data["fps"] + }, + "tasks": instance.data["tasks"] + } + + parents = instance.data.get('parents', []) + self.log.debug(f"parents: {pformat(parents)}") + + actual = {name: in_info} + + for parent in reversed(parents): + parent_name = parent["entity_name"] + next_dict = { + parent_name: { + "entity_type": parent["entity_type"], + "childs": actual + } + } + actual = next_dict + + final_context = self._update_dict(final_context, actual) + + # adding hierarchy context to instance + context.data["hierarchyContext"] = final_context + self.log.debug(pformat(final_context)) + + def _update_dict(self, ex_dict, new_dict): + for key in ex_dict: + if key in new_dict and isinstance(ex_dict[key], dict): + new_dict[key] = self._update_dict(ex_dict[key], new_dict[key]) + else: + if ex_dict.get(key) and new_dict.get(key): + continue + else: + new_dict[key] = ex_dict[key] + + return new_dict \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py similarity index 98% rename from openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py rename to openpype/plugins/publish/extract_trim_video_audio.py index 51dc84e9a2..b0c30283d9 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -14,7 +14,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): # must be before `ExtractThumbnailSP` order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Trim Video/Audio" - hosts = ["standalonepublisher"] + hosts = ["standalonepublisher", "traypublisher"] families = ["clip", "trimming"] # make sure it is enabled only if at least both families are available diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index bc1f9b9e6c..daeb442f28 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -24,6 +24,10 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): if instance.data.get("assetEntity"): self.log.info("Instance has set asset document in its data.") + elif "editorial" in instance.data.get("creator_identifier", ""): + # skip if it is editorial + self.log.info("Editorial instance is no need to check...") + else: raise PublishValidationError(( "Instance \"{}\" doesn't have asset document " From 284c152ff0d892d273763f0428c4fce645162886 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Jul 2022 12:47:23 +0100 Subject: [PATCH 141/432] Reopen previous level after the update --- openpype/hosts/unreal/plugins/load/load_layout.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index b2d5b43e1e..4fdfac51c8 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -788,6 +788,16 @@ class LayoutLoader(plugin.Loader): sequences = ar.get_assets(filter) sequence = sequences[0].get_asset() + prev_level = None + + if not master_level: + curr_level = unreal.LevelEditorSubsystem().get_current_level() + curr_level_path = curr_level.get_outer().get_path_name() + # If the level path does not start with "/Game/", the current + # level is a temporary, unsaved level. + if curr_level_path.startswith("/Game/"): + prev_level = curr_level_path + # Get layout level filter = unreal.ARFilter( class_names=["World"], @@ -832,6 +842,8 @@ class LayoutLoader(plugin.Loader): if master_level: EditorLevelLibrary.load_level(master_level) + elif prev_level: + EditorLevelLibrary.load_level(prev_level) def remove(self, container): """ From c9ad287c7b4521da8c56ccf4d197c3e3befed61f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 16:40:14 +0200 Subject: [PATCH 142/432] trayp: fix import after develop merge --- openpype/hosts/traypublisher/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 0683b149ec..a0c42a55b1 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,5 +1,5 @@ from openpype.lib.attribute_definitions import FileDef -from openpype.pipeline import ( +from openpype.pipeline.create import ( Creator, HiddenCreator, CreatedInstance From ec7e441cea078bdeccf448b69855fd810a627d0c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 17:06:27 +0200 Subject: [PATCH 143/432] trayp: changing extension propagation --- .../defaults/project_settings/traypublisher.json | 14 +++----------- .../schema_project_traypublisher.json | 12 ++++++++---- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index c360dc2a13..2cb7d358ed 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -276,27 +276,19 @@ "family": "review", "variant": "Reference", "review": true, - "filter_ext": [ - "mov", - "mp4" - ] + "output_file_type": ".mp4" }, { "family": "plate", "variant": "", "review": false, - "filter_ext": [ - "mov", - "mp4" - ] + "output_file_type": ".mov" }, { "family": "audio", "variant": "", "review": false, - "filter_ext": [ - "wav" - ] + "output_file_type": ".wav" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index f77d5fbe06..7c61aeed50 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -256,10 +256,14 @@ "default": true }, { - "type": "list", - "key": "filter_ext", - "label": "Allowed input file types", - "object_type": "text" + "type": "enum", + "key": "output_file_type", + "label": "Integrating file type", + "enum_items": [ + {".mp4": "MP4"}, + {".mov": "MOV"}, + {".wav": "WAV"} + ] } ] } From 3845c90f95f073657f3a07b0d5df7ebf4e99e8c7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 17:06:56 +0200 Subject: [PATCH 144/432] trayp: solving an issue with ocio media source --- .../plugins/create/create_editorial.py | 139 ++++++++++++++---- 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index b87253a705..28e58804c7 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -17,6 +17,8 @@ from openpype.hosts.traypublisher.api.editorial import ( from openpype.pipeline import CreatedInstance from openpype.lib import ( + get_ffprobe_data, + FileDef, TextDef, NumberDef, @@ -212,9 +214,16 @@ or updating already created. Publishing will create OTIO file. "fps": fps }) + # get path of sequence + sequence_path_data = pre_create_data["sequence_filepath_data"] + media_path_data = pre_create_data["media_filepaths_data"] + + sequence_path = self._get_path_from_file_data(sequence_path_data) + media_path = self._get_path_from_file_data(media_path_data) + # get otio timeline - otio_timeline = self._create_otio_instance( - subset_name, instance_data, pre_create_data) + otio_timeline = self._create_otio_timeline( + sequence_path, fps) # Create all clip instances clip_instance_properties.update({ @@ -222,43 +231,52 @@ or updating already created. Publishing will create OTIO file. "parent_asset_name": asset_name, "variant": instance_data["variant"] }) + + # create clip instances self._get_clip_instances( otio_timeline, + media_path, clip_instance_properties, family_presets=allowed_family_presets ) - def _create_otio_instance(self, subset_name, data, pre_create_data): - # get path of sequence - file_path_data = pre_create_data["sequence_filepath_data"] - media_path_data = pre_create_data["media_filepaths_data"] - - file_path = self._get_path_from_file_data(file_path_data) - media_path = self._get_path_from_file_data(media_path_data) - - # get editorial sequence file into otio timeline object - extension = os.path.splitext(file_path)[1] - kwargs = {} - if extension == ".edl": - # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is asssumed. - kwargs["rate"] = data["fps"] - - self.log.info(f"kwargs: {kwargs}") - otio_timeline = otio.adapters.read_from_file( - file_path, **kwargs) + # create otio editorial instance + self._create_otio_instance( + subset_name, instance_data, + sequence_path, media_path, + otio_timeline + ) + def _create_otio_instance( + self, + subset_name, + data, + sequence_path, + media_path, + otio_timeline + ): # Pass precreate data to creator attributes data.update({ - "sequenceFilePath": file_path, + "sequenceFilePath": sequence_path, "editorialSourcePath": media_path, "otioTimeline": otio.adapters.write_to_string(otio_timeline) }) self._create_instance(self.family, subset_name, data) - return otio_timeline + def _create_otio_timeline(self, sequence_path, fps): + # get editorial sequence file into otio timeline object + extension = os.path.splitext(sequence_path)[1] + + kwargs = {} + if extension == ".edl": + # EDL has no frame rate embedded so needs explicit + # frame rate else 24 is asssumed. + kwargs["rate"] = fps + + self.log.info(f"kwargs: {kwargs}") + return otio.adapters.read_from_file(sequence_path, **kwargs) def _get_path_from_file_data(self, file_path_data): # TODO: just temporarly solving only one media file @@ -275,6 +293,7 @@ or updating already created. Publishing will create OTIO file. def _get_clip_instances( self, otio_timeline, + media_path, clip_instance_properties, family_presets ): @@ -284,6 +303,9 @@ or updating already created. Publishing will create OTIO file. descended_from_type=otio.schema.Track ) + # media data for audio sream and reference solving + media_data = self._get_media_source_metadata(media_path) + for track in tracks: self.log.debug(f"track.name: {track.name}") try: @@ -298,10 +320,15 @@ or updating already created. Publishing will create OTIO file. self.log.debug(f"track_start_frame: {track_start_frame}") for clip in track.each_child(): - if not self._validate_clip_for_processing(clip): continue + # get available frames info to clip data + self._create_otio_reference(clip, media_path, media_data) + + # convert timeline range to source range + self._restore_otio_source_range(clip) + base_instance_data = self._get_base_instance_data( clip, clip_instance_properties, @@ -326,6 +353,68 @@ or updating already created. Publishing will create OTIO file. ) self.log.debug(f"{pformat(dict(instance.data))}") + def _restore_otio_source_range(self, otio_clip): + otio_clip.source_range = otio_clip.range_in_parent() + + def _create_otio_reference( + self, + otio_clip, + media_path, + media_data + ): + start_frame = media_data["start_frame"] + frame_duration = media_data["duration"] + fps = media_data["fps"] + + available_range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + start_frame, fps), + duration=otio.opentime.RationalTime( + frame_duration, fps) + ) + # in case old OTIO or video file create `ExternalReference` + media_reference = otio.schema.ExternalReference( + target_url=media_path, + available_range=available_range + ) + + otio_clip.media_reference = media_reference + + def _get_media_source_metadata(self, full_input_path_single_file): + return_data = {} + + try: + media_data = get_ffprobe_data( + full_input_path_single_file, self.log + ) + self.log.debug(f"__ media_data: {pformat(media_data)}") + + # get video stream data + video_stream = media_data["streams"][0] + return_data = { + "video": True, + "start_frame": 0, + "duration": int(video_stream["nb_frames"]), + "fps": float(video_stream["r_frame_rate"][:-2]) + } + + # get audio streams data + audio_stream = [ + stream for stream in media_data["streams"] + if stream["codec_type"] == "audio" + ] + + if audio_stream: + return_data["audio"] = True + + except Exception as exc: + raise AssertionError(( + "FFprobe couldn't read information about input file: " + f"\"{full_input_path_single_file}\". Error message: {exc}" + )) + + return return_data + def _make_subset_instance( self, clip, @@ -355,7 +444,7 @@ or updating already created. Publishing will create OTIO file. else: # add review family if defined future_instance_data.update({ - "filterExt": _fpreset["filter_ext"], + "outputFileType": _fpreset["output_file_type"], "parent_instance_id": parenting_data["instance_id"], "creator_attributes": { "parent_instance": parenting_data["instance_label"] From 968cbe8b984304769be9730dd3cff2633db00a7e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 17:07:18 +0200 Subject: [PATCH 145/432] removing plugin which will not be needed --- .../publish/collect_editorial_resources.py | 271 ------------------ 1 file changed, 271 deletions(-) delete mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py deleted file mode 100644 index 33a852e7a5..0000000000 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_resources.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import re -import tempfile -import pyblish.api -from copy import deepcopy -import clique - - -class CollectInstanceResources(pyblish.api.InstancePlugin): - """Collect instance's resources""" - - # must be after `CollectInstances` - order = pyblish.api.CollectorOrder - label = "Collect Editorial Resources" - hosts = ["standalonepublisher"] - families = ["clip"] - - def process(self, instance): - self.context = instance.context - self.log.info(f"Processing instance: {instance}") - self.new_instances = [] - subset_files = dict() - subset_dirs = list() - anatomy = self.context.data["anatomy"] - anatomy_data = deepcopy(self.context.data["anatomyData"]) - anatomy_data.update({"root": anatomy.roots}) - - subset = instance.data["subset"] - clip_name = instance.data["clipName"] - - editorial_source_root = instance.data["editorialSourceRoot"] - editorial_source_path = instance.data["editorialSourcePath"] - - # if `editorial_source_path` then loop through - if editorial_source_path: - # add family if mov or mp4 found which is longer for - # cutting `trimming` to enable `ExtractTrimmingVideoAudio` plugin - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - instance.data["stagingDir"] = staging_dir - instance.data["families"] += ["trimming"] - return - - # if template pattern in path then fill it with `anatomy_data` - if "{" in editorial_source_root: - editorial_source_root = editorial_source_root.format( - **anatomy_data) - - self.log.debug(f"root: {editorial_source_root}") - # loop `editorial_source_root` and find clip name in folders - # and look for any subset name alternatives - for root, dirs, _files in os.walk(editorial_source_root): - # search only for directories related to clip name - correct_clip_dir = None - for _d_search in dirs: - # avoid all non clip dirs - if _d_search not in clip_name: - continue - # found correct dir for clip - correct_clip_dir = _d_search - - # continue if clip dir was not found - if not correct_clip_dir: - continue - - clip_dir_path = os.path.join(root, correct_clip_dir) - subset_files_items = list() - # list content of clip dir and search for subset items - for subset_item in os.listdir(clip_dir_path): - # avoid all items which are not defined as subsets by name - if subset not in subset_item: - continue - - subset_item_path = os.path.join( - clip_dir_path, subset_item) - # if it is dir store it to `subset_dirs` list - if os.path.isdir(subset_item_path): - subset_dirs.append(subset_item_path) - - # if it is file then store it to `subset_files` list - if os.path.isfile(subset_item_path): - subset_files_items.append(subset_item_path) - - if subset_files_items: - subset_files.update({clip_dir_path: subset_files_items}) - - # break the loop if correct_clip_dir was captured - # no need to cary on if correct folder was found - if correct_clip_dir: - break - - if subset_dirs: - # look all dirs and check for subset name alternatives - for _dir in subset_dirs: - instance_data = deepcopy( - {k: v for k, v in instance.data.items()}) - sub_dir = os.path.basename(_dir) - # if subset name is only alternative then create new instance - if sub_dir != subset: - instance_data = self.duplicate_instance( - instance_data, subset, sub_dir) - - # create all representations - self.create_representations( - os.listdir(_dir), instance_data, _dir) - - if sub_dir == subset: - self.new_instances.append(instance_data) - # instance.data.update(instance_data) - - if subset_files: - unique_subset_names = list() - root_dir = list(subset_files.keys()).pop() - files_list = subset_files[root_dir] - search_pattern = f"({subset}[A-Za-z0-9]+)(?=[\\._\\s])" - for _file in files_list: - pattern = re.compile(search_pattern) - match = pattern.findall(_file) - if not match: - continue - match_subset = match.pop() - if match_subset in unique_subset_names: - continue - unique_subset_names.append(match_subset) - - self.log.debug(f"unique_subset_names: {unique_subset_names}") - - for _un_subs in unique_subset_names: - instance_data = self.duplicate_instance( - instance.data, subset, _un_subs) - - # create all representations - self.create_representations( - [os.path.basename(f) for f in files_list - if _un_subs in f], - instance_data, root_dir) - - # remove the original instance as it had been used only - # as template and is duplicated - self.context.remove(instance) - - # create all instances in self.new_instances into context - for new_instance in self.new_instances: - _new_instance = self.context.create_instance( - new_instance["name"]) - _new_instance.data.update(new_instance) - - def duplicate_instance(self, instance_data, subset, new_subset): - - new_instance_data = dict() - for _key, _value in instance_data.items(): - new_instance_data[_key] = _value - if not isinstance(_value, str): - continue - if subset in _value: - new_instance_data[_key] = _value.replace( - subset, new_subset) - - self.log.info(f"Creating new instance: {new_instance_data['name']}") - self.new_instances.append(new_instance_data) - return new_instance_data - - def create_representations( - self, files_list, instance_data, staging_dir): - """ Create representations from Collection object - """ - # collecting frames for later frame start/end reset - frames = list() - # break down Collection object to collections and reminders - collections, remainder = clique.assemble(files_list) - # add staging_dir to instance_data - instance_data["stagingDir"] = staging_dir - # add representations to instance_data - instance_data["representations"] = list() - - collection_head_name = None - # loop through collections and create representations - for _collection in collections: - ext = _collection.tail[1:] - collection_head_name = _collection.head - frame_start = list(_collection.indexes)[0] - frame_end = list(_collection.indexes)[-1] - repre_data = { - "frameStart": frame_start, - "frameEnd": frame_end, - "name": ext, - "ext": ext, - "files": [item for item in _collection], - "stagingDir": staging_dir - } - - if instance_data.get("keepSequence"): - repre_data_keep = deepcopy(repre_data) - instance_data["representations"].append(repre_data_keep) - - if "review" in instance_data["families"]: - repre_data.update({ - "thumbnail": True, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": self.context.data.get("fps"), - "name": "review", - "tags": ["review", "ftrackreview", "delete"], - }) - instance_data["representations"].append(repre_data) - - # add to frames for frame range reset - frames.append(frame_start) - frames.append(frame_end) - - # loop through reminders and create representations - for _reminding_file in remainder: - ext = os.path.splitext(_reminding_file)[-1][1:] - if ext not in instance_data["extensions"]: - continue - if collection_head_name and ( - (collection_head_name + ext) not in _reminding_file - ) and (ext in ["mp4", "mov"]): - self.log.info(f"Skipping file: {_reminding_file}") - continue - frame_start = 1 - frame_end = 1 - - repre_data = { - "name": ext, - "ext": ext, - "files": _reminding_file, - "stagingDir": staging_dir - } - - # exception for thumbnail - if "thumb" in _reminding_file: - repre_data.update({ - 'name': "thumbnail", - 'thumbnail': True - }) - - # exception for mp4 preview - if ext in ["mp4", "mov"]: - frame_start = 0 - frame_end = ( - (instance_data["frameEnd"] - instance_data["frameStart"]) - + 1) - # add review ftrack family into families - for _family in ["review", "ftrack"]: - if _family not in instance_data["families"]: - instance_data["families"].append(_family) - repre_data.update({ - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": self.context.data.get("fps"), - "name": "review", - "thumbnail": True, - "tags": ["review", "ftrackreview", "delete"], - }) - - # add to frames for frame range reset only if no collection - if not collections: - frames.append(frame_start) - frames.append(frame_end) - - instance_data["representations"].append(repre_data) - - # reset frame start / end - instance_data["frameStart"] = min(frames) - instance_data["frameEnd"] = max(frames) From eff02322efb897bb4649130b011dd2ad46a9bb87 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 17:08:16 +0200 Subject: [PATCH 146/432] general: adding traypublisher host --- openpype/plugins/publish/collect_otio_frame_ranges.py | 2 +- openpype/plugins/publish/collect_otio_subset_resources.py | 8 +++----- openpype/plugins/publish/extract_otio_trimming_video.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index c86e777850..40e89e29bc 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -23,7 +23,7 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): label = "Collect OTIO Frame Ranges" order = pyblish.api.CollectorOrder - 0.08 families = ["shot", "clip"] - hosts = ["resolve", "hiero", "flame"] + hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, instance): # get basic variables diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index fc6a9b50f2..ca29b82f4e 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -23,7 +23,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): label = "Collect OTIO Subset Resources" order = pyblish.api.CollectorOrder - 0.077 families = ["clip"] - hosts = ["resolve", "hiero", "flame"] + hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, instance): @@ -198,7 +198,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): if kwargs.get("collection"): collection = kwargs.get("collection") - files = [f for f in collection] + files = list(collection) ext = collection.format("{tail}") representation_data.update({ "name": ext[1:], @@ -220,7 +220,5 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): }) if kwargs.get("trim") is True: - representation_data.update({ - "tags": ["trim"] - }) + representation_data["tags"] = ["trim"] return representation_data diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 19625fa568..46a4056a9d 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -20,7 +20,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): order = api.ExtractorOrder label = "Extract OTIO trim longer video" families = ["trim"] - hosts = ["resolve", "hiero", "flame"] + hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, instance): self.staging_dir = self.staging_dir(instance) From 4bd7d4f43e5fa0ba1a3e7db06867ddd28c52fcd1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 21:50:33 +0200 Subject: [PATCH 147/432] trayp: adding reivew toggle to instance also add audio family condition for available ffmpeg streams --- .../plugins/create/create_editorial.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 28e58804c7..55c4ca76b7 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -91,6 +91,15 @@ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): return new_instance + def get_instance_attr_defs(self): + return [ + BoolDef( + "add_review_family", + default=True, + label="Review" + ) + ] + class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): identifier = "editorial_shot" @@ -114,7 +123,6 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): attr_defs.extend(CLIP_ATTR_DEFS) return attr_defs - class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): identifier = "editorial_plate" family = "plate" @@ -345,6 +353,13 @@ or updating already created. Publishing will create OTIO file. )) for _fpreset in family_presets: + # exclude audio family if no audio stream + if ( + _fpreset["family"] == "audio" + and not media_data.get("audio") + ): + continue + instance = self._make_subset_instance( clip, _fpreset, @@ -447,12 +462,8 @@ or updating already created. Publishing will create OTIO file. "outputFileType": _fpreset["output_file_type"], "parent_instance_id": parenting_data["instance_id"], "creator_attributes": { - "parent_instance": parenting_data["instance_label"] - }, - "publish_attributes": { - "CollectReviewFamily": { - "add_review_family": _fpreset.get("review") - } + "parent_instance": parenting_data["instance_label"], + "add_review_family": _fpreset.get("review") } }) From 49f67f0aca708d0b60055ccefadd414734159c56 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 21:51:09 +0200 Subject: [PATCH 148/432] trayp: collect reviewable for editorial --- .../publish/collect_editorial_reviewable.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py new file mode 100644 index 0000000000..6cd8c42546 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py @@ -0,0 +1,30 @@ +import os + +import pyblish.api + + +class CollectEditorialReviewable(pyblish.api.InstancePlugin): + """Collect reviwiewable toggle to instance and representation data + """ + + label = "Collect Editorial Reviewable" + order = pyblish.api.CollectorOrder + + families = ["plate", "review", "audio"] + hosts = ["traypublisher"] + + def process(self, instance): + creator_identifier = instance.data["creator_identifier"] + if "editorial" not in creator_identifier: + return + + creator_attributes = instance.data["creator_attributes"] + repre = instance.data["representations"][0] + + if creator_attributes["add_review_family"]: + repre["tags"].append("review") + instance.data["families"].append("review") + + instance.data["representations"] = [repre] + + self.log.debug("instance.data {}".format(instance.data)) From 0c95e86ccc2735c25d3a5d9bcd31a62083a7ce67 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 21:51:40 +0200 Subject: [PATCH 149/432] trayp: add more keys to sync between editorial instances --- .../traypublisher/plugins/publish/collect_shot_instances.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 5abafa498d..86505f76c5 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -15,12 +15,16 @@ class CollectShotInstance(pyblish.api.InstancePlugin): SHARED_KEYS = [ "asset", "fps", + "handleStart", + "handleEnd", "frameStart", "frameEnd", "clipIn", "clipOut", "sourceIn", - "sourceOut" + "sourceOut", + "otioClip", + "workfileFrameStart" ] def process(self, instance): From 7793ea5580595c95d875b056bdf1a61685f63a1d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 20 Jul 2022 19:26:10 +0100 Subject: [PATCH 150/432] Added documentation for UE5, layout and rendering --- website/docs/artist_hosts_unreal.md | 132 +++++++++++++++++- website/docs/assets/unreal-avalon_tools.jpg | Bin 25212 -> 0 bytes website/docs/assets/unreal-container.jpg | Bin 10414 -> 0 bytes website/docs/assets/unreal_add_level.png | Bin 0 -> 8393 bytes website/docs/assets/unreal_container.jpg | Bin 0 -> 10414 bytes website/docs/assets/unreal_create_render.png | Bin 0 -> 124745 bytes .../unreal_layout_loading_no_sequence.png | Bin 0 -> 12529 bytes .../assets/unreal_layout_loading_result.png | Bin 0 -> 23833 bytes website/docs/assets/unreal_level_list.png | Bin 0 -> 21009 bytes .../assets/unreal_level_list_no_sequences.png | Bin 0 -> 10784 bytes .../assets/unreal_level_streaming_method.png | Bin 0 -> 153336 bytes ...al_level_streaming_method_no_sequences.png | Bin 0 -> 82416 bytes website/docs/assets/unreal_load_layout.png | Bin 0 -> 138927 bytes .../docs/assets/unreal_load_layout_batch.png | Bin 0 -> 121461 bytes website/docs/assets/unreal_openpype_tools.png | Bin 0 -> 27332 bytes .../assets/unreal_openpype_tools_create.png | Bin 0 -> 27449 bytes .../assets/unreal_openpype_tools_load.png | Bin 0 -> 27465 bytes .../assets/unreal_openpype_tools_manage.png | Bin 0 -> 27475 bytes .../assets/unreal_openpype_tools_publish.png | Bin 0 -> 27453 bytes .../assets/unreal_openpype_tools_render.png | Bin 0 -> 27453 bytes website/docs/assets/unreal_publish_render.png | Bin 0 -> 247922 bytes .../assets/unreal_setting_level_sequence.png | Bin 0 -> 3881 bytes 22 files changed, 127 insertions(+), 5 deletions(-) delete mode 100644 website/docs/assets/unreal-avalon_tools.jpg delete mode 100644 website/docs/assets/unreal-container.jpg create mode 100644 website/docs/assets/unreal_add_level.png create mode 100644 website/docs/assets/unreal_container.jpg create mode 100644 website/docs/assets/unreal_create_render.png create mode 100644 website/docs/assets/unreal_layout_loading_no_sequence.png create mode 100644 website/docs/assets/unreal_layout_loading_result.png create mode 100644 website/docs/assets/unreal_level_list.png create mode 100644 website/docs/assets/unreal_level_list_no_sequences.png create mode 100644 website/docs/assets/unreal_level_streaming_method.png create mode 100644 website/docs/assets/unreal_level_streaming_method_no_sequences.png create mode 100644 website/docs/assets/unreal_load_layout.png create mode 100644 website/docs/assets/unreal_load_layout_batch.png create mode 100644 website/docs/assets/unreal_openpype_tools.png create mode 100644 website/docs/assets/unreal_openpype_tools_create.png create mode 100644 website/docs/assets/unreal_openpype_tools_load.png create mode 100644 website/docs/assets/unreal_openpype_tools_manage.png create mode 100644 website/docs/assets/unreal_openpype_tools_publish.png create mode 100644 website/docs/assets/unreal_openpype_tools_render.png create mode 100644 website/docs/assets/unreal_publish_render.png create mode 100644 website/docs/assets/unreal_setting_level_sequence.png diff --git a/website/docs/artist_hosts_unreal.md b/website/docs/artist_hosts_unreal.md index 1ff09893e3..45a0c8bb6f 100644 --- a/website/docs/artist_hosts_unreal.md +++ b/website/docs/artist_hosts_unreal.md @@ -8,6 +8,20 @@ sidebar_label: Unreal OpenPype supports Unreal in similar ways as in other DCCs Yet there are few specific you need to be aware of. +### Creating the Unreal project + +Selecting a task and opening it with Unreal will generate the Unreal project, if it hasn't been created before. +By default, OpenPype includes the plugin that will be built together with the project. + +Alternatively, the Environment variable `"OPENPYPE_UNREAL_PLUGIN"` can be set to the path of a compiled version of the plugin. +The version of the compiled plugin must match the version of Unreal with which the project is being created. + +:::note +Unreal version 5.0 onwards requires the following Environment variable: + +`"UE_PYTHONPATH": "{PYTHONPATH}"` +::: + ### Project naming Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are @@ -15,9 +29,9 @@ invalid. If OpenPype detects such name it automatically prepends letter **P** to ## OpenPype global tools -OpenPype global tools can be found in *Window* main menu: +OpenPype global tools can be found in Unreal's toolbar and in the *Tools* main menu: -![Unreal OpenPype Menu](assets/unreal-avalon_tools.jpg) +![Unreal OpenPype Menu](assets/unreal_openpype_tools.png) - [Create](artist_tools.md#creator) - [Load](artist_tools.md#loader) @@ -31,10 +45,118 @@ OpenPype global tools can be found in *Window* main menu: To import Static Mesh model, just choose **OpenPype → Load ...** and select your mesh. Static meshes are transferred as FBX files as specified in [Unreal Engine 4 Static Mesh Pipeline](https://docs.unrealengine.com/en-US/Engine/Content/Importing/FBX/StaticMeshes/index.html). This action will create new folder with subset name (`unrealStaticMeshMain_CON` for example) and put all data into it. Inside, you can find: -![Unreal Container Content](assets/unreal-container.jpg) +![Unreal Container Content](assets/unreal_container.jpg) -In this case there is **lambert1**, material pulled from Maya when this static mesh was published, **unrealStaticMeshCube** is the geometry itself, **unrealStaticMeshCube_CON** is a *AssetContainer* type and is there to mark this directory as Avalon Container (to track changes) and to hold OpenPype metadata. +In this case there is **lambert1**, material pulled from Maya when this static mesh was published, **antennaA_modelMain** is the geometry itself, **modelMain_v002_CON** is a *AssetContainer* type and is there to mark this directory as Avalon Container (to track changes) and to hold OpenPype metadata. ### Publishing -Publishing of Static Mesh works in similar ways. Select your mesh in *Content Browser* and **OpenPype → Create ...**. This will create folder named by subset you've chosen - for example **unrealStaticMeshDefault_INS**. It this folder is that mesh and *Avalon Publish Instance* asset marking this folder as publishable instance and holding important metadata on it. If you want to publish this instance, go **OpenPype → Publish ...** \ No newline at end of file +Publishing of Static Mesh works in similar ways. Select your mesh in *Content Browser* and **OpenPype → Create ...**. This will create folder named by subset you've chosen - for example **unrealStaticMeshDefault_INS**. It this folder is that mesh and *Avalon Publish Instance* asset marking this folder as publishable instance and holding important metadata on it. If you want to publish this instance, go **OpenPype → Publish ...** + +## Layout + +There are two different layout options in Unreal, depending on the type of project you are working on. +One only imports the layout, and saves it in a level. +The other uses [Master Sequences](https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/Sequencer/Overview/TracksShot/) to track the whole level sequence hierarchy. +You can choose in the Project Settings if you want to generate the level sequences. + +![Unreal OP Settings Level Sequence](assets/unreal_setting_level_sequence.png) + +### Loading + +To load a layout, click on the OpenPype icon in Unreal’s main taskbar, and select **Load**. + +![Unreal OP Tools Load](assets/unreal_openpype_tools_load.png) + +Select the task on the left, then right click on the layout asset and select **Load Layout**. + +![Unreal Layout Load](assets/unreal_load_layout.png) + +If you need to load multiple layouts, you can select more than one task on the left, and you can load them together. + +![Unreal Layout Load Batch](assets/unreal_load_layout_batch.png) + +### Navigating the project + +The layout will be imported in the directory `/Content/OpenPype`. The layout will be split into two subfolders: +- *Assets*, which will contain all the rigs and models contained in the layout; +- *Asset name* (in the following example, *episode 2*), a folder named as the **asset** of the current **task**. + +![Unreal Layout Loading Result](assets/unreal_layout_loading_result.png) + +If you chose to generate the level sequences, in the second folder you will find the master level for the task (usually an episode), the level sequence and the folders for all the scenes in the episodes. +Otherwise you will find the level generated for the loaded layout. + +#### Layout without level sequences + +In the layout folder, you will find the level with the imported layout and an object of *AssetContainer* type. The latter is there to mark this directory as Avalon Container (to track changes) and to hold OpenPype metadata. + +![Unreal Layout Loading No Sequence](assets/unreal_layout_loading_no_sequence.png) + +The layout level will and should contain only the data included in the layout. To add lighting, or other elements, like an environment, you have to create a master level, and add the layout level as a [streaming level](https://docs.unrealengine.com/5.0/en-US/level-streaming-in-unreal-engine/). + +Create the master level and open it. Then, open the *Levels* window (from the menu **Windows → Levels**). Click on **Levels → Add Existing** and select the layout level and the other levels you with to include in the scene. The following example shows a master level in which have been added a light level and the layout level. + +![Unreal Add Level](assets/unreal_add_level.png) +![Unreal Level List](assets/unreal_level_list_no_sequences.png) + +#### Layout with level sequences + +In the episode folder, you will find the master level for the episode, the master level sequence and the folders for all the scenes in the episodes. + +After opening the master level, open the *Levels* window (from the menu **Windows → Levels**), and you will see the list of the levels of each shot of the episode for which a layout has been loaded. + +![Unreal Level List](assets/unreal_level_list.png) + +If it has not been added already, you will need to add the environment to the level. Click on **Levels → Add Existing** and select the level with the environment (check with the studio where it is located). + +![Unreal Add Level](assets/unreal_add_level.png) + +After adding the environment level to the master level, you will need to set it as always loaded by right clicking it, and selecting **Change Streaming Method** and selecting **Always Loaded**. + +![Unreal Level Streaming Method](assets/unreal_level_streaming_method.png) + +### Update layouts + +To manage loaded layouts, click on the OpenPype icon in Unreal’s main taskbar, and select **Manage**. + +![Unreal OP Tools Manage](assets/unreal_openpype_tools_manage.png) + +You will get a list of all the assets that have been loaded in the project. +The version number will be in red if it isn’t the latest version. Right click on the element, and select Update if you need to update the layout. + +:::note +**DO NOT** update rigs or models imported with a layout. Update only the layout. +::: + +## Rendering + +:::note +The rendering requires a layout loaded with the option to create the level sequences **on**. +::: + +To render and publish an episode, a scene or a shot, you will need to create a publish instance. The publish instance for the rendering is based on one level sequence. That means that if you want to render the whole episode, you will need to create it for the level sequence of the episode, but if you want to render just one shot, you will need to create it for that shot. + +Navigate to the folder that contains the level sequence that you need to render. Select the level sequence, and then click on the OpenPype icon in Unreal’s main taskbar, and select **Create**. + +![Unreal OP Tools Create](assets/unreal_openpype_tools_create.png) + +In the Instance Creator, select **Unreal - Render**, give it a name, and click **Create**. + +![Unreal OP Instance Creator](assets/unreal_create_render.png) + +The render instance will be created in `/Content/OpenPype/PublishInstances`. + +Select the instance you need to render, and then click on the OpenPype icon in Unreal’s main taskbar, and select **Render**. You can render more than one instance at a time, if needed. Just select all the instances that you need to render before selecting the **Render** button from the OpenPype menu. + +![Unreal OP Tools Render](assets/unreal_openpype_tools_render.png) + +Once the render is finished, click on the OpenPype icon in Unreal’s main taskbar, and select **Publish**. + +![Unreal OP Tools Publish](assets/unreal_openpype_tools_publish.png) + +On the left, you will see the render instances. They will be automatically reorganised to have an instance for each shot. So, for example, if you have created the render instance for the whole episode, here you will have an instance for each shot in the episode. + +![Unreal Publish Render](assets/unreal_publish_render.png) + +Click on the play button in the bottom right, and it will start the publishing process. diff --git a/website/docs/assets/unreal-avalon_tools.jpg b/website/docs/assets/unreal-avalon_tools.jpg deleted file mode 100644 index 531fbe516a2d0e645140c4a102afbbfa4cbdbbd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25212 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 diff --git a/website/docs/assets/unreal-container.jpg b/website/docs/assets/unreal-container.jpg deleted file mode 100644 index f0c0a61e9519359a5dd980295e0d1d2b3d573e2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10414 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 diff --git a/website/docs/assets/unreal_add_level.png b/website/docs/assets/unreal_add_level.png new file mode 100644 index 0000000000000000000000000000000000000000..caeef03d10148da9cccfdbc8b0816b130709710e GIT binary patch literal 8393 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 VC)c$H{QcmOlDxWHwTwl`{{l*bXLbMp literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_container.jpg b/website/docs/assets/unreal_container.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0fda640b00a4327255a316d96683dc9ae2331903 GIT binary patch literal 10414 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_create_render.png b/website/docs/assets/unreal_create_render.png new file mode 100644 index 0000000000000000000000000000000000000000..2e3ef20b35bf1d89bc77946472886d4719bf7b66 GIT binary patch literal 124745 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

sT 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∨e?`UX!=eGccIe-aKm{I|H{ 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


8;lMR(C3OiYA(rG%8Odl)3#q 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 z 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�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 t5%TuMTpYfozivRT!Iw4)68MdOwxt*N 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 SDDz~X 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 z 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
    J* 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_openpype_tools_manage.png b/website/docs/assets/unreal_openpype_tools_manage.png new file mode 100644 index 0000000000000000000000000000000000000000..af7b182842f7046776a32dd3e5dc70e6101ed3b0 GIT binary patch literal 27475 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 RPrtlEl9N`Fs+2Ga{2$Lx;9md$ literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_openpype_tools_render.png b/website/docs/assets/unreal_openpype_tools_render.png new file mode 100644 index 0000000000000000000000000000000000000000..377dc2951eb1040153b5ada28254b06cb9a89fb9 GIT binary patch literal 27453 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

    `CB2QUkU(80j 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 `~(bU{R)4ugh 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 z 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 Date: Thu, 21 Jul 2022 08:22:49 +0200 Subject: [PATCH 151/432] general: making exctract trim video audio compatible with traypublisher --- .../publish/extract_trim_video_audio.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py index b0c30283d9..8136ff1a6a 100644 --- a/openpype/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -40,6 +40,20 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): fps = instance.data["fps"] video_file_path = instance.data["editorialSourcePath"] extensions = instance.data.get("extensions", ["mov"]) + output_file_type = instance.data.get("outputFileType") + + frame_start = int(instance.data["frameStart"]) + frame_end = int(instance.data["frameEnd"]) + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + + clip_start_h = float(instance.data["clipInH"]) + _dur = instance.data["clipDuration"] + handle_dur = (handle_start + handle_end) + clip_dur_h = float(_dur + handle_dur) + + if output_file_type: + extensions = [output_file_type] for ext in extensions: self.log.info("Processing ext: `{}`".format(ext)) @@ -49,16 +63,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): clip_trimed_path = os.path.join( staging_dir, instance.data["name"] + ext) - # # check video file metadata - # input_data = plib.get_ffprobe_streams(video_file_path)[0] - # self.log.debug(f"__ input_data: `{input_data}`") - - start = float(instance.data["clipInH"]) - dur = float(instance.data["clipDurationH"]) if ext == ".wav": # offset time as ffmpeg is having bug - start += 0.5 + clip_start_h += 0.5 # remove "review" from families instance.data["families"] = [ fml for fml in instance.data["families"] @@ -67,9 +75,9 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): ffmpeg_args = [ ffmpeg_path, - "-ss", str(start / fps), + "-ss", str(clip_start_h / fps), "-i", video_file_path, - "-t", str(dur / fps) + "-t", str(clip_dur_h / fps) ] if ext in [".mov", ".mp4"]: ffmpeg_args.extend([ @@ -98,10 +106,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): "ext": ext[1:], "files": os.path.basename(clip_trimed_path), "stagingDir": staging_dir, - "frameStart": int(instance.data["frameStart"]), - "frameEnd": int(instance.data["frameEnd"]), - "frameStartFtrack": int(instance.data["frameStartH"]), - "frameEndFtrack": int(instance.data["frameEndH"]), + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartFtrack": frame_start - handle_start, + "frameEndFtrack": frame_end + handle_end, "fps": fps, } From 34f43fe86a664877e7695a09f9e3a29388db0ca1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 08:24:01 +0200 Subject: [PATCH 152/432] trayp: passing clipDuration attribute --- .../hosts/traypublisher/plugins/create/create_editorial.py | 2 +- .../traypublisher/plugins/publish/collect_clip_instances.py | 4 ++-- .../traypublisher/plugins/publish/collect_shot_instances.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 55c4ca76b7..899a45e269 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -455,7 +455,6 @@ or updating already created. Publishing will create OTIO file. "instance_label": label, "instance_id": c_instance.data["instance_id"] }) - else: # add review family if defined future_instance_data.update({ @@ -623,6 +622,7 @@ or updating already created. Publishing will create OTIO file. "frameEnd": int(frame_end), "clipIn": int(clip_in), "clipOut": int(clip_out), + "clipDuration": int(clip.duration().value), "sourceIn": int(source_in), "sourceOut": int(source_out) } diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py index e3dfb1512a..bc86cb8ef3 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py @@ -6,7 +6,7 @@ class CollectClipInstance(pyblish.api.InstancePlugin): """Collect clip instances and resolve its parent""" label = "Collect Clip Instances" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.081 hosts = ["traypublisher"] families = ["plate", "review", "audio"] @@ -29,4 +29,4 @@ class CollectClipInstance(pyblish.api.InstancePlugin): instance.context.data["editorialSourcePath"]) instance.data["families"].append("trimming") - self.log.debug(pformat(instance.data)) \ No newline at end of file + self.log.debug(pformat(instance.data)) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 86505f76c5..9d8ed8ed72 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -21,6 +21,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "frameEnd", "clipIn", "clipOut", + "clipDuration", "sourceIn", "sourceOut", "otioClip", @@ -99,6 +100,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "frameEnd": workfile_start_frame + frame_dur, "clipIn": _cr_attrs["clipIn"], "clipOut": _cr_attrs["clipOut"], + "clipDuration": _cr_attrs["clipDuration"], "sourceIn": _cr_attrs["sourceIn"], "sourceOut": _cr_attrs["sourceOut"], "workfileFrameStart": workfile_start_frame From 449fabf449fcc47e807807dfc8fcbfc9b11a4bc2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 08:24:29 +0200 Subject: [PATCH 153/432] global: removing trayp host from plugins --- .../publish/collect_otio_subset_resources.py | 20 +++++++++---------- .../publish/extract_otio_trimming_video.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index ca29b82f4e..9c19f8a78e 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -23,7 +23,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): label = "Collect OTIO Subset Resources" order = pyblish.api.CollectorOrder - 0.077 families = ["clip"] - hosts = ["resolve", "hiero", "flame", "traypublisher"] + hosts = ["resolve", "hiero", "flame"] def process(self, instance): @@ -116,8 +116,10 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # check in two way if it is sequence if hasattr(otio.schema, "ImageSequenceReference"): # for OpenTimelineIO 0.13 and newer - if isinstance(media_ref, - otio.schema.ImageSequenceReference): + if isinstance( + media_ref, + otio.schema.ImageSequenceReference + ): is_sequence = True else: # for OpenTimelineIO 0.12 and older @@ -139,11 +141,9 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): padding=media_ref.frame_zero_padding ) collection.indexes.update( - [i for i in range(a_frame_start_h, (a_frame_end_h + 1))]) + list(range(a_frame_start_h, (a_frame_end_h + 1))) + ) - self.log.debug(collection) - repre = self._create_representation( - frame_start, frame_end, collection=collection) else: # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` @@ -152,9 +152,9 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): path, trimmed_media_range_h, metadata) self.staging_dir, collection = collection_data - self.log.debug(collection) - repre = self._create_representation( - frame_start, frame_end, collection=collection) + self.log.debug(collection) + repre = self._create_representation( + frame_start, frame_end, collection=collection) else: _trim = False dirname, filename = os.path.split(media_ref.target_url) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 46a4056a9d..19625fa568 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -20,7 +20,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): order = api.ExtractorOrder label = "Extract OTIO trim longer video" families = ["trim"] - hosts = ["resolve", "hiero", "flame", "traypublisher"] + hosts = ["resolve", "hiero", "flame"] def process(self, instance): self.staging_dir = self.staging_dir(instance) From 09182b312eca0ec853e2a2536b2426a0d5218e6e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 08:31:34 +0200 Subject: [PATCH 154/432] ftrack: adding options for plugin to settings --- openpype/settings/defaults/project_settings/ftrack.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 70cda68cb4..f6074d5464 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -447,6 +447,9 @@ "enabled": false, "ftrack_custom_attributes": {} }, + "IntegrateFtrackComponentOverwrite": { + "enabled": true + }, "IntegrateFtrackInstance": { "family_mapping": { "camera": "cam", From b60384f534c8df83738ca35985c74ce1e83b7c03 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:12:02 +0200 Subject: [PATCH 155/432] ftrack: optional plugin with optional attributes --- .../integrate_ftrack_component_overwrite.py | 5 ++++- .../projects_schema/schema_project_ftrack.json | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py index 047fd8462c..8cb2336391 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py @@ -13,7 +13,10 @@ class IntegrateFtrackComponentOverwrite(pyblish.api.InstancePlugin): active = False def process(self, instance): - component_list = instance.data['ftrackComponentsList'] + component_list = instance.data.get('ftrackComponentsList') + if not component_list: + self.log.info("No component to overwrite...") + return for cl in component_list: cl['component_overwrite'] = 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 e008fd85ee..c06bec0f58 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -930,6 +930,21 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "IntegrateFtrackComponentOverwrite", + "label": "IntegrateFtrackComponentOverwrite", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "dict", "key": "IntegrateFtrackInstance", From bb9c03a94f1f0060424aa52973e3f14746cd475b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:12:22 +0200 Subject: [PATCH 156/432] ftrack: adding additional families to settings --- openpype/settings/defaults/project_settings/ftrack.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f6074d5464..3e86581a03 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -301,7 +301,9 @@ "traypublisher" ], "families": [ - "plate" + "plate", + "review", + "audio" ], "task_types": [], "tasks": [], From cc47c30d5a45cad1805b2c796f5fae2b214d18ee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:12:52 +0200 Subject: [PATCH 157/432] global: adding trayp families to plugins --- openpype/plugins/publish/extract_otio_file.py | 2 +- openpype/plugins/publish/validate_editorial_asset_name.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_file.py b/openpype/plugins/publish/extract_otio_file.py index 3bd217d5d4..4d310ce109 100644 --- a/openpype/plugins/publish/extract_otio_file.py +++ b/openpype/plugins/publish/extract_otio_file.py @@ -12,7 +12,7 @@ class ExtractOTIOFile(openpype.api.Extractor): label = "Extract OTIO file" order = pyblish.api.ExtractorOrder - 0.45 families = ["workfile"] - hosts = ["resolve", "hiero"] + hosts = ["resolve", "hiero", "traypublisher"] def process(self, instance): # create representation data diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 702e87b58d..694788c414 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -19,7 +19,8 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): "hiero", "standalonepublisher", "resolve", - "flame" + "flame", + "traypublisher" ] def process(self, context): From f7a6a606f53ae3f3ea376dd546f8f7958953c17b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:13:13 +0200 Subject: [PATCH 158/432] global: dealing with reviewable in trim audio/video plugin --- openpype/plugins/publish/extract_trim_video_audio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py index 8136ff1a6a..06817c4b5a 100644 --- a/openpype/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -41,6 +41,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): video_file_path = instance.data["editorialSourcePath"] extensions = instance.data.get("extensions", ["mov"]) output_file_type = instance.data.get("outputFileType") + reviewable = "review" in instance.data["families"] frame_start = int(instance.data["frameStart"]) frame_end = int(instance.data["frameEnd"]) @@ -111,9 +112,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): "frameStartFtrack": frame_start - handle_start, "frameEndFtrack": frame_end + handle_end, "fps": fps, + "tags": [] } - if ext in [".mov", ".mp4"]: + if ext in [".mov", ".mp4"] and reviewable: repre.update({ "thumbnail": True, "tags": ["review", "ftrackreview", "delete"]}) From 7e6569fdd261481fde442c85452219441ceb629d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:13:36 +0200 Subject: [PATCH 159/432] global: adding trayp family --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 1a5d74bf26..b8855ee2bd 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -65,7 +65,13 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["hiero", "resolve", "standalonepublisher", "flame"] + hosts = [ + "hiero", + "resolve", + "standalonepublisher", + "flame", + "traypublisher" + ] optional = False def process(self, context): From 97879475732a5edef30d1ff625b6e2b01a0dd81a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:14:13 +0200 Subject: [PATCH 160/432] trayp: collect review input to instance data --- .../plugins/publish/collect_editorial_reviewable.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py index 6cd8c42546..2e4ad9e181 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py @@ -19,12 +19,8 @@ class CollectEditorialReviewable(pyblish.api.InstancePlugin): return creator_attributes = instance.data["creator_attributes"] - repre = instance.data["representations"][0] if creator_attributes["add_review_family"]: - repre["tags"].append("review") instance.data["families"].append("review") - instance.data["representations"] = [repre] - self.log.debug("instance.data {}".format(instance.data)) From 0ea71b05fb0e38c925a771dd551088344ce2479e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 09:24:28 +0200 Subject: [PATCH 161/432] global: adding review family to filters with non trayp exception --- openpype/plugins/publish/extract_thumbnail.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 7933595b89..b4c4bb2036 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -20,7 +20,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder families = [ "imagesequence", "render", "render2d", "prerender", - "source", "plate", "take" + "source", "plate", "take", "review" ] hosts = ["shell", "fusion", "resolve", "traypublisher"] enabled = False @@ -29,6 +29,14 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ffmpeg_args = None def process(self, instance): + # make sure this apply only to reveiw in both family keys + # HACK: only traypublisher review family is allowed + if ( + instance.data["family"] != "review" + and "review" in instance.data["families"] + ): + return + self.log.info("subset {}".format(instance.data['subset'])) # skip crypto passes. From dc7856e919d3b7536c1bd5643d1b0e7ccbc8d059 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 11:56:20 +0200 Subject: [PATCH 162/432] trayp: processing PR comments --- openpype/hosts/traypublisher/api/editorial.py | 20 ++++--- .../plugins/create/create_editorial.py | 55 ++++++------------- .../plugins/create/create_from_settings.py | 3 - .../plugins/publish/collect_clip_instances.py | 8 ++- .../publish/collect_editorial_reviewable.py | 2 - .../plugins/publish/collect_shot_instances.py | 2 +- 6 files changed, 37 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 948e05ec61..d6f876ab76 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -5,7 +5,7 @@ from openpype.client import get_asset_by_id from openpype.pipeline.create import CreatorError -class ShotMetadataSover: +class ShotMetadataSolver: """Collecting hierarchy context from `parents` and `hierarchy` data present in `clip` family instances coming from the request json data file @@ -22,12 +22,18 @@ class ShotMetadataSover: shot_hierarchy = None shot_add_tasks = None - def __init__(self, creator_settings, logger): - self.clip_name_tokenizer = creator_settings["clip_name_tokenizer"] - self.shot_rename = creator_settings["shot_rename"] - self.shot_hierarchy = creator_settings["shot_hierarchy"] - self.shot_add_tasks = creator_settings["shot_add_tasks"] - + def __init__( + self, + clip_name_tokenizer, + shot_rename, + shot_hierarchy, + shot_add_tasks, + logger + ): + self.clip_name_tokenizer = clip_name_tokenizer + self.shot_rename = shot_rename + self.shot_hierarchy = shot_hierarchy + self.shot_add_tasks = shot_add_tasks self.log = logger def _rename_template(self, data): diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 899a45e269..7b2585d630 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -11,7 +11,7 @@ from openpype.hosts.traypublisher.api.plugin import ( HiddenTrayPublishCreator ) from openpype.hosts.traypublisher.api.editorial import ( - ShotMetadataSover + ShotMetadataSolver ) from openpype.pipeline import CreatedInstance @@ -65,13 +65,6 @@ CLIP_ATTR_DEFS = [ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): host_name = "traypublisher" - def __init__( - self, project_settings, *args, **kwargs - ): - super(EditorialClipInstanceCreatorBase, self).__init__( - project_settings, *args, **kwargs - ) - def create(self, instance_data, source_data=None): self.log.info(f"instance_data: {instance_data}") subset_name = instance_data["subset"] @@ -106,13 +99,6 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): family = "shot" label = "Editorial Shot" - def __init__( - self, project_settings, *args, **kwargs - ): - super(EditorialShotInstanceCreator, self).__init__( - project_settings, *args, **kwargs - ) - def get_instance_attr_defs(self): attr_defs = [ TextDef( @@ -123,44 +109,24 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): attr_defs.extend(CLIP_ATTR_DEFS) return attr_defs + class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): identifier = "editorial_plate" family = "plate" label = "Editorial Plate" - def __init__( - self, project_settings, *args, **kwargs - ): - super(EditorialPlateInstanceCreator, self).__init__( - project_settings, *args, **kwargs - ) - class EditorialAudioInstanceCreator(EditorialClipInstanceCreatorBase): identifier = "editorial_audio" family = "audio" label = "Editorial Audio" - def __init__( - self, project_settings, *args, **kwargs - ): - super(EditorialAudioInstanceCreator, self).__init__( - project_settings, *args, **kwargs - ) - class EditorialReviewInstanceCreator(EditorialClipInstanceCreatorBase): identifier = "editorial_review" family = "review" label = "Editorial Review" - def __init__( - self, project_settings, *args, **kwargs - ): - super(EditorialReviewInstanceCreator, self).__init__( - project_settings, *args, **kwargs - ) - class EditorialSimpleCreator(TrayPublishCreator): @@ -188,8 +154,19 @@ or updating already created. Publishing will create OTIO file. ) # get this creator settings by identifier self._creator_settings = editorial_creators.get(self.identifier) - self._shot_metadata_solver = ShotMetadataSover( - self._creator_settings, self.log) + + clip_name_tokenizer = self._creator_settings["clip_name_tokenizer"] + shot_rename = self._creator_settings["shot_rename"] + shot_hierarchy = self._creator_settings["shot_hierarchy"] + shot_add_tasks = self._creator_settings["shot_add_tasks"] + + self._shot_metadata_solver = ShotMetadataSolver( + clip_name_tokenizer, + shot_rename, + shot_hierarchy, + shot_add_tasks, + self.log + ) # try to set main attributes from settings if self._creator_settings.get("default_variants"): @@ -717,4 +694,4 @@ or updating already created. Publishing will create OTIO file. attr_defs.append(UISeparatorDef()) attr_defs.extend(CLIP_ATTR_DEFS) - return attr_defs \ No newline at end of file + return attr_defs diff --git a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py index 1271e03fdb..41c1c29bb0 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py +++ b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py @@ -1,5 +1,4 @@ import os -from pprint import pformat from openpype.api import get_project_settings, Logger log = Logger.get_logger(__name__) @@ -16,8 +15,6 @@ def initialize(): global_variables = globals() for item in simple_creators: - log.debug(pformat(item)) - dynamic_plugin = SettingsCreator.from_settings(item) global_variables[dynamic_plugin.__name__] = dynamic_plugin diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py index bc86cb8ef3..ca269a9c27 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py @@ -13,7 +13,13 @@ class CollectClipInstance(pyblish.api.InstancePlugin): def process(self, instance): creator_identifier = instance.data["creator_identifier"] - if "editorial" not in creator_identifier: + if ( + creator_identifier not in [ + "editorial_plate", + "editorial_audio", + "editorial_review" + ] + ): return instance.data["families"].append("clip") diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py index 2e4ad9e181..34f7a9ead8 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py @@ -1,5 +1,3 @@ -import os - import pyblish.api diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 9d8ed8ed72..e6f1173bc4 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -166,4 +166,4 @@ class CollectShotInstance(pyblish.api.InstancePlugin): else: new_dict[key] = ex_dict[key] - return new_dict \ No newline at end of file + return new_dict From 5d49d9c3d2876bddc4a1856b50e6ef94bf0c90d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 12:29:38 +0200 Subject: [PATCH 163/432] trayp: adding universal attribute for new asset creation --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 ++ openpype/plugins/publish/validate_asset_docs.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 7b2585d630..fcaaeb1e75 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -550,6 +550,8 @@ or updating already created. Publishing will create OTIO file. "asset": parent_asset_name, "task": "", + "new_asset_publishing": True, + # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index daeb442f28..9f997d4817 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -24,7 +24,7 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): if instance.data.get("assetEntity"): self.log.info("Instance has set asset document in its data.") - elif "editorial" in instance.data.get("creator_identifier", ""): + elif instance.context.data.get("new_asset_publishing"): # skip if it is editorial self.log.info("Editorial instance is no need to check...") From 7c30798bec528b8410fda39dd409022696afbf95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 12:30:11 +0200 Subject: [PATCH 164/432] global: removing redundant check --- openpype/plugins/publish/extract_thumbnail.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index b4c4bb2036..89738a8063 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -20,7 +20,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder families = [ "imagesequence", "render", "render2d", "prerender", - "source", "plate", "take", "review" + "source", "clip", "take" ] hosts = ["shell", "fusion", "resolve", "traypublisher"] enabled = False @@ -29,13 +29,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ffmpeg_args = None def process(self, instance): - # make sure this apply only to reveiw in both family keys - # HACK: only traypublisher review family is allowed - if ( - instance.data["family"] != "review" - and "review" in instance.data["families"] - ): - return self.log.info("subset {}".format(instance.data['subset'])) From 60adefa5ccf4cf737c8f78338e8e8a5173045726 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 14:54:21 +0200 Subject: [PATCH 165/432] global: renaming `newAssetPublishing` --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 +- openpype/plugins/publish/validate_asset_docs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index fcaaeb1e75..db0287129a 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -550,7 +550,7 @@ or updating already created. Publishing will create OTIO file. "asset": parent_asset_name, "task": "", - "new_asset_publishing": True, + "newAssetPublishing": True, # parent time properties "trackStartFrame": track_start_frame, diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index 9f997d4817..dbec9edd7b 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -24,7 +24,7 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): if instance.data.get("assetEntity"): self.log.info("Instance has set asset document in its data.") - elif instance.context.data.get("new_asset_publishing"): + elif instance.context.data.get("newAssetPublishing"): # skip if it is editorial self.log.info("Editorial instance is no need to check...") From cf2e5177dd6b7635cdcf0b53720375abf67dd2c2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 15:32:25 +0200 Subject: [PATCH 166/432] trayp: adding docstrings --- openpype/hosts/traypublisher/api/editorial.py | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index d6f876ab76..92ad65a851 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -6,12 +6,12 @@ from openpype.pipeline.create import CreatorError class ShotMetadataSolver: - """Collecting hierarchy context from `parents` and `hierarchy` data - present in `clip` family instances coming from the request json data file + """ Solving hierarchical metadata - It will add `hierarchical_context` into each instance for integrate - plugins to be able to create needed parents for the context if they - don't exist yet + Used during editorial publishing. Works with imput + clip name and settings defining python formatable + template. Settings also define searching patterns + and its token keys used for formating in templates. """ NO_DECOR_PATERN = re.compile(r"\{([a-z]*?)\}") @@ -37,6 +37,17 @@ class ShotMetadataSolver: self.log = logger def _rename_template(self, data): + """Shot renaming function + + Args: + data (dict): formating data + + Raises: + CreatorError: If missing keys + + Returns: + str: formated new name + """ shot_rename_template = self.shot_rename[ "shot_rename_template"] try: @@ -51,6 +62,20 @@ class ShotMetadataSolver: )) def _generate_tokens(self, clip_name, source_data): + """Token generator + + Settings defines token pairs key and regex expression. + + Args: + clip_name (str): name of clip in editorial + source_data (dict): data for formating + + Raises: + CreatorError: if missing key + + Returns: + dict: updated source_data + """ output_data = deepcopy(source_data["anatomy_data"]) output_data["clip_name"] = clip_name @@ -78,7 +103,20 @@ class ShotMetadataSolver: return output_data def _create_parents_from_settings(self, parents, data): + """Formating parent components. + Args: + parents (list): list of dict parent components + data (dict): formating data + + Raises: + CreatorError: missing formating key + CreatorError: missing token key + KeyError: missing parent token + + Returns: + list: list of dict of parent components + """ # fill the parents parts from presets shot_hierarchy = deepcopy(self.shot_hierarchy) hierarchy_parents = shot_hierarchy["parents"] @@ -152,6 +190,14 @@ class ShotMetadataSolver: return parents def _create_hierarchy_path(self, parents): + """Converting hierarchy path from parents + + Args: + parents (list): list of dict parent components + + Returns: + str: hierarchy path + """ return "/".join( [ p["entity_name"] for p in parents @@ -164,6 +210,17 @@ class ShotMetadataSolver: asset_doc, project_doc ): + """Returning parents from context on selected asset. + + Context defined in Traypublisher project tree. + + Args: + asset_doc (db obj): selected asset doc + project_doc (db obj): actual project doc + + Returns: + list: list of dict parent components + """ project_name = project_doc["name"] visual_hierarchy = [asset_doc] current_doc = asset_doc @@ -192,6 +249,17 @@ class ShotMetadataSolver: ] def _generate_tasks_from_settings(self, project_doc): + """Convert settings inputs to task data. + + Args: + project_doc (db obj): actual project doc + + Raises: + KeyError: Missing task type in project doc + + Returns: + dict: tasks data + """ tasks_to_add = {} project_tasks = project_doc["config"]["tasks"] @@ -214,6 +282,17 @@ class ShotMetadataSolver: return tasks_to_add def generate_data(self, clip_name, source_data): + """Metadata generator. + + Converts input data to hierarchy mentadata. + + Args: + clip_name (str): clip name + source_data (dict): formating data + + Returns: + (str, dict): shot name and hierarchy data + """ self.log.info(f"_ source_data: {source_data}") tasks = {} From 976411521bf4e7f2db521813b9622e16dd62e800 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 11:10:41 +0200 Subject: [PATCH 167/432] trayp: addresing issue from PR - different edl test https://github.com/pypeclub/OpenPype/pull/3492#pullrequestreview-1047573472 --- .../traypublisher/plugins/create/create_editorial.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index db0287129a..d6d669a56c 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -18,6 +18,7 @@ from openpype.pipeline import CreatedInstance from openpype.lib import ( get_ffprobe_data, + convert_ffprobe_fps_value, FileDef, TextDef, @@ -259,6 +260,7 @@ or updating already created. Publishing will create OTIO file. # EDL has no frame rate embedded so needs explicit # frame rate else 24 is asssumed. kwargs["rate"] = fps + kwargs["ignore_timecode_mismatch"] = True self.log.info(f"kwargs: {kwargs}") return otio.adapters.read_from_file(sequence_path, **kwargs) @@ -387,7 +389,11 @@ or updating already created. Publishing will create OTIO file. "video": True, "start_frame": 0, "duration": int(video_stream["nb_frames"]), - "fps": float(video_stream["r_frame_rate"][:-2]) + "fps": float( + convert_ffprobe_fps_value( + video_stream["r_frame_rate"] + ) + ) } # get audio streams data From c34a1270a29c6d660b1c7f40dcca259171b1a553 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 13:05:58 +0200 Subject: [PATCH 168/432] trayp: adding docstrings --- .../plugins/create/create_editorial.py | 290 ++++++++++++++---- 1 file changed, 238 insertions(+), 52 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index d6d669a56c..3bc8f89556 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -64,6 +64,11 @@ CLIP_ATTR_DEFS = [ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): + """ Wrapper class for clip family creators + + Args: + HiddenTrayPublishCreator (BaseCreator): hidden supporting class + """ host_name = "traypublisher" def create(self, instance_data, source_data=None): @@ -96,6 +101,13 @@ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): + """ Shot family class + + The shot metadata instance carrier. + + Args: + EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class + """ identifier = "editorial_shot" family = "shot" label = "Editorial Shot" @@ -112,24 +124,54 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): + """ Plate family class + + Plate representation instance. + + Args: + EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class + """ identifier = "editorial_plate" family = "plate" label = "Editorial Plate" class EditorialAudioInstanceCreator(EditorialClipInstanceCreatorBase): + """ Audio family class + + Audio representation instance. + + Args: + EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class + """ identifier = "editorial_audio" family = "audio" label = "Editorial Audio" class EditorialReviewInstanceCreator(EditorialClipInstanceCreatorBase): + """ Review family class + + Review representation instance. + + Args: + EditorialClipInstanceCreatorBase (BaseCreator): hidden supporting class + """ identifier = "editorial_review" family = "review" label = "Editorial Review" class EditorialSimpleCreator(TrayPublishCreator): + """ Editorial creator class + + Simple workflow creator. This creator only disecting input + video file into clip chunks and then converts each to + defined format defined Settings for each subset preset. + + Args: + TrayPublishCreator (Creator): Tray publisher plugin class + """ label = "Editorial Simple" family = "editorial" @@ -242,6 +284,15 @@ or updating already created. Publishing will create OTIO file. media_path, otio_timeline ): + """Otio instance creating function + + Args: + subset_name (str): name of subset + data (dict): instnance data + sequence_path (str): path to sequence file + media_path (str): path to media file + otio_timeline (otio.Timeline): otio timeline object + """ # Pass precreate data to creator attributes data.update({ "sequenceFilePath": sequence_path, @@ -252,6 +303,15 @@ or updating already created. Publishing will create OTIO file. self._create_instance(self.family, subset_name, data) def _create_otio_timeline(self, sequence_path, fps): + """Creating otio timeline from sequence path + + Args: + sequence_path (str): path to sequence file + fps (float): frame per second + + Returns: + otio.Timeline: otio timeline object + """ # get editorial sequence file into otio timeline object extension = os.path.splitext(sequence_path)[1] @@ -266,6 +326,17 @@ or updating already created. Publishing will create OTIO file. return otio.adapters.read_from_file(sequence_path, **kwargs) def _get_path_from_file_data(self, file_path_data): + """Converting creator path data to single path string + + Args: + file_path_data (FileDefItem): creator path data inputs + + Raises: + FileExistsError: in case nothing had been set + + Returns: + str: path string + """ # TODO: just temporarly solving only one media file if isinstance(file_path_data, list): file_path_data = file_path_data.pop() @@ -281,9 +352,17 @@ or updating already created. Publishing will create OTIO file. self, otio_timeline, media_path, - clip_instance_properties, + instance_data, family_presets ): + """Helping function fro creating clip instance + + Args: + otio_timeline (otio.Timeline): otio timeline object + media_path (str): media file path string + instance_data (dict): clip instance data + family_presets (list): list of dict settings subset presets + """ self.asset_name_check = [] tracks = otio_timeline.each_child( @@ -318,7 +397,7 @@ or updating already created. Publishing will create OTIO file. base_instance_data = self._get_base_instance_data( clip, - clip_instance_properties, + instance_data, track_start_frame ) @@ -348,6 +427,14 @@ or updating already created. Publishing will create OTIO file. self.log.debug(f"{pformat(dict(instance.data))}") def _restore_otio_source_range(self, otio_clip): + """Infusing source range. + + Otio clip is missing proper source clip range so + here we add them from from parent timeline frame range. + + Args: + otio_clip (otio.Clip): otio clip object + """ otio_clip.source_range = otio_clip.range_in_parent() def _create_otio_reference( @@ -356,6 +443,13 @@ or updating already created. Publishing will create OTIO file. media_path, media_data ): + """Creating otio reference at otio clip. + + Args: + otio_clip (otio.Clip): otio clip object + media_path (str): media file path string + media_data (dict): media metadata + """ start_frame = media_data["start_frame"] frame_duration = media_data["duration"] fps = media_data["fps"] @@ -374,12 +468,23 @@ or updating already created. Publishing will create OTIO file. otio_clip.media_reference = media_reference - def _get_media_source_metadata(self, full_input_path_single_file): + def _get_media_source_metadata(self, path): + """Get all available metadata from file + + Args: + path (str): media file path string + + Raises: + AssertionError: ffprobe couldn't read metadata + + Returns: + dict: media file metadata + """ return_data = {} try: media_data = get_ffprobe_data( - full_input_path_single_file, self.log + path, self.log ) self.log.debug(f"__ media_data: {pformat(media_data)}") @@ -408,44 +513,55 @@ or updating already created. Publishing will create OTIO file. except Exception as exc: raise AssertionError(( "FFprobe couldn't read information about input file: " - f"\"{full_input_path_single_file}\". Error message: {exc}" + f"\"{path}\". Error message: {exc}" )) return return_data def _make_subset_instance( self, - clip, - _fpreset, - future_instance_data, + otio_clip, + preset, + instance_data, parenting_data ): - family = _fpreset["family"] + """Making subset instance from input preset + + Args: + otio_clip (otio.Clip): otio clip object + preset (dict): sigle family preset + instance_data (dict): instance data + parenting_data (dict): shot instance parent data + + Returns: + CreatedInstance: creator instance object + """ + family = preset["family"] label = self._make_subset_naming( - _fpreset, - future_instance_data + preset, + instance_data ) - future_instance_data["label"] = label + instance_data["label"] = label # add file extension filter only if it is not shot family if family == "shot": - future_instance_data["otioClip"] = ( - otio.adapters.write_to_string(clip)) + instance_data["otioClip"] = ( + otio.adapters.write_to_string(otio_clip)) c_instance = self.create_context.creators[ "editorial_shot"].create( - future_instance_data) + instance_data) parenting_data.update({ "instance_label": label, "instance_id": c_instance.data["instance_id"] }) else: # add review family if defined - future_instance_data.update({ - "outputFileType": _fpreset["output_file_type"], + instance_data.update({ + "outputFileType": preset["output_file_type"], "parent_instance_id": parenting_data["instance_id"], "creator_attributes": { "parent_instance": parenting_data["instance_label"], - "add_review_family": _fpreset.get("review") + "add_review_family": preset.get("review") } }) @@ -453,24 +569,33 @@ or updating already created. Publishing will create OTIO file. editorial_clip_creator = self.create_context.creators[ creator_identifier] c_instance = editorial_clip_creator.create( - future_instance_data) + instance_data) return c_instance def _make_subset_naming( self, - _fpreset, - future_instance_data + preset, + instance_data ): - shot_name = future_instance_data["shotName"] - variant_name = future_instance_data["variant"] - family = _fpreset["family"] + """ Subset name maker + + Args: + preset (dict): single preset item + instance_data (dict): instance data + + Returns: + str: label string + """ + shot_name = instance_data["shotName"] + variant_name = instance_data["variant"] + family = preset["family"] # get variant name from preset or from inharitance - _variant_name = _fpreset.get("variant") or variant_name + _variant_name = preset.get("variant") or variant_name self.log.debug(f"__ family: {family}") - self.log.debug(f"__ _fpreset: {_fpreset}") + self.log.debug(f"__ preset: {preset}") # subset name subset_name = "{}{}".format( @@ -481,7 +606,7 @@ or updating already created. Publishing will create OTIO file. subset_name ) - future_instance_data.update({ + instance_data.update({ "family": family, "label": label, "variant": _variant_name, @@ -492,21 +617,31 @@ or updating already created. Publishing will create OTIO file. def _get_base_instance_data( self, - clip, - clip_instance_properties, + otio_clip, + instance_data, track_start_frame, ): + """ Factoring basic set of instance data. + + Args: + otio_clip (otio.Clip): otio clip object + instance_data (dict): precreate instance data + track_start_frame (int): track start frame + + Returns: + dict: instance data + """ # get clip instance properties - parent_asset_name = clip_instance_properties["parent_asset_name"] - handle_start = clip_instance_properties["handle_start"] - handle_end = clip_instance_properties["handle_end"] - timeline_offset = clip_instance_properties["timeline_offset"] - workfile_start_frame = clip_instance_properties["workfile_start_frame"] - fps = clip_instance_properties["fps"] - variant_name = clip_instance_properties["variant"] + parent_asset_name = instance_data["parent_asset_name"] + handle_start = instance_data["handle_start"] + handle_end = instance_data["handle_end"] + timeline_offset = instance_data["timeline_offset"] + workfile_start_frame = instance_data["workfile_start_frame"] + fps = instance_data["fps"] + variant_name = instance_data["variant"] # basic unique asset name - clip_name = os.path.splitext(clip.name)[0].lower() + clip_name = os.path.splitext(otio_clip.name)[0].lower() project_doc = get_project(self.project_name) shot_name, shot_metadata = self._shot_metadata_solver.generate_data( @@ -529,7 +664,7 @@ or updating already created. Publishing will create OTIO file. self._validate_name_uniqueness(shot_name) timing_data = self._get_timing_data( - clip, + otio_clip, timeline_offset, track_start_frame, workfile_start_frame @@ -571,15 +706,26 @@ or updating already created. Publishing will create OTIO file. def _get_timing_data( self, - clip, + otio_clip, timeline_offset, track_start_frame, workfile_start_frame ): + """Returning available timing data + + Args: + otio_clip (otio.Clip): otio clip object + timeline_offset (int): offset value + track_start_frame (int): starting frame input + workfile_start_frame (int): start frame for shot's workfiles + + Returns: + dict: timing metadata + """ # frame ranges data - clip_in = clip.range_in_parent().start_time.value + clip_in = otio_clip.range_in_parent().start_time.value clip_in += track_start_frame - clip_out = clip.range_in_parent().end_time_inclusive().value + clip_out = otio_clip.range_in_parent().end_time_inclusive().value clip_out += track_start_frame self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") @@ -589,10 +735,10 @@ or updating already created. Publishing will create OTIO file. clip_in += timeline_offset clip_out += timeline_offset - clip_duration = clip.duration().value + clip_duration = otio_clip.duration().value self.log.info(f"clip duration: {clip_duration}") - source_in = clip.trimmed_range().start_time.value + source_in = otio_clip.trimmed_range().start_time.value source_out = source_in + clip_duration # define starting frame for future shot @@ -607,12 +753,20 @@ or updating already created. Publishing will create OTIO file. "frameEnd": int(frame_end), "clipIn": int(clip_in), "clipOut": int(clip_out), - "clipDuration": int(clip.duration().value), + "clipDuration": int(otio_clip.duration().value), "sourceIn": int(source_in), "sourceOut": int(source_out) } def _get_allowed_family_presets(self, pre_create_data): + """ Filter out allowed family presets. + + Args: + pre_create_data (dict): precreate attributes inputs + + Returns: + list: lit of dict with preset items + """ self.log.debug(f"__ pre_create_data: {pre_create_data}") return [ {"family": "shot"}, @@ -622,41 +776,73 @@ or updating already created. Publishing will create OTIO file. ] ] - def _validate_clip_for_processing(self, clip): - if clip.name is None: + def _validate_clip_for_processing(self, otio_clip): + """Validate otio clip attribues + + Args: + otio_clip (otio.Clip): otio clip object + + Returns: + bool: True if all passing conditions + """ + if otio_clip.name is None: return False - if isinstance(clip, otio.schema.Gap): + if isinstance(otio_clip, otio.schema.Gap): return False # skip all generators like black empty if isinstance( - clip.media_reference, + otio_clip.media_reference, otio.schema.GeneratorReference): return False # Transitions are ignored, because Clips have the full frame # range. - if isinstance(clip, otio.schema.Transition): + if isinstance(otio_clip, otio.schema.Transition): return False return True def _validate_name_uniqueness(self, name): + """ Validating name uniqueness. + + In context of other clip names in sequence file. + + Args: + name (str): shot name string + """ if name not in self.asset_name_check: self.asset_name_check.append(name) else: - self.log.warning(f"duplicate shot name: {name}") + self.log.warning( + f"Duplicate shot name: {name}! " + "Please check names in the input sequence files." + ) - def _create_instance(self, family, subset_name, data): + def _create_instance(self, family, subset_name, instance_data): + """ CreatedInstance object creator + + Args: + family (str): family name + subset_name (str): subset name + instance_data (dict): instance data + """ # Create new instance - new_instance = CreatedInstance(family, subset_name, data, self) + new_instance = CreatedInstance( + family, subset_name, instance_data, self + ) # Host implementation of storing metadata about instance HostContext.add_instance(new_instance.data_to_store()) # Add instance to current context self._add_instance_to_context(new_instance) def get_pre_create_attr_defs(self): + """ Creating pre-create attributes at creator plugin. + + Returns: + list: list of attribute object instances + """ # Use same attributes as for instance attrobites attr_defs = [ FileDef( From 2acf9289a14da87faabc79180c5c7a53d4361000 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 14:00:47 +0200 Subject: [PATCH 169/432] global: change reading from instance rather then context --- openpype/plugins/publish/validate_asset_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index dbec9edd7b..9a1ca5b8de 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -24,7 +24,7 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): if instance.data.get("assetEntity"): self.log.info("Instance has set asset document in its data.") - elif instance.context.data.get("newAssetPublishing"): + elif instance.data.get("newAssetPublishing"): # skip if it is editorial self.log.info("Editorial instance is no need to check...") From f5f7e52c42c9a43a4746683ba7cc0904fadab661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 22 Jul 2022 14:01:48 +0200 Subject: [PATCH 170/432] Update openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_clip_instances.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py index ca269a9c27..bdf7c05f3d 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_clip_instances.py @@ -13,13 +13,11 @@ class CollectClipInstance(pyblish.api.InstancePlugin): def process(self, instance): creator_identifier = instance.data["creator_identifier"] - if ( - creator_identifier not in [ - "editorial_plate", - "editorial_audio", - "editorial_review" - ] - ): + if creator_identifier not in [ + "editorial_plate", + "editorial_audio", + "editorial_review" + ]: return instance.data["families"].append("clip") From 409cd5b870b9ebf7acc70c752c5b900a72ee9fd3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 14:07:44 +0200 Subject: [PATCH 171/432] trayp: processing PR suggestion --- .../plugins/publish/collect_editorial_reviewable.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py index 34f7a9ead8..4af4fb94e9 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py @@ -2,7 +2,9 @@ import pyblish.api class CollectEditorialReviewable(pyblish.api.InstancePlugin): - """Collect reviwiewable toggle to instance and representation data + """ Collect review input from user. + + Adds the input to instance data. """ label = "Collect Editorial Reviewable" @@ -13,7 +15,11 @@ class CollectEditorialReviewable(pyblish.api.InstancePlugin): def process(self, instance): creator_identifier = instance.data["creator_identifier"] - if "editorial" not in creator_identifier: + if creator_identifier not in [ + "editorial_plate", + "editorial_audio", + "editorial_review" + ]: return creator_attributes = instance.data["creator_attributes"] From abfe580eeed15293d929cce4170bb41862a33868 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 14:27:32 +0200 Subject: [PATCH 172/432] trayp: adding docstrings --- .../plugins/publish/collect_shot_instances.py | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index e6f1173bc4..716f73022e 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -4,7 +4,11 @@ import opentimelineio as otio class CollectShotInstance(pyblish.api.InstancePlugin): - """Collect shot instances and resolve its parent""" + """ Collect shot instances + + Resolving its user inputs from creator attributes + to instance data. + """ label = "Collect Shot Instances" order = pyblish.api.CollectorOrder - 0.09 @@ -50,6 +54,19 @@ class CollectShotInstance(pyblish.api.InstancePlugin): self.log.debug(pformat(instance.data)) def _get_otio_clip(self, instance): + """ Converts otio string data. + + Convert them to proper otio object + and finds its equivalent at otio timeline. + This process is a hack to support also + resolving parent range. + + Args: + instance (obj): publishing instance + + Returns: + otio.Clip: otio clip object + """ context = instance.context # convert otio clip from string to object otio_clip_string = instance.data.pop("otioClip") @@ -63,8 +80,6 @@ class CollectShotInstance(pyblish.api.InstancePlugin): descended_from_type=otio.schema.Clip) if clip.name == otio_clip.name ] - self.log.debug(otio_timeline.each_child( - descended_from_type=otio.schema.Clip)) otio_clip = clips.pop() self.log.debug(f"__ otioclip.parent: {otio_clip.parent}") @@ -72,6 +87,14 @@ class CollectShotInstance(pyblish.api.InstancePlugin): return otio_clip def _distribute_shared_data(self, instance): + """ Distribute all defined keys. + + All data are shared between all related + instances in context. + + Args: + instance (obj): publishing instance + """ context = instance.context instance_id = instance.data["instance_id"] @@ -85,6 +108,14 @@ class CollectShotInstance(pyblish.api.InstancePlugin): } def _solve_inputs_to_data(self, instance): + """ Resolve all user inputs into instance data. + + Args: + instance (obj): publishing instance + + Returns: + dict: instance data updating data + """ _cr_attrs = instance.data["creator_attributes"] workfile_start_frame = _cr_attrs["workfile_start_frame"] frame_start = _cr_attrs["frameStart"] @@ -107,6 +138,11 @@ class CollectShotInstance(pyblish.api.InstancePlugin): } def _solve_hierarchy_context(self, instance): + """ Adding hierarchy data to context shared data. + + Args: + instance (obj): publishing instance + """ context = instance.context final_context = ( @@ -157,13 +193,21 @@ class CollectShotInstance(pyblish.api.InstancePlugin): self.log.debug(pformat(final_context)) def _update_dict(self, ex_dict, new_dict): + """ Recursion function + + Updating nested data with another nested data. + + Args: + ex_dict (dict): nested data + new_dict (dict): nested data + + Returns: + dict: updated nested data + """ for key in ex_dict: if key in new_dict and isinstance(ex_dict[key], dict): new_dict[key] = self._update_dict(ex_dict[key], new_dict[key]) - else: - if ex_dict.get(key) and new_dict.get(key): - continue - else: - new_dict[key] = ex_dict[key] + elif not ex_dict.get(key) or not new_dict.get(key): + new_dict[key] = ex_dict[key] return new_dict From f0ca08b4959dde095b5ae4599cdee76fd8ac86f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 16:30:50 +0200 Subject: [PATCH 173/432] nuke: no need to remove slate frame collection is already without it.. --- openpype/hosts/nuke/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 925cab0bef..37ce03dc55 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -181,8 +181,6 @@ class ExporterReview(object): # get first and last frame self.first_frame = min(self.collection.indexes) self.last_frame = max(self.collection.indexes) - if "slate" in self.instance.data["families"]: - self.first_frame += 1 else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." From 0aeb10b78d204e6e3778e8f7dc1078fe9bad6068 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 16:31:12 +0200 Subject: [PATCH 174/432] nuke: no need to convert to int if it already is int --- openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index af5e8e9d27..5f7b1f3806 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -98,7 +98,7 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): self.log.error(msg) raise ValidationException(msg) - collected_frames_len = int(len(collection.indexes)) + collected_frames_len = len(collection.indexes) coll_start = min(collection.indexes) coll_end = max(collection.indexes) From d4f96ae720c258c7ec6895d5398ee2a0c3e96812 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:30:23 +0200 Subject: [PATCH 175/432] change order of some collectors --- openpype/plugins/publish/collect_datetime_data.py | 2 +- openpype/plugins/publish/collect_machine_name.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_datetime_data.py b/openpype/plugins/publish/collect_datetime_data.py index 1675ae1a98..0d21490d8d 100644 --- a/openpype/plugins/publish/collect_datetime_data.py +++ b/openpype/plugins/publish/collect_datetime_data.py @@ -9,7 +9,7 @@ from openpype.api import config class CollectDateTimeData(pyblish.api.ContextPlugin): - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.5 label = "Collect DateTime data" def process(self, context): diff --git a/openpype/plugins/publish/collect_machine_name.py b/openpype/plugins/publish/collect_machine_name.py index 72ef68f8ed..8c25966031 100644 --- a/openpype/plugins/publish/collect_machine_name.py +++ b/openpype/plugins/publish/collect_machine_name.py @@ -11,7 +11,7 @@ import pyblish.api class CollectMachineName(pyblish.api.ContextPlugin): label = "Local Machine Name" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.5 hosts = ["*"] def process(self, context): From 0b88bc1fcd689d8096fe294e48951b2663d49aa9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:31:50 +0200 Subject: [PATCH 176/432] added collector to stored current context into publish context data --- .../publish/collect_current_context.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 openpype/plugins/publish/collect_current_context.py diff --git a/openpype/plugins/publish/collect_current_context.py b/openpype/plugins/publish/collect_current_context.py new file mode 100644 index 0000000000..ebcbc6a4aa --- /dev/null +++ b/openpype/plugins/publish/collect_current_context.py @@ -0,0 +1,44 @@ +""" +Provides: + context -> projectName (str) + context -> asset (str) + context -> task (str) +""" + +import pyblish.api +from openpype.pipeline import legacy_io + + +class CollectCurrentContext(pyblish.api.ContextPlugin): + """Collect project context into publish context data. + + Plugin does not override any value if is already set. + """ + + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect Current context" + + def process(self, context): + # Set project name in context data + project_name = context.data.get("projectName") + asset_name = context.data.get("asset") + task_name = context.data.get("task") + if not project_name: + project_name = legacy_io.current_project() + context.data["projectName"] = project_name + + if not asset_name: + asset_name = legacy_io.Session.get("AVALON_ASSET") + context.data["asset"] = asset_name + + if not task_name: + task_name = legacy_io.Session.get("AVALON_TASK") + context.data["task"] = task_name + + # QUESTION should we be explicit with keys? (the same on instances) + # - 'asset' -> 'assetName' + # - 'task' -> 'taskName' + + self.log.info(( + "Collected project context\nProject: {}\nAsset: {}\nTask: {}" + ).format(project_name, asset_name, task_name)) From 477acd1d5ef55d71117d89b467831347b449989e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:32:05 +0200 Subject: [PATCH 177/432] create context plugin makes sure that project name is set --- openpype/plugins/publish/collect_from_create_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index d2be633cbe..78bd821bfb 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -19,6 +19,9 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): if not create_context: return + project_name = create_context.project_name + if project_name: + context.data["projectName"] = project_name for created_instance in create_context.instances: instance_data = created_instance.data_to_store() if instance_data["active"]: From 9ce6ea6f363eb24ef79c730a671c119b18ee92c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:57:23 +0200 Subject: [PATCH 178/432] make sure legacy io is installed --- openpype/plugins/publish/collect_current_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_current_context.py b/openpype/plugins/publish/collect_current_context.py index ebcbc6a4aa..7e42700d7d 100644 --- a/openpype/plugins/publish/collect_current_context.py +++ b/openpype/plugins/publish/collect_current_context.py @@ -19,7 +19,10 @@ class CollectCurrentContext(pyblish.api.ContextPlugin): label = "Collect Current context" def process(self, context): - # Set project name in context data + # Make sure 'legacy_io' is intalled + legacy_io.install() + + # Check if values are already set project_name = context.data.get("projectName") asset_name = context.data.get("asset") task_name = context.data.get("task") From d585ae526cf1d9306091f242c039e2efa5b29d00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 19:20:03 +0200 Subject: [PATCH 179/432] get project name from 'context.data["projectName"]' or 'anatomy.project_name' at obvious places --- .../submit_maya_remote_publish_deadline.py | 12 +++++------- .../plugins/publish/collect_anatomy_object.py | 11 +++++++---- .../plugins/publish/collect_avalon_entities.py | 12 +++++++----- openpype/plugins/publish/collect_hierarchy.py | 4 +--- .../plugins/publish/collect_rendered_files.py | 16 +++++----------- .../plugins/publish/collect_resources_path.py | 6 +----- .../plugins/publish/integrate_hero_version.py | 6 ++---- openpype/plugins/publish/integrate_thumbnail.py | 3 +-- 8 files changed, 29 insertions(+), 41 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 57572fcb24..6e53099162 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -5,7 +5,6 @@ from maya import cmds from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.settings import get_project_settings -import openpype.api import pyblish.api @@ -34,7 +33,9 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): targets = ["local"] def process(self, instance): - settings = get_project_settings(os.getenv("AVALON_PROJECT")) + project_name = instance.context.data["projectName"] + # TODO settings can be received from 'context.data["project_settings"]' + settings = get_project_settings(project_name) # use setting for publish job on farm, no reason to have it separately deadline_publish_job_sett = (settings["deadline"] ["publish"] @@ -53,9 +54,6 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): scene = instance.context.data["currentFile"] scenename = os.path.basename(scene) - # Get project code - project_name = legacy_io.Session["AVALON_PROJECT"] - job_name = "{scene} [PUBLISH]".format(scene=scenename) batch_name = "{code} - {scene}".format(code=project_name, scene=scenename) @@ -107,8 +105,8 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) - # TODO replace legacy_io with context.data ? - environment["AVALON_PROJECT"] = legacy_io.Session["AVALON_PROJECT"] + # TODO replace legacy_io with context.data + environment["AVALON_PROJECT"] = project_name environment["AVALON_ASSET"] = legacy_io.Session["AVALON_ASSET"] environment["AVALON_TASK"] = legacy_io.Session["AVALON_TASK"] environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index b1415098b6..8128221925 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -1,24 +1,27 @@ """Collect Anatomy object. Requires: - os.environ -> AVALON_PROJECT + context -> projectName Provides: context -> anatomy (openpype.pipeline.anatomy.Anatomy) """ -import os + import pyblish.api from openpype.pipeline import Anatomy class CollectAnatomyObject(pyblish.api.ContextPlugin): - """Collect Anatomy object into Context""" + """Collect Anatomy object into Context. + + Order offset could be changed to '-0.45'. + """ order = pyblish.api.CollectorOrder - 0.4 label = "Collect Anatomy Object" def process(self, context): - project_name = os.environ.get("AVALON_PROJECT") + project_name = context.data.get("projectName") if project_name is None: raise AssertionError( "Environment `AVALON_PROJECT` is not set." diff --git a/openpype/plugins/publish/collect_avalon_entities.py b/openpype/plugins/publish/collect_avalon_entities.py index 6cd0d136e8..0a7afc086f 100644 --- a/openpype/plugins/publish/collect_avalon_entities.py +++ b/openpype/plugins/publish/collect_avalon_entities.py @@ -1,11 +1,13 @@ """Collect Anatomy and global anatomy data. Requires: - session -> AVALON_PROJECT, AVALON_ASSET + session -> AVALON_ASSET + context -> projectName Provides: - context -> projectEntity - project entity from database - context -> assetEntity - asset entity from database + context -> projectEntity - Project document from database. + context -> assetEntity - Asset document from database only if 'asset' is + set in context. """ import pyblish.api @@ -15,14 +17,14 @@ from openpype.pipeline import legacy_io class CollectAvalonEntities(pyblish.api.ContextPlugin): - """Collect Anatomy into Context""" + """Collect Anatomy into Context.""" order = pyblish.api.CollectorOrder - 0.1 label = "Collect Avalon Entities" def process(self, context): legacy_io.install() - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = context.data["projectName"] asset_name = legacy_io.Session["AVALON_ASSET"] task_name = legacy_io.Session["AVALON_TASK"] diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 91d5162d62..687397be8a 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -1,7 +1,5 @@ import pyblish.api -from openpype.pipeline import legacy_io - class CollectHierarchy(pyblish.api.ContextPlugin): """Collecting hierarchy from `parents`. @@ -20,7 +18,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): def process(self, context): temp_context = {} - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = context.data["projectName"] final_context = {} final_context[project_name] = {} final_context[project_name]['entity_type'] = 'Project' diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 670e57ed10..8c5d591148 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -1,7 +1,7 @@ """Loads publishing context from json and continues in publish process. Requires: - anatomy -> context["anatomy"] *(pyblish.api.CollectorOrder - 0.11) + anatomy -> context["anatomy"] *(pyblish.api.CollectorOrder - 0.4) Provides: context, instances -> All data from previous publishing process. @@ -21,6 +21,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): `OPENPYPE_PUBLISH_DATA`. Those files _MUST_ share same context. """ + order = pyblish.api.CollectorOrder - 0.2 # Keep "filesequence" for backwards compatibility of older jobs targets = ["filesequence", "farm"] @@ -122,19 +123,12 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): "Missing `OPENPYPE_PUBLISH_DATA`") paths = os.environ["OPENPYPE_PUBLISH_DATA"].split(os.pathsep) - project_name = os.environ.get("AVALON_PROJECT") - if project_name is None: - raise AssertionError( - "Environment `AVALON_PROJECT` was not found." - "Could not set project `root` which may cause issues." - ) - - # TODO root filling should happen after collect Anatomy + # Using already collected Anatomy + anatomy = context.data["anatomy"] self.log.info("Getting root setting for project \"{}\"".format( - project_name + anatomy.project_name )) - anatomy = context.data["anatomy"] self.log.info("anatomy: {}".format(anatomy.roots)) try: session_is_set = False diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 8bdf70b529..00f65b8b67 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -13,8 +13,6 @@ import copy import pyblish.api -from openpype.pipeline import legacy_io - class CollectResourcesPath(pyblish.api.InstancePlugin): """Generate directory path where the files and resources will be stored""" @@ -58,7 +56,6 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "effect", "staticMesh", "skeletalMesh" - ] def process(self, instance): @@ -86,11 +83,10 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy - project_name = legacy_io.Session["AVALON_PROJECT"] self.log.warning(( "Deprecation warning: Anatomy does not have set `folder`" " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) + ).format(anatomy.project_name)) file_path = anatomy_filled["publish"]["path"] # Directory diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 5f97a9bd41..735b7e50fa 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -71,7 +71,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): template_key = self._get_template_key(instance) anatomy = instance.context.data["anatomy"] - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = anatomy.project_name if template_key not in anatomy.templates: self.log.warning(( "!!! Anatomy of project \"{}\" does not have set" @@ -454,7 +454,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): ) if bulk_writes: - project_name = legacy_io.Session["AVALON_PROJECT"] legacy_io.database[project_name].bulk_write( bulk_writes ) @@ -517,11 +516,10 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(template_data) # solve deprecated situation when `folder` key is not underneath # `publish` anatomy - project_name = legacy_io.Session["AVALON_PROJECT"] self.log.warning(( "Deprecation warning: Anatomy does not have set `folder`" " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) + ).format(anatomy.project_name)) file_path = anatomy_filled[template_key]["path"] # Directory diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index fd50858a91..8ae0dd2d60 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -39,9 +39,8 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ) return - project_name = legacy_io.Session["AVALON_PROJECT"] - anatomy = instance.context.data["anatomy"] + project_name = anatomy.project_name if "publish" not in anatomy.templates: self.log.warning("Anatomy is missing the \"publish\" key!") return From 2453892f3fe12f1eee9615f94ac5c88ab6414f94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 19:21:09 +0200 Subject: [PATCH 180/432] raise KnownPublishError instead of AssertionError --- openpype/plugins/publish/collect_anatomy_object.py | 8 ++++---- .../plugins/publish/collect_avalon_entities.py | 9 +++++---- openpype/plugins/publish/collect_rendered_files.py | 14 +++++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index 8128221925..725cae2b14 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -8,7 +8,7 @@ Provides: """ import pyblish.api -from openpype.pipeline import Anatomy +from openpype.pipeline import Anatomy, KnownPublishError class CollectAnatomyObject(pyblish.api.ContextPlugin): @@ -23,10 +23,10 @@ class CollectAnatomyObject(pyblish.api.ContextPlugin): def process(self, context): project_name = context.data.get("projectName") if project_name is None: - raise AssertionError( - "Environment `AVALON_PROJECT` is not set." + raise KnownPublishError(( + "Project name is not set in 'projectName'." "Could not initialize project's Anatomy." - ) + )) context.data["anatomy"] = Anatomy(project_name) diff --git a/openpype/plugins/publish/collect_avalon_entities.py b/openpype/plugins/publish/collect_avalon_entities.py index 0a7afc086f..3b05b6ae98 100644 --- a/openpype/plugins/publish/collect_avalon_entities.py +++ b/openpype/plugins/publish/collect_avalon_entities.py @@ -13,7 +13,7 @@ Provides: import pyblish.api from openpype.client import get_project, get_asset_by_name -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, KnownPublishError class CollectAvalonEntities(pyblish.api.ContextPlugin): @@ -29,9 +29,10 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): task_name = legacy_io.Session["AVALON_TASK"] project_entity = get_project(project_name) - assert project_entity, ( - "Project '{0}' was not found." - ).format(project_name) + if not project_entity: + raise KnownPublishError( + "Project '{0}' was not found.".format(project_name) + ) self.log.debug("Collected Project \"{}\"".format(project_entity)) context.data["projectEntity"] = project_entity diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 8c5d591148..8f8d0a5eeb 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -12,7 +12,7 @@ import json import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, KnownPublishError class CollectRenderedFiles(pyblish.api.ContextPlugin): @@ -20,6 +20,10 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): This collector will try to find json files in provided `OPENPYPE_PUBLISH_DATA`. Those files _MUST_ share same context. + Note: + We should split this collector and move the part which handle reading + of file and it's context from session data before collect anatomy + and instance creation dependent on anatomy can be done here. """ order = pyblish.api.CollectorOrder - 0.2 @@ -119,8 +123,12 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): def process(self, context): self._context = context - assert os.environ.get("OPENPYPE_PUBLISH_DATA"), ( - "Missing `OPENPYPE_PUBLISH_DATA`") + if not os.environ.get("OPENPYPE_PUBLISH_DATA"): + raise KnownPublishError("Missing `OPENPYPE_PUBLISH_DATA`") + + # QUESTION + # Do we support (or want support) multiple files in the variable? + # - what if they have different context? paths = os.environ["OPENPYPE_PUBLISH_DATA"].split(os.pathsep) # Using already collected Anatomy From d2e1fe84456feda9c3a8432d665715c5408c2d57 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 22:16:06 +0200 Subject: [PATCH 181/432] nuke: fixing local rendering slate workflow --- .../hosts/nuke/plugins/publish/extract_render_local.py | 8 -------- .../hosts/nuke/plugins/publish/extract_slate_frame.py | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 1595fe03fb..1b3bf46b71 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -31,10 +31,6 @@ class NukeRenderLocal(openpype.api.Extractor): first_frame = instance.data.get("frameStartHandle", None) - # exception for slate workflow - if "slate" in families: - first_frame -= 1 - last_frame = instance.data.get("frameEndHandle", None) node_subset_name = instance.data.get("name", None) @@ -68,10 +64,6 @@ class NukeRenderLocal(openpype.api.Extractor): int(last_frame) ) - # exception for slate workflow - if "slate" in families: - first_frame += 1 - ext = node["file_type"].value() if "representations" not in instance.data: diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 99ade4cf9b..ccfaf0ed46 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -13,6 +13,7 @@ from openpype.hosts.nuke.api import ( get_view_process_node ) + class ExtractSlateFrame(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts From 3755c5bf05d352de26647f05b5c2940d4022c30f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 10:45:35 +0200 Subject: [PATCH 182/432] implemented helper method to get representation path --- .../publish/integrate_ftrack_instances.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index c8d9e4117d..09a8672d77 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -360,6 +360,30 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): )) instance.data["ftrackComponentsList"] = component_list + def _get_repre_path(self, instance, repre, only_published): + published_path = repre.get("published_path") + if published_path: + published_path = os.path.normpath(published_path) + if os.path.exists(published_path): + return published_path + + if only_published: + return None + + comp_files = repre["files"] + if isinstance(comp_files, (tuple, list, set)): + filename = comp_files[0] + else: + filename = comp_files + + staging_dir = repre.get("stagingDir") + if not staging_dir: + staging_dir = instance.data["stagingDir"] + src_path = os.path.normpath(os.path.join(staging_dir, filename)) + if os.path.exists(src_path): + return src_path + return None + def _get_asset_version_status_name(self, instance): if not self.asset_versions_status_profiles: return None From 0474456e77c038af1cd1905e4a586cc8a6e27aae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 10:53:06 +0200 Subject: [PATCH 183/432] use helper method to calculate representation path for integration --- .../publish/integrate_ftrack_instances.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 09a8672d77..f1a4f28fd1 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -58,7 +58,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): version_number = int(instance_version) family = instance.data["family"] - family_low = instance.data["family"].lower() + family_low = family.lower() asset_type = instance.data.get("ftrackFamily") if not asset_type and family_low in self.family_mapping: @@ -140,24 +140,16 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component = None first_thumbnail_component_repre = None for repre in thumbnail_representations: - published_path = repre.get("published_path") - if not published_path: - comp_files = repre["files"] - if isinstance(comp_files, (tuple, list, set)): - filename = comp_files[0] - else: - filename = comp_files - - published_path = os.path.join( - repre["stagingDir"], filename + repre_path = self._get_repre_path(instance, repre, False) + if not repre_path: + self.log.warning( + "Published path is not set and source was removed." ) - if not os.path.exists(published_path): - continue - repre["published_path"] = published_path + continue # Create copy of base comp item and append it thumbnail_item = copy.deepcopy(base_component_item) - thumbnail_item["component_path"] = repre["published_path"] + thumbnail_item["component_path"] = repre_path thumbnail_item["component_data"] = { "name": "thumbnail" } @@ -216,6 +208,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): extended_asset_name = "" multiple_reviewable = len(review_representations) > 1 for repre in review_representations: + repre_path = self._get_repre_path(instance, repre, False) + if not repre_path: + self.log.warning( + "Published path is not set and source was removed." + ) + continue + # Create copy of base comp item and append it review_item = copy.deepcopy(base_component_item) @@ -270,7 +269,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): fps = instance_fps # Change location - review_item["component_path"] = repre["published_path"] + review_item["component_path"] = repre_path # Change component data review_item["component_data"] = { # Default component name is "main". @@ -327,7 +326,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add others representations as component for repre in other_representations: - published_path = repre.get("published_path") + published_path = self._get_repre_path(instance, repre, True) if not published_path: continue # Create copy of base comp item and append it @@ -368,7 +367,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): return published_path if only_published: - return None + return published_path comp_files = repre["files"] if isinstance(comp_files, (tuple, list, set)): From 266bce0f48070310f8b44ebfc25ab8b83ba51698 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 10:53:13 +0200 Subject: [PATCH 184/432] reduce duplicated variables --- .../modules/ftrack/plugins/publish/integrate_ftrack_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index c4f7b1f05d..58591bacfd 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -26,8 +26,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): families = ["ftrack"] def process(self, instance): - session = instance.context.data["ftrackSession"] - context = instance.context component_list = instance.data.get("ftrackComponentsList") if not component_list: self.log.info( @@ -36,8 +34,8 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): ) return - session = instance.context.data["ftrackSession"] context = instance.context + session = context.data["ftrackSession"] parent_entity = None default_asset_name = None From 8e5a2a082ee18b46f3223c6212fdd65510dd2bee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 10:57:23 +0200 Subject: [PATCH 185/432] added docstring to ftrack get repre path method --- .../publish/integrate_ftrack_instances.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index f1a4f28fd1..8eb8479183 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -360,6 +360,26 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): instance.data["ftrackComponentsList"] = component_list def _get_repre_path(self, instance, repre, only_published): + """Get representation path that can be used for integration. + + When 'only_published' is set to true the validation of path is not + relevant. In that case we just need what is set in 'published_path' + as "reference". The reference is not used to get or upload the file but + for reference where the file was published. + + Args: + instance (pyblish.Instance): Processed instance object. Used + for source of staging dir if representation does not have + filled it. + repre (dict): Representation on instance which could be and + could not be integrated with main integrator. + only_published (bool): Care only about published paths and + ignore if filepath is not existing anymore. + + Returns: + str: Path to representation file. + None: Path is not filled or does not exists. + """ published_path = repre.get("published_path") if published_path: published_path = os.path.normpath(published_path) From fcf6e70107cf609c9a561ec2821455100b9faa9e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:03:24 +0200 Subject: [PATCH 186/432] add missing empty line --- .../modules/ftrack/plugins/publish/integrate_ftrack_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 8eb8479183..d937e64790 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -380,6 +380,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): str: Path to representation file. None: Path is not filled or does not exists. """ + published_path = repre.get("published_path") if published_path: published_path = os.path.normpath(published_path) From 2657ff27f186bdcf8098f8f7878947fc36bec1f5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 13:32:00 +0300 Subject: [PATCH 187/432] Replace deprecated functions --- openpype/hosts/maya/api/lib_rendersettings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 38f493a4a8..6f41a5d169 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -7,11 +7,11 @@ import sys from openpype.api import ( get_project_settings, - get_asset) + ) from openpype.pipeline import legacy_io from openpype.pipeline import CreatorError - +from openpype.pipeline.context_tools import get_current_project_asset class RenderSettings(object): @@ -66,7 +66,7 @@ class RenderSettings(object): renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() - asset_doc = get_asset() + asset_doc = get_current_project_asset() # project_settings/maya/create/CreateRender/aov_separator try: aov_separator = self._aov_chars[( From c7bf29d17cdb1c5ceea21dc3e104427290cf71a3 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 13:33:17 +0300 Subject: [PATCH 188/432] Style fixes --- openpype/hosts/maya/api/lib_rendersettings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 6f41a5d169..0668c242f0 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -7,12 +7,13 @@ import sys from openpype.api import ( get_project_settings, - ) +) from openpype.pipeline import legacy_io from openpype.pipeline import CreatorError from openpype.pipeline.context_tools import get_current_project_asset + class RenderSettings(object): _image_prefix_nodes = { From a39eef07f4a91fe775d8c492fc0dbbf9502f4c2f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 13:52:52 +0300 Subject: [PATCH 189/432] Fix frame range reset. --- openpype/hosts/maya/api/lib_rendersettings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 0668c242f0..ee61f954e0 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -7,11 +7,13 @@ import sys from openpype.api import ( get_project_settings, + ) from openpype.pipeline import legacy_io from openpype.pipeline import CreatorError from openpype.pipeline.context_tools import get_current_project_asset +from openpype.hosts.maya.api.commands import reset_frame_range class RenderSettings(object): @@ -152,6 +154,7 @@ class RenderSettings(object): cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa elif (cmds.setAttr(str(attribute), type=True)) == "string": cmds.setAttr(str(attribute), str(value), type = "string") # noqa + reset_frame_range() def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" From 6e77634c67f39ce22d06068bc5110c2cae46686f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 14:05:57 +0300 Subject: [PATCH 190/432] Fix attribute type check bug. --- openpype/hosts/maya/api/lib_rendersettings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index ee61f954e0..c3bccf0add 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -148,11 +148,11 @@ class RenderSettings(object): # command accordingly. for item in additional_options: attribute, value = item - if (cmds.setAttr(str(attribute), type=True)) == "long": + if (cmds.getAttr(str(attribute), type=True)) == "long": cmds.setAttr(str(attribute), int(value)) - elif (cmds.setAttr(str(attribute), type=True)) == "bool": + elif (cmds.getAttr(str(attribute), type=True)) == "bool": cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa - elif (cmds.setAttr(str(attribute), type=True)) == "string": + elif (cmds.getAttr(str(attribute), type=True)) == "string": cmds.setAttr(str(attribute), str(value), type = "string") # noqa reset_frame_range() From 5e9799ee1649c5686fd2987b54331c6b1ea14b57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Jul 2022 20:04:08 +0800 Subject: [PATCH 191/432] Enable write color sets on animation publish automatically --- openpype/hosts/maya/plugins/create/create_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 5cd1f7090a..ef6608054d 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -22,7 +22,7 @@ class CreateAnimation(plugin.Creator): self.data[key] = value # Write vertex colors with the geometry. - self.data["writeColorSets"] = False + self.data["writeColorSets"] = True self.data["writeFaceSets"] = False # Include only renderable visible shapes. From 9377d20be1f10c41f49e303062485d7a8f6af85d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 16:12:12 +0200 Subject: [PATCH 192/432] implemented functions to extract template data --- openpype/pipeline/template_data.py | 226 +++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 openpype/pipeline/template_data.py diff --git a/openpype/pipeline/template_data.py b/openpype/pipeline/template_data.py new file mode 100644 index 0000000000..de46650f9d --- /dev/null +++ b/openpype/pipeline/template_data.py @@ -0,0 +1,226 @@ +from openpype.client import get_project, get_asset_by_name +from openpype.settings import get_system_settings +from openpype.lib.local_settings import get_openpype_username + + +def get_general_template_data(system_settings=None): + """General template data based on system settings or machine. + + Output contains formatting keys: + - 'studio[name]' - Studio name filled from system settings + - 'studio[code]' - Studio code filled from system settings + - 'user' - User's name using 'get_openpype_username' + + Args: + system_settings (Dict[str, Any]): System settings. + """ + + if not system_settings: + system_settings = get_system_settings() + studio_name = system_settings["general"]["studio_name"] + studio_code = system_settings["general"]["studio_code"] + return { + "studio": { + "name": studio_name, + "code": studio_code + }, + "user": get_openpype_username() + } + + +def get_project_template_data(project_doc): + """Extract data from project document that are used in templates. + + Project document must have 'name' and (at this moment) optional + key 'data.code'. + + Output contains formatting keys: + - 'project[name]' - Project name + - 'project[code]' - Project code + + Args: + project_doc (Dict[str, Any]): Queried project document. + + Returns: + Dict[str, Dict[str, str]]: Template data based on project document. + """ + + project_code = project_doc.get("data", {}).get("code") + return { + "project": { + "name": project_doc["name"], + "code": project_code + } + } + + +def get_asset_template_data(asset_doc, project_name): + """Extract data from asset document that are used in templates. + + Output dictionary contains keys: + - 'asset' - asset name + - 'hierarchy' - parent asset names joined with '/' + - 'parent' - direct parent name, project name used if is under project + + Required document fields: + Asset: 'name', 'data.parents' + + Args: + asset_doc (Dict[str, Any]): Queried asset document. + project_name (str): Is used for 'parent' key if asset doc does not have + any. + + Returns: + Dict[str, str]: Data that are based on asset document and can be used + in templates. + """ + + asset_parents = asset_doc["data"]["parents"] + hierarchy = "/".join(asset_parents) + if asset_parents: + parent_name = asset_parents[-1] + else: + parent_name = project_name + + return { + "asset": asset_doc["name"], + "hierarchy": hierarchy, + "parent": parent_name + } + + +def get_task_type(asset_doc, task_name): + """Get task type based on asset document and task name. + + Required document fields: + Asset: 'data.tasks' + + Args: + asset_doc (Dict[str, Any]): Queried asset document. + task_name (str): Task name which is under asset. + + Returns: + str: Task type name. + None: Task was not found on asset document. + """ + + asset_tasks_info = asset_doc["data"]["tasks"] + return asset_tasks_info.get(task_name, {}).get("type") + + +def get_task_template_data(project_doc, asset_doc, task_name): + """"Extract task specific data from project and asset documents. + + Required document fields: + Project: 'config.tasks' + Asset: 'data.tasks'. + + Args: + project_doc (Dict[str, Any]): Queried project document. + asset_doc (Dict[str, Any]): Queried asset document. + tas_name (str): Name of task for which data should be returned. + + Returns: + Dict[str, Dict[str, str]]: Template data + """ + + project_task_types = project_doc["config"]["tasks"] + task_type = get_task_type(asset_doc, task_name) + task_code = project_task_types.get(task_type, {}).get("short_name") + + return { + "task": { + "name": task_name, + "type": task_type, + "short": task_code, + } + } + + +def get_template_data( + project_doc, + asset_doc=None, + task_name=None, + host_name=None, + system_settings=None +): + """Prepare data for templates filling from entered documents and info. + + This function does not "auto fill" any values except system settings and + it's on purpose. + + Universal function to receive template data from passed arguments. Only + required argument is project document all other arguments are optional + and their values won't be added to template data if are not passed. + + Required document fields: + Project: 'name', 'data.code', 'config.tasks' + Asset: 'name', 'data.parents', 'data.tasks' + + Args: + project_doc (Dict[str, Any]): Mongo document of project from MongoDB. + asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB. + task_name (Union[str, None]): Task name under passed asset. + host_name (Union[str, None]): Used to fill '{app}' key. + system_settings (Union[Dict, None]): Prepared system settings. + They're queried if not passed (may be slower). + + Returns: + Dict[str, Any]: Data prepared for filling workdir template. + """ + + template_data = get_general_template_data(system_settings) + template_data.update(get_project_template_data(project_doc)) + if asset_doc: + template_data.update(get_asset_template_data( + asset_doc, project_doc["name"] + )) + if task_name: + template_data.update(get_task_template_data( + project_doc, asset_doc, task_name + )) + + if host_name: + template_data["app"] = host_name + + return template_data + + +def get_template_data_with_names( + project_name, + asset_name=None, + task_name=None, + host_name=None, + system_settings=None +): + """Prepare data for templates filling from entered entity names and info. + + Copy of 'get_template_data' but based on entity names instead of documents. + Only difference is that documents are queried. + + Args: + project_name (str): Project name for which template data are + calculated. + asset_name (Union[str, None]): Asset name for which template data are + calculated. + task_name (Union[str, None]): Task name under passed asset. + host_name (Union[str, None]):Used to fill '{app}' key. + because workdir template may contain `{app}` key. + system_settings (Union[Dict, None]): Prepared system settings. + They're queried if not passed. + + Returns: + Dict[str, Any]: Data prepared for filling workdir template. + """ + + project_doc = get_project(project_name, fields=["name", "data.code"]) + asset_doc = None + if asset_name: + asset_doc = get_asset_by_name( + project_name, + asset_name, + fields=["name", "data.parents", "data.tasks"] + ) + return get_template_data( + project_doc, asset_doc, task_name, host_name, system_settings + ) From a26fd8394c71f0f01552f20987ac6618747d1572 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 17:32:26 +0300 Subject: [PATCH 193/432] Propagate render settings key to grey out apply button. --- openpype/hosts/maya/api/menu.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c3ce8b0227..7d2d0dc3f5 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,7 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import BuildWorkfile, get_current_project_settings from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -98,12 +98,18 @@ def install(): ) cmds.menuItem(divider=True) - - cmds.menuItem( - "Set Render Settings", - command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings() # noqa - ) - + # project_settings/maya/RenderSettings/apply_render_settings + render_settings_flag = get_current_project_settings()["maya"]["RenderSettings"]["apply_render_settings"] # noqa + if render_settings_flag: + cmds.menuItem( + "Set Render Settings", + command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings(), # noqa + enable=True) + else: + cmds.menuItem( + "Set Render Settings", + command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings(), # noqa + enable=False) cmds.menuItem(divider=True) cmds.menuItem( From 58309c3d3b970ea5f55a08e6b1b1c092b3d6413a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 17:38:58 +0300 Subject: [PATCH 194/432] Remove Mental Ray related code. --- openpype/hosts/maya/api/lib_rendersettings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index c3bccf0add..768f9156c3 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -19,7 +19,6 @@ from openpype.hosts.maya.api.commands import reset_frame_range class RenderSettings(object): _image_prefix_nodes = { - 'mentalray': 'defaultRenderGlobals.imageFilePrefix', 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'defaultRenderGlobals.imageFilePrefix', @@ -27,7 +26,6 @@ class RenderSettings(object): } _image_prefixes = { - 'mentalray': 'maya///{aov_separator}', # noqa 'vray': 'maya///', 'arnold': 'maya///{aov_separator}', # noqa 'renderman': 'maya///{aov_separator}', From 2a3255a9cb6a5eed64c906cd28cfdb2e6679d83b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 16:40:35 +0200 Subject: [PATCH 195/432] added function which calculate template data based on context session --- openpype/pipeline/context_tools.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index a8e55479b6..0535ce5d54 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -19,7 +19,9 @@ from openpype.client import ( from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import filter_pyblish_plugins + from .anatomy import Anatomy +from .template_data import get_template_data_with_names from . import ( legacy_io, register_loader_plugin_path, @@ -336,6 +338,7 @@ def get_current_project_asset(asset_name=None, asset_id=None, fields=None): return None return get_asset_by_name(project_name, asset_name, fields=fields) + def is_representation_from_latest(representation): """Return whether the representation is from latest version @@ -348,3 +351,29 @@ def is_representation_from_latest(representation): project_name = legacy_io.active_project() return version_is_latest(project_name, representation["parent"]) + + +def get_template_data_from_session(session=None, system_settings=None): + """Template data for template fill from session keys. + + Args: + session (Union[Dict[str, str], None]): The Session to use. If not + provided use the currently active global Session. + system_settings (Union[Dict[str, Any], Any]): Prepared system settings. + Optional are auto received if not passed. + + Returns: + Dict[str, Any]: All available data from session. + """ + + if session is None: + session = legacy_io.Session + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] + host_name = session["AVALON_APP"] + + return get_template_data_with_names( + project_name, asset_name, task_name, host_name, system_settings + ) From 5c6b47e503b78e841a173575f222b89d49b5c1f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 16:47:11 +0200 Subject: [PATCH 196/432] mark functions in lib as deprecated and re-use functions from openpype.pipeline --- openpype/lib/avalon_context.py | 80 +++++++++------------------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 4076a91c36..73014f5a5d 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -21,14 +21,10 @@ from openpype.client import ( get_representations, get_workfile_info, ) -from openpype.settings import ( - get_project_settings, - get_system_settings -) +from openpype.settings import get_project_settings from .profiles_filtering import filter_profiles from .events import emit_event from .path_templates import StringTemplate -from .local_settings import get_openpype_username legacy_io = None @@ -222,17 +218,11 @@ def get_asset(asset_name=None): return get_current_project_asset(asset_name=asset_name) +@deprecated("openpype.pipeline.template_data.get_general_template_data") def get_system_general_anatomy_data(system_settings=None): - if not system_settings: - system_settings = get_system_settings() - studio_name = system_settings["general"]["studio_name"] - studio_code = system_settings["general"]["studio_code"] - return { - "studio": { - "name": studio_name, - "code": studio_code - } - } + from openpype.pipeline.template_data import get_general_template_data + + return get_general_template_data(system_settings) def get_linked_asset_ids(asset_doc): @@ -424,7 +414,7 @@ def get_workfile_template_key( return default -# TODO rename function as is not just "work" specific +@deprecated("openpype.pipeline.template_data.get_template_data") def get_workdir_data(project_doc, asset_doc, task_name, host_name): """Prepare data for workdir template filling from entered information. @@ -437,40 +427,14 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): Returns: dict: Data prepared for filling workdir template. + """ - task_type = asset_doc['data']['tasks'].get(task_name, {}).get('type') - project_task_types = project_doc["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") + from openpype.pipeline.template_data import get_template_data - asset_parents = asset_doc["data"]["parents"] - hierarchy = "/".join(asset_parents) - - parent_name = project_doc["name"] - if asset_parents: - parent_name = asset_parents[-1] - - data = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "task": { - "name": task_name, - "type": task_type, - "short": task_code, - }, - "asset": asset_doc["name"], - "parent": parent_name, - "app": host_name, - "user": get_openpype_username(), - "hierarchy": hierarchy, - } - - system_general_data = get_system_general_anatomy_data() - data.update(system_general_data) - - return data + return get_template_data( + project_doc, asset_doc, task_name, host_name + ) def get_workdir_with_workdir_data( @@ -565,27 +529,21 @@ def get_workdir( ) -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.get_template_data_from_session") def template_data_from_session(session=None): """ Return dictionary with template from session keys. Args: session (dict, Optional): The Session to use. If not provided use the currently active global Session. + Returns: dict: All available data from session. + """ - if session is None: - session = legacy_io.Session - - project_name = session["AVALON_PROJECT"] - asset_name = session["AVALON_ASSET"] - task_name = session["AVALON_TASK"] - host_name = session["AVALON_APP"] - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) - return get_workdir_data(project_doc, asset_doc, task_name, host_name) + from openpype.pipeline.context_tools import get_template_data_from_session + return get_template_data_from_session(session) @with_pipeline_io @@ -660,13 +618,14 @@ def compute_session_changes( @with_pipeline_io def get_workdir_from_session(session=None, template_key=None): from openpype.pipeline import Anatomy + from openpype.pipeline.context_tools import get_template_data_from_session if session is None: session = legacy_io.Session project_name = session["AVALON_PROJECT"] host_name = session["AVALON_APP"] anatomy = Anatomy(project_name) - template_data = template_data_from_session(session) + template_data = get_template_data_from_session(session) anatomy_filled = anatomy.format(template_data) if not template_key: @@ -695,8 +654,8 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): Returns: dict: The changed key, values in the current Session. - """ + changes = compute_session_changes( legacy_io.Session, task=task, @@ -768,6 +727,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and `legacy_io` is used if not entered. """ + from openpype.pipeline import Anatomy # Use legacy_io if dbcon is not entered From 54bb85b2043bab1b9b1a0b5d8236d2c694c9a66f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 17:47:58 +0300 Subject: [PATCH 197/432] Remove unnecessary comment. --- openpype/hosts/maya/api/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 7d2d0dc3f5..ed546ba7a8 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -98,7 +98,7 @@ def install(): ) cmds.menuItem(divider=True) - # project_settings/maya/RenderSettings/apply_render_settings + render_settings_flag = get_current_project_settings()["maya"]["RenderSettings"]["apply_render_settings"] # noqa if render_settings_flag: cmds.menuItem( From f120f22c71ce2590e191fcf58b4be9967b17f15c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 16:48:13 +0200 Subject: [PATCH 198/432] Added information about removement to docstrings of deprecated functions --- openpype/lib/avalon_context.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 73014f5a5d..521d1e05e1 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -184,6 +184,9 @@ def is_latest(representation): Returns: bool: Whether the representation is of latest version. + + Deprecated: + Function will be removed after release version 3.14.* """ from openpype.pipeline.context_tools import is_representation_from_latest @@ -193,7 +196,11 @@ def is_latest(representation): @deprecated("openpype.pipeline.load.any_outdated_containers") def any_outdated(): - """Return whether the current scene has any outdated content""" + """Return whether the current scene has any outdated content. + + Deprecated: + Function will be removed after release version 3.14.* + """ from openpype.pipeline.load import any_outdated_containers @@ -211,6 +218,9 @@ def get_asset(asset_name=None): Returns: (MongoDB document) + + Deprecated: + Function will be removed after release version 3.14.* """ from openpype.pipeline.context_tools import get_current_project_asset @@ -220,6 +230,10 @@ def get_asset(asset_name=None): @deprecated("openpype.pipeline.template_data.get_general_template_data") def get_system_general_anatomy_data(system_settings=None): + """ + Deprecated: + Function will be removed after release version 3.14.* + """ from openpype.pipeline.template_data import get_general_template_data return get_general_template_data(system_settings) @@ -287,7 +301,10 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): Returns: None: If asset, subset or version were not found. - dict: Last version document for entered . + dict: Last version document for entered. + + Deprecated: + Function will be removed after release version 3.14.* """ if not project_name: @@ -428,6 +445,8 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): Returns: dict: Data prepared for filling workdir template. + Deprecated: + Function will be removed after release version 3.14.* """ from openpype.pipeline.template_data import get_template_data @@ -540,6 +559,8 @@ def template_data_from_session(session=None): Returns: dict: All available data from session. + Deprecated: + Function will be removed after release version 3.14.* """ from openpype.pipeline.context_tools import get_template_data_from_session From 3561454a5f83129629929f3c9b6d937654d3e787 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 16:48:41 +0200 Subject: [PATCH 199/432] removed unused imports --- openpype/lib/avalon_context.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 521d1e05e1..95c547ce34 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -13,10 +13,8 @@ from openpype.client import ( get_project, get_assets, get_asset_by_name, - get_subset_by_name, get_subsets, get_last_versions, - get_last_version_by_subset_id, get_last_version_by_subset_name, get_representations, get_workfile_info, From 8d7b9af7a52209fc706838abc83109724d5e4741 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 18:00:13 +0300 Subject: [PATCH 200/432] Grab image prefixes from settings. --- openpype/hosts/maya/api/lib_rendersettings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 768f9156c3..e5acdc2139 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -7,7 +7,7 @@ import sys from openpype.api import ( get_project_settings, - + get_current_project_settings ) from openpype.pipeline import legacy_io @@ -26,10 +26,10 @@ class RenderSettings(object): } _image_prefixes = { - 'vray': 'maya///', - 'arnold': 'maya///{aov_separator}', # noqa + 'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa + 'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa 'renderman': 'maya///{aov_separator}', - 'redshift': 'maya///{aov_separator}' # noqa + 'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa } _aov_chars = { From 9f9ac018bdc076f16fd7940b387445674f192277 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 17:23:12 +0200 Subject: [PATCH 201/432] use new functions instead of 'get_workdir_data' --- openpype/hosts/nuke/api/lib.py | 9 ++++---- .../tvpaint/plugins/load/load_workfile.py | 10 ++++----- .../unreal/hooks/pre_workfile_preparation.py | 13 ++++------- openpype/lib/applications.py | 10 ++++++--- openpype/lib/avalon_context.py | 9 +++++--- .../action_fill_workfile_attr.py | 13 +++++++---- openpype/tools/workfiles/save_as_dialog.py | 22 +++++-------------- 7 files changed, 39 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 74db164ae5..87647e214e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -23,7 +23,6 @@ from openpype.api import ( Logger, BuildWorkfile, get_version_from_path, - get_workdir_data, get_current_project_settings, ) from openpype.tools.utils import host_tools @@ -34,6 +33,7 @@ from openpype.settings import ( get_anatomy_settings, ) from openpype.modules import ModulesManager +from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( discover_legacy_creator_plugins, legacy_io, @@ -965,12 +965,11 @@ def format_anatomy(data): data["version"] = get_version_from_path(file) project_name = anatomy.project_name - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, data["avalon"]["asset"]) + asset_name = data["avalon"]["asset"] task_name = os.environ["AVALON_TASK"] host_name = os.environ["AVALON_APP"] - context_data = get_workdir_data( - project_doc, asset_doc, task_name, host_name + context_data = get_template_data_with_names( + project_name, asset_name, task_name, host_name ) data.update(context_data) data.update({ diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index c6dc765a27..8b09d20755 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -1,10 +1,8 @@ import os -from openpype.client import get_project, get_asset_by_name from openpype.lib import ( StringTemplate, get_workfile_template_key_from_context, - get_workdir_data, get_last_workfile_with_version, ) from openpype.pipeline import ( @@ -12,6 +10,7 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) +from openpype.pipeline.template_data import get_template_data_with_names from openpype.hosts.tvpaint.api import lib, pipeline, plugin @@ -54,9 +53,6 @@ class LoadWorkfile(plugin.Loader): asset_name = legacy_io.Session["AVALON_ASSET"] task_name = legacy_io.Session["AVALON_TASK"] - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) - template_key = get_workfile_template_key_from_context( asset_name, task_name, @@ -66,7 +62,9 @@ class LoadWorkfile(plugin.Loader): ) anatomy = Anatomy(project_name) - data = get_workdir_data(project_doc, asset_doc, task_name, host_name) + data = get_template_data_with_names( + project_name, asset_name, task_name, host_name + ) data["root"] = anatomy.roots file_template = anatomy.templates[template_key]["file"] diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 5be04fc841..50b34bd573 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """Hook to launch Unreal and prepare projects.""" import os +import copy from pathlib import Path from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, ApplicationNotFound, - get_workdir_data, get_workfile_template_key ) import openpype.hosts.unreal.lib as unreal_lib @@ -35,18 +35,13 @@ class UnrealPrelaunchHook(PreLaunchHook): return last_workfile.name # Prepare data for fill data and for getting workfile template key - task_name = self.data["task_name"] anatomy = self.data["anatomy"] - asset_doc = self.data["asset_doc"] project_doc = self.data["project_doc"] - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") + # Use already prepared workdir data + workdir_data = copy.deepcopy(self.data["workdir_data"]) + task_type = workdir_data.get("task", {}).get("type") - workdir_data = get_workdir_data( - project_doc, asset_doc, task_name, self.host_name - ) # QUESTION raise exception if version is part of filename template? workdir_data["version"] = 1 workdir_data["ext"] = "uproject" diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index f46197e15f..da8623ea13 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -28,7 +28,6 @@ from . import PypeLogger from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username from .avalon_context import ( - get_workdir_data, get_workdir_with_workdir_data, get_workfile_template_key, get_last_workfile @@ -1576,6 +1575,9 @@ def prepare_context_environments(data, env_group=None): data (EnvironmentPrepData): Dictionary where result and intermediate result will be stored. """ + + from openpype.pipeline.template_data import get_template_data + # Context environments log = data["log"] @@ -1596,7 +1598,9 @@ def prepare_context_environments(data, env_group=None): # Load project specific environments project_name = project_doc["name"] project_settings = get_project_settings(project_name) + system_settings = get_system_settings() data["project_settings"] = project_settings + data["system_settings"] = system_settings # Apply project specific environments on current env value apply_project_environments_value( project_name, data["env"], project_settings, env_group @@ -1619,8 +1623,8 @@ def prepare_context_environments(data, env_group=None): if not app.is_host: return - workdir_data = get_workdir_data( - project_doc, asset_doc, task_name, app.host_name + workdir_data = get_template_data( + project_doc, asset_doc, task_name, app.host_name, system_settings ) data["workdir_data"] = workdir_data diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 95c547ce34..42854f39d6 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -533,11 +533,13 @@ def get_workdir( TemplateResult: Workdir path. """ + from openpype.pipeline import Anatomy + from openpype.pipeline.template_data import get_template_data + if not anatomy: - from openpype.pipeline import Anatomy anatomy = Anatomy(project_doc["name"]) - workdir_data = get_workdir_data( + workdir_data = get_template_data( project_doc, asset_doc, task_name, host_name ) # Output is TemplateResult object which contain useful data @@ -748,6 +750,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): """ from openpype.pipeline import Anatomy + from openpype.pipeline.template_data import get_template_data # Use legacy_io if dbcon is not entered if not dbcon: @@ -766,7 +769,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): # Prepare project for workdir data project_name = dbcon.active_project() project_doc = get_project(project_name) - workdir_data = get_workdir_data( + workdir_data = get_template_data( project_doc, asset_doc, task_name, dbcon.Session["AVALON_APP"] ) # Prepare anatomy diff --git a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py index d91649d7ba..c7fa2dce5e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py +++ b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py @@ -11,13 +11,13 @@ from openpype.client import ( get_project, get_assets, ) -from openpype.settings import get_project_settings +from openpype.settings import get_project_settings, get_system_settings from openpype.lib import ( get_workfile_template_key, - get_workdir_data, StringTemplate, ) from openpype.pipeline import Anatomy +from openpype.pipeline.template_data import get_template_data from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype_modules.ftrack.lib.avalon_sync import create_chunks @@ -279,14 +279,19 @@ class FillWorkfileAttributeAction(BaseAction): extension = "{ext}" project_doc = get_project(project_name) project_settings = get_project_settings(project_name) + system_settings = get_system_settings() anatomy = Anatomy(project_name) templates_by_key = {} operations = [] for asset_doc, task_entities in asset_docs_with_task_entities: for task_entity in task_entities: - workfile_data = get_workdir_data( - project_doc, asset_doc, task_entity["name"], host_name + workfile_data = get_template_data( + project_doc, + asset_doc, + task_entity["name"], + host_name, + system_settings ) # Use version 1 for each workfile workfile_data["version"] = 1 diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index b62fd2c889..ea602846e7 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -5,18 +5,12 @@ import logging from Qt import QtWidgets, QtCore -from openpype.client import ( - get_project, - get_asset_by_name, -) -from openpype.lib import ( - get_last_workfile_with_version, - get_workdir_data, -) +from openpype.lib import get_last_workfile_with_version from openpype.pipeline import ( registered_host, legacy_io, ) +from openpype.pipeline.template_data import get_template_data_with_names from openpype.tools.utils import PlaceholderLineEdit log = logging.getLogger(__name__) @@ -30,16 +24,10 @@ def build_workfile_data(session): asset_name = session["AVALON_ASSET"] task_name = session["AVALON_TASK"] host_name = session["AVALON_APP"] - project_doc = get_project( - project_name, fields=["name", "data.code", "config.tasks"] - ) - asset_doc = get_asset_by_name( - project_name, - asset_name, - fields=["name", "data.tasks", "data.parents"] - ) - data = get_workdir_data(project_doc, asset_doc, task_name, host_name) + data = get_template_data_with_names( + project_name, asset_name, task_name, host_name + ) data.update({ "version": 1, "comment": "", From c44ec02d5e1ff3a370fa03d3057f53663f791e3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Jul 2022 23:36:17 +0800 Subject: [PATCH 202/432] update the setting which allows switching on/off write color sets in animation publish --- .../maya/plugins/create/create_animation.py | 3 +- .../defaults/project_settings/maya.json | 2 ++ .../schemas/schema_maya_create.json | 29 ++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index ef6608054d..b7f473acef 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -11,6 +11,7 @@ class CreateAnimation(plugin.Creator): label = "Animation" family = "animation" icon = "male" + write_color_sets = False def __init__(self, *args, **kwargs): super(CreateAnimation, self).__init__(*args, **kwargs) @@ -22,7 +23,7 @@ class CreateAnimation(plugin.Creator): self.data[key] = value # Write vertex colors with the geometry. - self.data["writeColorSets"] = True + self.data["writeColorSets"] = self.write_color_sets self.data["writeFaceSets"] = False # Include only renderable visible shapes. diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index c96acbff6d..70bedf55d8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -90,9 +90,11 @@ }, "CreateAnimation": { "enabled": true, + "write_color_sets": false, "defaults": [ "Main" ] + }, "CreateAss": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 09287a8b50..9000b0246f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -143,6 +143,31 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CreateAnimation", + "label": "Create Animation", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "write_color_sets", + "label": "Write Color Sets" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + }, { "type": "schema_template", "name": "template_create_plugin", @@ -159,10 +184,6 @@ "key": "CreateMultiverseUsdOver", "label": "Create Multiverse USD Override" }, - { - "key": "CreateAnimation", - "label": "Create Animation" - }, { "key": "CreateAss", "label": "Create Ass" From 8259be5a1ad3815e4a5eb3a39edf7c858dddff0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 17:36:45 +0200 Subject: [PATCH 203/432] simplified collect anatomy context data --- .../publish/collect_anatomy_context_data.py | 66 ++++++------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 0794adfb67..8433816908 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -15,10 +15,8 @@ Provides: import json import pyblish.api -from openpype.lib import ( - get_system_general_anatomy_data -) from openpype.pipeline import legacy_io +from openpype.pipeline.template_data import get_template_data class CollectAnatomyContextData(pyblish.api.ContextPlugin): @@ -33,11 +31,15 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "asset": "AssetName", "hierarchy": "path/to/asset", "task": "Working", + "user": "MeDespicable", + # Duplicated entry "username": "MeDespicable", + # Current host name + "app": "maya" + *** OPTIONAL *** - "app": "maya" # Current application base name - + mutliple keys from `datetimeData` # see it's collector + + mutliple keys from `datetimeData` (See it's collector) } """ @@ -45,52 +47,26 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): label = "Collect Anatomy Context Data" def process(self, context): + host_name = context.data["hostName"] + system_settings = context.data["system_settings"] project_entity = context.data["projectEntity"] - context_data = { - "project": { - "name": project_entity["name"], - "code": project_entity["data"].get("code") - }, - "username": context.data["user"], - "app": context.data["hostName"] - } - - context.data["anatomyData"] = context_data - - # add system general settings anatomy data - system_general_data = get_system_general_anatomy_data() - context_data.update(system_general_data) - - datetime_data = context.data.get("datetimeData") or {} - context_data.update(datetime_data) - asset_entity = context.data.get("assetEntity") + task_name = None if asset_entity: task_name = legacy_io.Session["AVALON_TASK"] - asset_tasks = asset_entity["data"]["tasks"] - task_type = asset_tasks.get(task_name, {}).get("type") + anatomy_data = get_template_data( + project_entity, asset_entity, task_name, host_name, system_settings + ) + anatomy_data.update(context.data.get("datetimeData") or {}) - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") + username = context.data["user"] + anatomy_data["user"] = username + # Backwards compatibility for 'username' key + anatomy_data["username"] = username - asset_parents = asset_entity["data"]["parents"] - hierarchy = "/".join(asset_parents) - - parent_name = project_entity["name"] - if asset_parents: - parent_name = asset_parents[-1] - - context_data.update({ - "asset": asset_entity["name"], - "parent": parent_name, - "hierarchy": hierarchy, - "task": { - "name": task_name, - "type": task_type, - "short": task_code, - } - }) + # Store + context.data["anatomyData"] = anatomy_data self.log.info("Global anatomy Data collected") - self.log.debug(json.dumps(context_data, indent=4)) + self.log.debug(json.dumps(anatomy_data, indent=4)) From 7aefc53d98fbc6509c5c90b4b86fd75d7a4344e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 18:23:58 +0200 Subject: [PATCH 204/432] removed unnecessary "app" key filling --- openpype/hosts/nuke/api/lib.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 87647e214e..501ab4ba93 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -910,19 +910,17 @@ def get_render_path(node): ''' Generate Render path from presets regarding avalon knob data ''' avalon_knob_data = read_avalon_data(node) - data = {'avalon': avalon_knob_data} nuke_imageio_writes = get_imageio_node_setting( node_class=avalon_knob_data["family"], plugin_name=avalon_knob_data["creator"], subset=avalon_knob_data["subset"] ) - host_name = os.environ.get("AVALON_APP") - data.update({ - "app": host_name, + data = { + "avalon": avalon_knob_data, "nuke_imageio_writes": nuke_imageio_writes - }) + } anatomy_filled = format_anatomy(data) return anatomy_filled["render"]["path"].replace("\\", "/") @@ -1127,10 +1125,8 @@ def create_write_node( if knob["name"] == "file_type": representation = knob["value"] - host_name = os.environ.get("AVALON_APP") try: data.update({ - "app": host_name, "imageio_writes": imageio_writes, "representation": representation, }) From a2c61b5233c4d20917c1c4594c6923738dc6b362 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Jul 2022 19:48:06 +0200 Subject: [PATCH 205/432] nuke: slate workflow switch to instance data --- openpype/hosts/nuke/plugins/publish/collect_slate_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py index 4257ed3131..bfe32d8fd1 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py +++ b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py @@ -33,6 +33,7 @@ class CollectSlate(pyblish.api.InstancePlugin): if slate_node: instance.data["slateNode"] = slate_node + instance.data["slate"] = True instance.data["families"].append("slate") instance.data["versionData"]["families"].append("slate") self.log.info( From 427c61f22c7b9bc68b1d6a64a238a4db762e7238 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Jul 2022 19:49:00 +0200 Subject: [PATCH 206/432] nuke: fixing farm and local rendering slate workflow --- .../nuke/plugins/publish/extract_render_local.py | 7 +++++-- .../nuke/plugins/publish/extract_slate_frame.py | 8 ++++++++ .../plugins/publish/submit_nuke_deadline.py | 15 +++++---------- .../plugins/publish/submit_publish_job.py | 8 ++++++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 1b3bf46b71..7cc9b2f928 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -80,8 +80,11 @@ class NukeRenderLocal(openpype.api.Extractor): repre = { 'name': ext, 'ext': ext, - 'frameStart': "%0{}d".format( - len(str(last_frame))) % first_frame, + 'frameStart': ( + "{{:0>{}}}" + .format(len(str(last_frame))) + .format(first_frame) + ), 'files': filenames, "stagingDir": out_dir } diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index ccfaf0ed46..b5cad143db 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -237,6 +237,7 @@ class ExtractSlateFrame(openpype.api.Extractor): def _render_slate_to_sequence(self, instance): # set slate frame first_frame = instance.data["frameStartHandle"] + last_frame = instance.data["frameEndHandle"] slate_first_frame = first_frame - 1 # render slate as sequence frame @@ -285,6 +286,13 @@ class ExtractSlateFrame(openpype.api.Extractor): matching_repre["files"] = [first_filename, slate_filename] elif slate_filename not in matching_repre["files"]: matching_repre["files"].insert(0, slate_filename) + matching_repre["frameStart"] = ( + "{{:0>{}}}" + .format(len(str(last_frame))) + .format(slate_first_frame) + ) + self.log.debug( + "__ matching_repre: {}".format(pformat(matching_repre))) self.log.warning("Added slate frame to representation files") diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 93fb511a34..a5f8270ec7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -80,10 +80,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "Using published scene for render {}".format(script_path) ) - # exception for slate workflow - if "slate" in instance.data["families"]: - submit_frame_start -= 1 - response = self.payload_submit( instance, script_path, @@ -99,10 +95,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): instance.data["publishJobState"] = "Suspended" if instance.data.get("bakingNukeScripts"): - # exception for slate workflow - if "slate" in instance.data["families"]: - submit_frame_start += 1 - for baking_script in instance.data["bakingNukeScripts"]: render_path = baking_script["bakeRenderPath"] script_path = baking_script["bakeScriptPath"] @@ -365,7 +357,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): if not instance.data.get("expectedFiles"): instance.data["expectedFiles"] = [] - dir = os.path.dirname(path) + dirname = os.path.dirname(path) file = os.path.basename(path) if "#" in file: @@ -377,9 +369,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): instance.data["expectedFiles"].append(path) return + if instance.data.get("slate"): + start_frame -= 1 + for i in range(start_frame, (end_frame + 1)): instance.data["expectedFiles"].append( - os.path.join(dir, (file % i)).replace("\\", "/")) + os.path.join(dirname, (file % i)).replace("\\", "/")) def get_limit_groups(self): """Search for limit group nodes and return group name. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 43ea64e565..f05ef31938 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -158,7 +158,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # mapping of instance properties to be transfered to new instance for every # specified family instance_transfer = { - "slate": ["slateFrames"], + "slate": ["slateFrames", "slate"], "review": ["lutPath"], "render2d": ["bakingNukeScripts", "version"], "renderlayer": ["convertToScanline"] @@ -585,11 +585,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): " This may cause issues on farm." ).format(staging)) + frame_start = int(instance.get("frameStartHandle")) + if instance.get("slate"): + frame_start -= 1 + rep = { "name": ext, "ext": ext, "files": [os.path.basename(f) for f in list(collection)], - "frameStart": int(instance.get("frameStartHandle")), + "frameStart": frame_start, "frameEnd": int(instance.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames "stagingDir": staging, From c53b7bba7784aff067cfa4cfdeffe35be146180c Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 21:09:54 +0300 Subject: [PATCH 207/432] Remove unnecessary unused function. --- openpype/hosts/maya/api/lib_rendersettings.py | 12 ------------ openpype/hosts/maya/plugins/create/create_render.py | 2 -- 2 files changed, 14 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index e5acdc2139..8c09175614 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -49,18 +49,6 @@ class RenderSettings(object): legacy_io.Session["AVALON_PROJECT"] ) - @staticmethod - def apply_defaults(renderer=None, project_settings=None): - if renderer is None: - renderer = cmds.getAttr( - 'defaultRenderGlobals.currentRenderer').lower() - # handle various renderman names - if renderer.startswith('renderman'): - renderer = 'renderman' - - render_settings = RenderSettings(project_settings) - render_settings.set_default_renderer_settings(renderer) - def set_default_renderer_settings(self, renderer=None): """Set basic settings based on renderer.""" if not renderer: diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index b73f550fa2..d4ad488b32 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -164,8 +164,6 @@ class CreateRender(plugin.Creator): collection = render_layer.createCollection("defaultCollection") collection.getSelector().setPattern('*') - self.log.info("Applying default render settings..") - lib_rendersettings.RenderSettings.apply_defaults() return self.instance def _deadline_webservice_changed(self): From 137ba908b51acf1a79963e71dc9278ec935f002a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Jul 2022 22:08:23 +0200 Subject: [PATCH 208/432] nuke: code style improvements --- .../plugins/publish/extract_render_local.py | 2 +- .../plugins/publish/precollect_instances.py | 17 ++++++++++------- .../nuke/plugins/publish/precollect_writes.py | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 1595fe03fb..7e66cdccda 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -123,4 +123,4 @@ class NukeRenderLocal(openpype.api.Extractor): self.log.info('Finished render') - self.log.debug("instance extracted: {}".format(instance.data)) + self.log.debug("_ instance.data: {}".format(instance.data)) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index b0da94c4ce..b396056eb9 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -50,7 +50,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # establish families family = avalon_knob_data["family"] families_ak = avalon_knob_data.get("families", []) - families = list() + families = [] # except disabled nodes but exclude backdrops in test if ("nukenodes" not in family) and (node["disable"].value()): @@ -111,10 +111,10 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): self.log.debug("__ families: `{}`".format(families)) # Get format - format = root['format'].value() - resolution_width = format.width() - resolution_height = format.height() - pixel_aspect = format.pixelAspect() + format_ = root['format'].value() + resolution_width = format_.width() + resolution_height = format_.height() + pixel_aspect = format_.pixelAspect() # get publish knob value if "publish" not in node.knobs(): @@ -125,8 +125,11 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): self.log.debug("__ _families_test: `{}`".format(_families_test)) for family_test in _families_test: if family_test in self.sync_workfile_version_on_families: - self.log.debug("Syncing version with workfile for '{}'" - .format(family_test)) + self.log.debug( + "Syncing version with workfile for '{}'".format( + family_test + ) + ) # get version to instance for integration instance.data['version'] = instance.context.data['version'] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index a97f34b370..e37cc8a80a 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -144,8 +144,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): self.log.debug("colorspace: `{}`".format(colorspace)) version_data = { - "families": [f.replace(".local", "").replace(".farm", "") - for f in _families_test if "write" not in f], + "families": [ + _f.replace(".local", "").replace(".farm", "") + for _f in _families_test if "write" != _f + ], "colorspace": colorspace } From 951cc995a52057e163f5cda99b492faf225adb40 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Jul 2022 22:09:06 +0200 Subject: [PATCH 209/432] nuke: fixing family after local render anatomyData family should be also changed --- openpype/hosts/nuke/plugins/publish/extract_render_local.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 7e66cdccda..6f0196690c 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -105,13 +105,16 @@ class NukeRenderLocal(openpype.api.Extractor): instance.data['family'] = 'render' families.remove('render.local') families.insert(0, "render2d") + instance.data["anatomyData"]["family"] = "render" elif "prerender.local" in families: instance.data['family'] = 'prerender' families.remove('prerender.local') families.insert(0, "prerender") + instance.data["anatomyData"]["family"] = "prerender" elif "still.local" in families: instance.data['family'] = 'image' families.remove('still.local') + instance.data["anatomyData"]["family"] = "image" instance.data["families"] = families collections, remainder = clique.assemble(filenames) From 361ba53f26d89e94758ff8f32e48444ba1715771 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:54:57 +0200 Subject: [PATCH 210/432] use new location of 'get_default_components' function --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index ace33ab92a..08e0849303 100644 --- a/start.py +++ b/start.py @@ -1113,7 +1113,7 @@ def boot(): def get_info(use_staging=None) -> list: """Print additional information to console.""" - from openpype.lib.mongo import get_default_components + from openpype.client.mongo import get_default_components from openpype.lib.log import PypeLogger components = get_default_components() From bfbb1225d0ed7a7acccf900e42bdccad60a05ced Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:57:19 +0200 Subject: [PATCH 211/432] Use 'Logger' instead of 'PypeLogger' --- start.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 08e0849303..e83589d160 100644 --- a/start.py +++ b/start.py @@ -1114,7 +1114,11 @@ def boot(): def get_info(use_staging=None) -> list: """Print additional information to console.""" from openpype.client.mongo import get_default_components - from openpype.lib.log import PypeLogger + try: + from openpype.lib.log import Logger + except ImportError: + # Backwards compatibility for 'PypeLogger' + from openpype.lib.log import PypeLogger as Logger components = get_default_components() @@ -1141,14 +1145,14 @@ def get_info(use_staging=None) -> list: os.environ.get("MUSTER_REST_URL"))) # Reinitialize - PypeLogger.initialize() + Logger.initialize() mongo_components = get_default_components() if mongo_components["host"]: inf.append(("Logging to MongoDB", mongo_components["host"])) inf.append((" - port", mongo_components["port"] or "")) - inf.append((" - database", PypeLogger.log_database_name)) - inf.append((" - collection", PypeLogger.log_collection_name)) + inf.append((" - database", Logger.log_database_name)) + inf.append((" - collection", Logger.log_collection_name)) inf.append((" - user", mongo_components["username"] or "")) if mongo_components["auth_db"]: inf.append((" - auth source", mongo_components["auth_db"])) From f7cb4cd83a4fc107b2960903ee8b87fc28c0052c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 11:01:54 +0200 Subject: [PATCH 212/432] added missing default settings --- .../settings/defaults/system_settings/modules.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 9d8910689a..3ed41c7a49 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -131,16 +131,17 @@ } } }, + "kitsu": { + "enabled": false, + "server": "" + }, "shotgrid": { "enabled": false, "leecher_manager_url": "http://127.0.0.1:3000", "leecher_backend_url": "http://127.0.0.1:8090", + "filter_projects_by_login": true, "shotgrid_settings": {} }, - "kitsu": { - "enabled": false, - "server": "" - }, "timers_manager": { "enabled": true, "auto_stop": true, @@ -209,4 +210,4 @@ "linux": "" } } -} +} \ No newline at end of file From 2e0fe9335151c6b7cdc9d25011216ca3b2705f5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:16:46 +0200 Subject: [PATCH 213/432] use KnownPublishError instead of assertions --- openpype/plugins/publish/integrate.py | 42 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8ab508adc9..e87538a5a4 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -517,14 +517,16 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # pre-flight validations if repre["ext"].startswith("."): - raise ValueError("Extension must not start with a dot '.': " - "{}".format(repre["ext"])) + raise KnownPublishError(( + "Extension must not start with a dot '.': {}" + ).format(repre["ext"])) if repre.get("transfers"): - raise ValueError("Representation is not allowed to have transfers" - "data before integration. They are computed in " - "the integrator" - "Got: {}".format(repre["transfers"])) + raise KnownPublishError(( + "Representation is not allowed to have transfers" + "data before integration. They are computed in " + "the integrator. Got: {}" + ).format(repre["transfers"])) # create template data for Anatomy template_data = copy.deepcopy(instance.data["anatomyData"]) @@ -563,8 +565,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "{}".format(instance_stagingdir)) stagingdir = instance_stagingdir if not stagingdir: - raise ValueError("No staging directory set for representation: " - "{}".format(repre)) + raise KnownPublishError( + "No staging directory set for representation: {}".format(repre) + ) self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data['anatomy'] @@ -574,9 +577,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: # Collection of files (sequence) - assert not any(os.path.isabs(fname) for fname in files), ( - "Given file names contain full paths" - ) + if any(os.path.isabs(fname) for fname in files): + raise KnownPublishError("Given file names contain full paths") src_collection = assemble(files) @@ -632,9 +634,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): dst_collection.indexes.clear() dst_collection.indexes.update(set(destination_indexes)) dst_collection.padding = destination_padding - assert ( - len(src_collection.indexes) == len(dst_collection.indexes) - ), "This is a bug" + if len(src_collection.indexes) != len(dst_collection.indexes): + raise KnownPublishError(( + "This is a bug. Source sequence frames length" + " does not match integration frames length" + )) # Multiple file transfers transfers = [] @@ -645,9 +649,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): else: # Single file fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) + if os.path.isabs(fname): + self.log.error( + "Filename in representation is filepath {}".format(fname) + ) + raise KnownPublishError( + "This is a bug. Representation file name is full path" + ) # Manage anatomy template data template_data.pop("frame", None) From 1bb9b27c7ff5a8c7d0a8fb4c1e631e5e6d33be1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:17:07 +0200 Subject: [PATCH 214/432] simplified staging dir resolving --- openpype/plugins/publish/integrate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e87538a5a4..fdf5b21a6b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -556,14 +556,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): continue template_data[anatomy_key] = value - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - else: + stagingdir = repre.get("stagingDir") + if not stagingdir: # Fall back to instance staging dir if not explicitly # set for representation in the instance - self.log.debug("Representation uses instance staging dir: " - "{}".format(instance_stagingdir)) + self.log.debug(( + "Representation uses instance staging dir: {}" + ).format(instance_stagingdir)) stagingdir = instance_stagingdir + if not stagingdir: raise KnownPublishError( "No staging directory set for representation: {}".format(repre) From 89d49533e4f15b3e055be9d01250780abb1bc199 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:17:56 +0200 Subject: [PATCH 215/432] add the values only if they are not 'None' --- openpype/plugins/publish/integrate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index fdf5b21a6b..87058dd2da 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -686,9 +686,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Also add these values to the context even if not used by the # destination template value = template_data.get(key) - if not value: - continue - repre_context[key] = template_data[key] + if value is not None: + repre_context[key] = value # Explicitly store the full list even though template data might # have a different value because it uses just a single udim tile From 5272907504aa4b6e825d715dd7b9c1714f6fb85b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:18:34 +0200 Subject: [PATCH 216/432] import source_hash directly --- openpype/plugins/publish/integrate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 87058dd2da..a5f5a66091 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -9,12 +9,12 @@ from bson.objectid import ObjectId from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api -import openpype.api from openpype.client import ( get_representations, get_subset_by_name, get_version_by_name, ) +from openype.lib import source_hash from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io @@ -834,6 +834,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" + # Anatomy data is pre-filled by Collectors anatomy_data = instance.data["anatomyData"] @@ -864,6 +865,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): path: modified path if possible, or unmodified path + warning logged """ + success, rootless_path = anatomy.find_root_template_from_path(path) if success: path = rootless_path @@ -885,6 +887,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): output_resources: array of dictionaries to be added to 'files' key in representation """ + file_infos = [] for file_path in destinations: file_info = self.prepare_file_info(file_path, anatomy, sites=sites) @@ -904,10 +907,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): Returns: dict: file info dictionary """ + return { "_id": ObjectId(), "path": self.get_rootless_path(anatomy, path), "size": os.path.getsize(path), - "hash": openpype.api.source_hash(path), + "hash": source_hash(path), "sites": sites } From 0c061c50276ac68ead8b7d3918b007e65ab543e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:26:38 +0200 Subject: [PATCH 217/432] added "output" to representation context keys to auto fill it to context --- openpype/plugins/publish/integrate.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index a5f5a66091..52a5ea2bfc 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -168,7 +168,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the database even if not used by the destination template db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "username" + "family", "hierarchy", "username", "output" ] skip_host_families = [] @@ -727,11 +727,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "context": repre_context } - # todo: simplify/streamline which additional data makes its way into - # the representation context - if repre.get("outputName"): - representation["context"]["output"] = repre['outputName'] - if is_sequence_representation and repre.get("frameStart") is not None: representation['context']['frame'] = template_data["frame"] From 9875f68cf43fef06e4670c6a5c61f3b3d5c0dbb0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:27:13 +0200 Subject: [PATCH 218/432] don't just check existence of key but also it's value when traversing repre and instance data --- openpype/plugins/publish/integrate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 52a5ea2bfc..f89e7b33ce 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -548,13 +548,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): }.items(): # Allow to take value from representation # if not found also consider instance.data - if key in repre: - value = repre[key] - elif key in instance.data: - value = instance.data[key] - else: - continue - template_data[anatomy_key] = value + value = repre.get(key) + if value is None: + value = instance.data.get(key) + + if value is not None: + template_data[anatomy_key] = value stagingdir = repre.get("stagingDir") if not stagingdir: From 0be6d5b55c0266241d7960a9a33056762cf788c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:29:24 +0200 Subject: [PATCH 219/432] removed backwards compatibility comments which as it's not backwards compatibility --- openpype/plugins/publish/integrate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index f89e7b33ce..7dfd8e4cac 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -700,14 +700,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): else: repre_id = ObjectId() - # Backwards compatibility: # Store first transferred destination as published path data - # todo: can we remove this? - # todo: We shouldn't change data that makes its way back into - # instance.data[] until we know the publish actually succeeded - # otherwise `published_path` might not actually be valid? + # - used primarily for reviews that are integrated to custom modules + # TODO we should probably store all integrated files + # related to the representation? published_path = transfers[0][1] - repre["published_path"] = published_path # Backwards compatibility + repre["published_path"] = published_path # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above From 12af64dbc0ed7eb6b415d55bc472c81c917eff7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:30:34 +0200 Subject: [PATCH 220/432] use last frame instead of first frame for padding and don't look at source collection padding --- openpype/plugins/publish/integrate.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7dfd8e4cac..3a86f4b373 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -78,12 +78,6 @@ def get_frame_padded(frame, padding): return "{frame:0{padding}d}".format(padding=padding, frame=frame) -def get_first_frame_padded(collection): - """Return first frame as padded number from `clique.Collection`""" - start_frame = next(iter(collection.indexes)) - return get_frame_padded(start_frame, padding=collection.padding) - - class IntegrateAsset(pyblish.api.InstancePlugin): """Register publish in the database and transfer files to destinations. @@ -588,7 +582,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # differs from the collection we want to shift the destination # frame indices from the source collection. destination_indexes = list(src_collection.indexes) - destination_padding = len(get_first_frame_padded(src_collection)) + # Use last frame for minimum padding + # - that should cover both 'udim' and 'frame' minimum padding + destination_padding = len(str(destination_indexes[-1])) if repre.get("frameStart") is not None and not is_udim: index_frame_start = int(repre.get("frameStart")) From 6cab5917c4903df529429ad5e5bf209409426708 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:36:23 +0200 Subject: [PATCH 221/432] use template padding for frames if padding is bigger then minimum collection's padding --- openpype/plugins/publish/integrate.py | 39 +++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 3a86f4b373..7a9cee593b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -565,7 +565,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data['anatomy'] - template = os.path.normpath(anatomy.templates[template_name]["path"]) + publish_template_category = anatomy.templates[template_name] + template = os.path.normpath(publish_template_category["path"]) is_udim = bool(repre.get("udim")) is_sequence_representation = isinstance(files, (list, tuple)) @@ -585,27 +586,25 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Use last frame for minimum padding # - that should cover both 'udim' and 'frame' minimum padding destination_padding = len(str(destination_indexes[-1])) - if repre.get("frameStart") is not None and not is_udim: - index_frame_start = int(repre.get("frameStart")) - - render_template = anatomy.templates[template_name] - # todo: should we ALWAYS manage the frame padding even when not - # having `frameStart` set? - frame_start_padding = int( - render_template.get( - "frame_padding", - render_template.get("padding") - ) + if not is_udim: + # Change padding for frames if template has defined higher + # padding. + template_padding = int( + publish_template_category["frame_padding"] ) + if template_padding > destination_padding: + destination_padding = template_padding - # Shift destination sequence to the start frame - src_start_frame = next(iter(src_collection.indexes)) - shift = index_frame_start - src_start_frame - if shift: - destination_indexes = [ - frame + shift for frame in destination_indexes - ] - destination_padding = frame_start_padding + if repre.get("frameStart") is not None: + index_frame_start = int(repre.get("frameStart")) + + # Shift destination sequence to the start frame + src_start_frame = next(iter(src_collection.indexes)) + shift = index_frame_start - src_start_frame + if shift: + destination_indexes = [ + frame + shift for frame in destination_indexes + ] # To construct the destination template with anatomy we require # a Frame or UDIM tile set for the template data. We use the first From 3835695376ff87983124a9ac802b5ecffa5e0344 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:38:51 +0200 Subject: [PATCH 222/432] simplified recalculation of destination indexes --- openpype/plugins/publish/integrate.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7a9cee593b..0387196a8a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -577,11 +577,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): src_collection = assemble(files) - # If the representation has `frameStart` set it renumbers the - # frame indices of the published collection. It will start from - # that `frameStart` index instead. Thus if that frame start - # differs from the collection we want to shift the destination - # frame indices from the source collection. destination_indexes = list(src_collection.indexes) # Use last frame for minimum padding # - that should cover both 'udim' and 'frame' minimum padding @@ -595,16 +590,19 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if template_padding > destination_padding: destination_padding = template_padding - if repre.get("frameStart") is not None: - index_frame_start = int(repre.get("frameStart")) - + # If the representation has `frameStart` set it renumbers the + # frame indices of the published collection. It will start from + # that `frameStart` index instead. Thus if that frame start + # differs from the collection we want to shift the destination + # frame indices from the source collection. + repre_frame_start = repre.get("frameStart") + if repre_frame_start is not None: + index_frame_start = int(repre["frameStart"]) # Shift destination sequence to the start frame - src_start_frame = next(iter(src_collection.indexes)) - shift = index_frame_start - src_start_frame - if shift: - destination_indexes = [ - frame + shift for frame in destination_indexes - ] + destination_indexes = [ + index_frame_start + idx + for idx in range(len(destination_indexes)) + ] # To construct the destination template with anatomy we require # a Frame or UDIM tile set for the template data. We use the first From 879df0a3a79121a2fe9472e89e99537fc24f2040 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:51:16 +0200 Subject: [PATCH 223/432] unify quotations --- openpype/plugins/publish/integrate.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 0387196a8a..81a2190a21 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -526,7 +526,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data = copy.deepcopy(instance.data["anatomyData"]) # required representation keys - files = repre['files'] + files = repre["files"] template_data["representation"] = repre["name"] template_data["ext"] = repre["ext"] @@ -564,11 +564,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ) self.log.debug("Anatomy template name: {}".format(template_name)) - anatomy = instance.context.data['anatomy'] + anatomy = instance.context.data["anatomy"] publish_template_category = anatomy.templates[template_name] template = os.path.normpath(publish_template_category["path"]) is_udim = bool(repre.get("udim")) + is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: # Collection of files (sequence) @@ -704,13 +705,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # we should simplify/clarify difference between data above # and the actual representation entity for the database data = repre.get("data", {}) - data.update({'path': published_path, 'template': template}) + data.update({"path": published_path, "template": template}) representation = { "_id": repre_id, "schema": "openpype:representation-2.0", "type": "representation", "parent": version["_id"], - "name": repre['name'], + "name": repre["name"], "data": data, # Imprint shortcut to context for performance reasons. @@ -718,7 +719,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): } if is_sequence_representation and repre.get("frameStart") is not None: - representation['context']['frame'] = template_data["frame"] + representation["context"]["frame"] = template_data["frame"] return { "representation": representation, @@ -779,7 +780,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): version_data[key] = instance.data[key] # Include instance.data[versionData] directly - version_data_instance = instance.data.get('versionData') + version_data_instance = instance.data.get("versionData") if version_data_instance: version_data.update(version_data_instance) From 74ad4a558d9574f85cfe852576b6fdc2d40641ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:51:24 +0200 Subject: [PATCH 224/432] fix typo in import --- openpype/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 81a2190a21..db55a17e59 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -14,7 +14,7 @@ from openpype.client import ( get_subset_by_name, get_version_by_name, ) -from openype.lib import source_hash +from openpype.lib import source_hash from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io From b5cdebe0707c9e4a9acccd16b6db92108ba8cca8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:56:39 +0200 Subject: [PATCH 225/432] make sure frame is filled durectly in sequence condition --- openpype/plugins/publish/integrate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index db55a17e59..c106649f2a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -621,6 +621,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] repre_context = template_filled.used_values + + # Make sure context contains frame + # NOTE: Frame would not be available only if template does not + # contain '{frame}' in template -> Do we want support it? + if not is_udim: + repre_context["frame"] = first_index_padded + self.log.debug("Template filled: {}".format(str(template_filled))) dst_collection = assemble([os.path.normpath(template_filled)]) @@ -718,9 +725,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "context": repre_context } - if is_sequence_representation and repre.get("frameStart") is not None: - representation["context"]["frame"] = template_data["frame"] - return { "representation": representation, "anatomy_data": template_data, From b0571153785b1bf6626738e8bf4f29c54c74c38d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Jul 2022 20:01:36 +0800 Subject: [PATCH 226/432] add write-color-sets option in point cache --- .../maya/plugins/create/create_pointcache.py | 5 +++- .../defaults/project_settings/maya.json | 1 + .../schemas/schema_maya_create.json | 30 ++++++++++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index e876015adb..0d71f2995d 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -12,13 +12,16 @@ class CreatePointCache(plugin.Creator): family = "pointcache" icon = "gears" + write_color_sets = False + + def __init__(self, *args, **kwargs): super(CreatePointCache, self).__init__(*args, **kwargs) # Add animation data self.data.update(lib.collect_animation_data()) - self.data["writeColorSets"] = False # Vertex colors with the geometry. + self.data["writeColorSets"] = self.write_color_sets # Vertex colors with the geometry. self.data["writeFaceSets"] = False # Vertex colors with the geometry. self.data["renderableOnly"] = False # Only renderable visible shapes self.data["visibleOnly"] = False # only nodes that are visible diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 70bedf55d8..d8b107b709 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -136,6 +136,7 @@ }, "CreatePointCache": { "enabled": true, + "write_color_sets": false, "defaults": [ "Main" ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 9000b0246f..e0684597f5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -168,6 +168,32 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CreatePointCache", + "label": "Create Cache", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "write_color_sets", + "label": "Write Color Sets" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + }, + { "type": "schema_template", "name": "template_create_plugin", @@ -208,10 +234,6 @@ "key": "CreateModel", "label": "Create Model" }, - { - "key": "CreatePointCache", - "label": "Create Cache" - }, { "key": "CreateRenderSetup", "label": "Create Render Setup" From 968151f3433ceed9fdf7ad9c793543ca493c26d8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Jul 2022 21:56:36 +0800 Subject: [PATCH 227/432] fix the name of Point Cache in the Project Setting --- openpype/hosts/maya/plugins/create/create_animation.py | 3 ++- openpype/hosts/maya/plugins/create/create_pointcache.py | 8 ++++---- .../projects_schema/schemas/schema_maya_create.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index b7f473acef..7fc9c1e63e 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -11,7 +11,8 @@ class CreateAnimation(plugin.Creator): label = "Animation" family = "animation" icon = "male" - write_color_sets = False + + write_color_sets = False def __init__(self, *args, **kwargs): super(CreateAnimation, self).__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 0d71f2995d..0da781dfa0 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -11,9 +11,8 @@ class CreatePointCache(plugin.Creator): label = "Point Cache" family = "pointcache" icon = "gears" - - write_color_sets = False - + + write_color_sets = False def __init__(self, *args, **kwargs): super(CreatePointCache, self).__init__(*args, **kwargs) @@ -21,7 +20,8 @@ class CreatePointCache(plugin.Creator): # Add animation data self.data.update(lib.collect_animation_data()) - self.data["writeColorSets"] = self.write_color_sets # Vertex colors with the geometry. + # Vertex colors with the geometry. + self.data["writeColorSets"] = self.write_color_sets self.data["writeFaceSets"] = False # Vertex colors with the geometry. self.data["renderableOnly"] = False # Only renderable visible shapes self.data["visibleOnly"] = False # only nodes that are visible diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index e0684597f5..2e4d8edef1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -172,7 +172,7 @@ "type": "dict", "collapsible": true, "key": "CreatePointCache", - "label": "Create Cache", + "label": "Create Point Cache", "checkbox_key": "enabled", "children": [ { From 6568e9cc605a39264077d6158baa76bf50d454f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Jul 2022 22:03:41 +0800 Subject: [PATCH 228/432] fix the name of Point Cache in Settings --- openpype/hosts/maya/plugins/create/create_animation.py | 2 +- openpype/hosts/maya/plugins/create/create_pointcache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 7fc9c1e63e..31d4f968d1 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -11,7 +11,7 @@ class CreateAnimation(plugin.Creator): label = "Animation" family = "animation" icon = "male" - + write_color_sets = False def __init__(self, *args, **kwargs): diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 0da781dfa0..1c83a9c20d 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -21,7 +21,7 @@ class CreatePointCache(plugin.Creator): self.data.update(lib.collect_animation_data()) # Vertex colors with the geometry. - self.data["writeColorSets"] = self.write_color_sets + self.data["writeColorSets"] = self.write_color_sets self.data["writeFaceSets"] = False # Vertex colors with the geometry. self.data["renderableOnly"] = False # Only renderable visible shapes self.data["visibleOnly"] = False # only nodes that are visible From 71a927d06ff6f6f407169aaffa2f79edb9b74199 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Jul 2022 22:08:04 +0800 Subject: [PATCH 229/432] add write color sets to Settings and rename Create Cache to Create Point Cache in Settings --- openpype/hosts/maya/plugins/create/create_animation.py | 1 - openpype/hosts/maya/plugins/create/create_pointcache.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 31d4f968d1..e47d4e5b5a 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -11,7 +11,6 @@ class CreateAnimation(plugin.Creator): label = "Animation" family = "animation" icon = "male" - write_color_sets = False def __init__(self, *args, **kwargs): diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 1c83a9c20d..5516445de8 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -11,7 +11,6 @@ class CreatePointCache(plugin.Creator): label = "Point Cache" family = "pointcache" icon = "gears" - write_color_sets = False def __init__(self, *args, **kwargs): From 4379dc019e4069ca44240aec565c1d136879f1a8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 27 Jul 2022 18:05:32 +0200 Subject: [PATCH 230/432] OP-3283 - implemented proper usage of {layer} in subset template for legacy creator {layer} placeholder could be used in project_settings/global/tools/creator/subset_name_profiles to drive lower/upper cases when layer is used in subset name (eg. when multiple subsets are created at once). Warning {layer} means keep layer name as it is, not lowercasing! --- .../plugins/create/create_legacy_image.py | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 9736471a26..142cddfd52 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -1,6 +1,11 @@ from Qt import QtWidgets from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline import legacy_io +from openpype.client import get_asset_by_name +from openpype.settings import get_project_settings +from openpype.lib import prepare_template_data +from openpype.lib.profiles_filtering import filter_profiles class CreateImage(create.LegacyCreator): @@ -82,7 +87,18 @@ class CreateImage(create.LegacyCreator): subset_name = creator_subset_name if len(groups) > 1: - subset_name += group.name.title().replace(" ", "") + subset_template = self._get_subset_template(self.family) + if not subset_template or 'layer' not in subset_template.lower(): + subset_name += group.name.title().replace(" ", "") + else: + fill_pairs = { + "variant": self.data["variant"], + "family": self.family, + "task": legacy_io.Session["AVALON_TASK"], + "layer": group.name + } + + subset_name = subset_template.format(**prepare_template_data(fill_pairs)) if group.long_name: for directory in group.long_name[::-1]: @@ -98,3 +114,34 @@ class CreateImage(create.LegacyCreator): # reusing existing group, need to rename afterwards if not create_group: stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + return {"layer": ""} + + def _get_subset_template(self, family): + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.tasks"] + ) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + tools_settings = get_project_settings(project_name)["global"]["tools"] + profiles = tools_settings["creator"]["subset_name_profiles"] + filtering_criteria = { + "families": family, + "hosts": "photoshop", + "tasks": task_name, + "task_types": task_type + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + if matching_profile: + return matching_profile["template"] From 4c849e8d86e7665cc4ee3e235403f2baf41e8b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 27 Jul 2022 18:14:22 +0200 Subject: [PATCH 231/432] :bug: fix environment resolution this will fix environment resolution of general settings in one pass --- start.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index e83589d160..cbf8ffd178 100644 --- a/start.py +++ b/start.py @@ -270,8 +270,11 @@ def set_openpype_global_environments() -> None: general_env = get_general_environments() + # first resolve general environment because merge doesn't expect + # values to be list. + # TODO: switch to OpenPype environment functions merged_env = acre.merge( - acre.parse(general_env), + acre.compute(acre.parse(general_env), cleanup=False), dict(os.environ) ) env = acre.compute( From 52314b0bf514f58c042c2a7c7bdd9d45a24ae2e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 19:03:08 +0200 Subject: [PATCH 232/432] update ftrack api to 2.3.3 --- openpype/modules/ftrack/ftrack_server/lib.py | 21 +++++++++++++++++--- poetry.lock | 20 +++++++++---------- pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index 3da1e7c7f0..947dacf917 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -7,6 +7,7 @@ import threading import datetime import time import queue +import collections import appdirs import pymongo @@ -309,7 +310,20 @@ class CustomEventHubSession(ftrack_api.session.Session): # Currently pending operations. self.recorded_operations = ftrack_api.operation.Operations() - self.record_operations = True + + # OpenPype change - In new API are operations properties + new_api = hasattr(self.__class__, "record_operations") + + if new_api: + self._record_operations = collections.defaultdict( + lambda: True + ) + self._auto_populate = collections.defaultdict( + lambda: auto_populate + ) + else: + self.record_operations = True + self.auto_populate = auto_populate self.cache_key_maker = cache_key_maker if self.cache_key_maker is None: @@ -328,6 +342,9 @@ class CustomEventHubSession(ftrack_api.session.Session): if cache is not None: self.cache.caches.append(cache) + if new_api: + self.merge_lock = threading.RLock() + self._managed_request = None self._request = requests.Session() self._request.auth = ftrack_api.session.SessionAuthentication( @@ -335,8 +352,6 @@ class CustomEventHubSession(ftrack_api.session.Session): ) self.request_timeout = timeout - self.auto_populate = auto_populate - # Fetch server information and in doing so also check credentials. self._server_information = self._fetch_server_information() diff --git a/poetry.lock b/poetry.lock index 0033bc0d73..33deab003e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -221,7 +221,7 @@ python-versions = "~=3.7" [[package]] name = "certifi" -version = "2022.5.18.1" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -456,19 +456,20 @@ python-versions = ">=3.7" [[package]] name = "ftrack-python-api" -version = "2.0.0" +version = "2.3.3" description = "Python API for ftrack." category = "main" optional = false -python-versions = ">=2.7.9, <4.0" +python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, < 3.10" [package.dependencies] +appdirs = ">=1,<2" arrow = ">=0.4.4,<1" -clique = ">=1.2.0,<2" +clique = "1.6.1" future = ">=0.16.0,<1" pyparsing = ">=2.0,<3" requests = ">=2,<3" -six = ">=1,<2" +six = ">=1.13.0,<2" termcolor = ">=1.1.0,<2" websocket-client = ">=0.40.0,<1" @@ -1885,8 +1886,8 @@ cachetools = [ {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, ] certifi = [ - {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, - {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] cffi = [ {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, @@ -2152,10 +2153,7 @@ frozenlist = [ {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, ] -ftrack-python-api = [ - {file = "ftrack-python-api-2.0.0.tar.gz", hash = "sha256:dd6f02c31daf5a10078196dc9eac4671e4297c762fbbf4df98de668ac12281d9"}, - {file = "ftrack_python_api-2.0.0-py2.py3-none-any.whl", hash = "sha256:d0df0f2df4b53947272f95e179ec98b477ee425bf4217b37bb59030ad989771e"}, -] +ftrack-python-api = [] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] diff --git a/pyproject.toml b/pyproject.toml index 1627b5e1c1..5785c7635b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ coolname = "*" clique = "1.6.*" Click = "^7" dnspython = "^2.1.0" -ftrack-python-api = "2.0.*" +ftrack-python-api = "^2.3.3" shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} gazu = "^0.8.28" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) From 3e7a9d3e468ebb7b9149fb3b5d7c1fed200732b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 19:04:22 +0200 Subject: [PATCH 233/432] use master branch of appdirs --- poetry.lock | 14 +++++++++----- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0033bc0d73..72e5763c9c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -92,7 +92,14 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/ActiveState/appdirs.git" +reference = "master" +resolved_reference = "193a2cbba58cce2542882fcedd0e49f6763672ed" [[package]] name = "arrow" @@ -1827,10 +1834,7 @@ ansicon = [ {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] +appdirs = [] arrow = [ {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, diff --git a/pyproject.toml b/pyproject.toml index 1627b5e1c1..4361c8c9f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ aiohttp = "^3.7" aiohttp_json_rpc = "*" # TVPaint server acre = { git = "https://github.com/pypeclub/acre.git" } opentimelineio = { version = "0.14.0.dev1", source = "openpype" } -appdirs = "^1.4.3" +appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master" } blessed = "^1.17" # openpype terminal formatting coolname = "*" clique = "1.6.*" From a1122496c1c57e62a6a1118cee0fbcc20d4eec1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Jul 2022 10:46:25 +0200 Subject: [PATCH 234/432] add missing project tasks into fields --- openpype/pipeline/template_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/template_data.py b/openpype/pipeline/template_data.py index de46650f9d..824a25127c 100644 --- a/openpype/pipeline/template_data.py +++ b/openpype/pipeline/template_data.py @@ -213,7 +213,9 @@ def get_template_data_with_names( Dict[str, Any]: Data prepared for filling workdir template. """ - project_doc = get_project(project_name, fields=["name", "data.code"]) + project_doc = get_project( + project_name, fields=["name", "data.code", "config.tasks"] + ) asset_doc = None if asset_name: asset_doc = get_asset_by_name( From 7adb8453861ce29f095082494ced13b755921fc5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 12:24:46 +0300 Subject: [PATCH 235/432] Add OCIO submodule. --- .gitmodules | 3 +++ vendor/configs/OpenColorIO-Configs | 1 + 2 files changed, 4 insertions(+) create mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/.gitmodules b/.gitmodules index dfd89cdb3c..bac3132b77 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor url = https://github.com/EvotecIT/PSWriteColor.git +[submodule "vendor/configs/OpenColorIO-Configs"] + path = vendor/configs/OpenColorIO-Configs + url = https://github.com/imageworks/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 0000000000..0bb079c08b --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 86070835b9883b46baa27e12bb079b9866b18356 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 12:33:29 +0300 Subject: [PATCH 236/432] Add OCIO path function. --- .../maya/plugins/publish/extract_look.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index d35b529c76..ce699d3d9a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -534,3 +534,25 @@ class ExtractModelRenderSets(ExtractLook): self.scene_type = self.scene_type_prefix + self.scene_type return typ + + +def get_ocio_config_path(profile_folder): + """Path to OpenPype vendorized OCIO. + + Vendorized OCIO config file path is grabbed from the specific path + hierarchy specified below. + + "{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio" + Args: + profile_folder (str): Name of folder to grab config file from. + + Returns: + str: Path to vendorized config file. + """ + return os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "OpenColorIO-Configs", + profile_folder, + "config.ocio" + ) From 03767d28912b65a47b66826cc359a6db0baf4533 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 13:03:37 +0300 Subject: [PATCH 237/432] move function --- .../maya/plugins/publish/extract_look.py | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index ce699d3d9a..42d4835fdf 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -27,6 +27,28 @@ def escape_space(path): return '"{}"'.format(path) if " " in path else path +def get_ocio_config_path(profile_folder): + """Path to OpenPype vendorized OCIO. + + Vendorized OCIO config file path is grabbed from the specific path + hierarchy specified below. + + "{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio" + Args: + profile_folder (str): Name of folder to grab config file from. + + Returns: + str: Path to vendorized config file. + """ + return os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "OpenColorIO-Configs", + profile_folder, + "config.ocio" + ) + + def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. @@ -492,7 +514,6 @@ class ExtractLook(openpype.api.Extractor): colorconvert = "--colorconvert sRGB linear" else: colorconvert = "" - # Ensure folder exists if not os.path.exists(os.path.dirname(converted)): os.makedirs(os.path.dirname(converted)) @@ -534,25 +555,3 @@ class ExtractModelRenderSets(ExtractLook): self.scene_type = self.scene_type_prefix + self.scene_type return typ - - -def get_ocio_config_path(profile_folder): - """Path to OpenPype vendorized OCIO. - - Vendorized OCIO config file path is grabbed from the specific path - hierarchy specified below. - - "{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio" - Args: - profile_folder (str): Name of folder to grab config file from. - - Returns: - str: Path to vendorized config file. - """ - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "OpenColorIO-Configs", - profile_folder, - "config.ocio" - ) From cd7ef426d891381de1c8d4e028c967793784d130 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 13:31:39 +0300 Subject: [PATCH 238/432] Add configuration variable to `maketx` --- openpype/hosts/maya/plugins/publish/extract_look.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 42d4835fdf..faea0247da 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -514,6 +514,9 @@ class ExtractLook(openpype.api.Extractor): colorconvert = "--colorconvert sRGB linear" else: colorconvert = "" + + config_path = get_ocio_config_path("nuke-default") + color_config = "--colorconfig {0}".format(config_path) # Ensure folder exists if not os.path.exists(os.path.dirname(converted)): os.makedirs(os.path.dirname(converted)) @@ -523,10 +526,11 @@ class ExtractLook(openpype.api.Extractor): filepath, converted, # Include `source-hash` as string metadata - "-sattrib", + "--sattrib", "sourceHash", escape_space(texture_hash), colorconvert, + color_config ) return converted, COPY, texture_hash From 81f3bd379b34acb9727a9ab6ad621a87e9bcb9b1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 13:31:58 +0300 Subject: [PATCH 239/432] Fix function path bug --- openpype/hosts/maya/plugins/publish/extract_look.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index faea0247da..f71a01e474 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -43,6 +43,7 @@ def get_ocio_config_path(profile_folder): return os.path.join( os.environ["OPENPYPE_ROOT"], "vendor", + "config", "OpenColorIO-Configs", profile_folder, "config.ocio" From 8c95aab796ec3cc284851b2d1c3170ead24a22b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Jul 2022 12:41:49 +0200 Subject: [PATCH 240/432] OP-3283 - extracted logic to plugin to reuse --- openpype/hosts/photoshop/api/plugin.py | 53 +++++++++++++++++++ .../plugins/create/create_legacy_image.py | 50 +++-------------- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index c80e6bbd06..ecbfbf91e3 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -2,6 +2,11 @@ import re from openpype.pipeline import LoaderPlugin from .launch_logic import stub +from openpype.pipeline import legacy_io +from openpype.client import get_asset_by_name +from openpype.settings import get_project_settings +from openpype.lib import prepare_template_data +from openpype.lib.profiles_filtering import filter_profiles def get_unique_layer_name(layers, asset_name, subset_name): @@ -33,3 +38,51 @@ class PhotoshopLoader(LoaderPlugin): @staticmethod def get_stub(): return stub() + + +def get_subset_template(family): + """Get subset template name from Settings""" + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.tasks"] + ) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + tools_settings = get_project_settings(project_name)["global"]["tools"] + profiles = tools_settings["creator"]["subset_name_profiles"] + filtering_criteria = { + "families": family, + "hosts": "photoshop", + "tasks": task_name, + "task_types": task_type + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + if matching_profile: + return matching_profile["template"] + + +def get_subset_name_for_multiple(subset_name, subset_template, group, + family, variant): + """Update subset name with layer information to differentiate multiple + + subset_template might contain specific way how to format layer name + ({layer},{Layer} or {LAYER}). If subset_template doesn't contain placeholder + at all, fall back to original solution. + """ + if not subset_template or 'layer' not in subset_template.lower(): + subset_name += group.name.title().replace(" ", "") + else: + fill_pairs = { + "family": family, + "variant": variant, + "task": legacy_io.Session["AVALON_TASK"], + "layer": group.name + } + + return subset_template.format(**prepare_template_data(fill_pairs)) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 142cddfd52..6d0587c20c 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -1,11 +1,8 @@ from Qt import QtWidgets from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop -from openpype.pipeline import legacy_io -from openpype.client import get_asset_by_name -from openpype.settings import get_project_settings -from openpype.lib import prepare_template_data -from openpype.lib.profiles_filtering import filter_profiles + +from openpype.hosts.photoshop.api.plugin import get_subset_template, get_subset_name_for_multiple class CreateImage(create.LegacyCreator): @@ -87,18 +84,12 @@ class CreateImage(create.LegacyCreator): subset_name = creator_subset_name if len(groups) > 1: - subset_template = self._get_subset_template(self.family) - if not subset_template or 'layer' not in subset_template.lower(): - subset_name += group.name.title().replace(" ", "") - else: - fill_pairs = { - "variant": self.data["variant"], - "family": self.family, - "task": legacy_io.Session["AVALON_TASK"], - "layer": group.name - } - - subset_name = subset_template.format(**prepare_template_data(fill_pairs)) + subset_template = get_subset_template(self.family) + subset_name = get_subset_name_for_multiple(subset_name, + subset_template, + group, + self.family, + self.data["variant"]) if group.long_name: for directory in group.long_name[::-1]: @@ -120,28 +111,3 @@ class CreateImage(create.LegacyCreator): cls, variant, task_name, asset_id, project_name, host_name ): return {"layer": ""} - - def _get_subset_template(self, family): - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["data.tasks"] - ) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - tools_settings = get_project_settings(project_name)["global"]["tools"] - profiles = tools_settings["creator"]["subset_name_profiles"] - filtering_criteria = { - "families": family, - "hosts": "photoshop", - "tasks": task_name, - "task_types": task_type - } - - matching_profile = filter_profiles(profiles, filtering_criteria) - if matching_profile: - return matching_profile["template"] From e287e1fd48af95c6bd5822e6d0f93d37b7896080 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 13:44:19 +0300 Subject: [PATCH 241/432] Fix bugs --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index f71a01e474..0b26e922d5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -43,7 +43,7 @@ def get_ocio_config_path(profile_folder): return os.path.join( os.environ["OPENPYPE_ROOT"], "vendor", - "config", + "configs", "OpenColorIO-Configs", profile_folder, "config.ocio" @@ -102,10 +102,11 @@ def maketx(source, destination, *args): # use oiio-optimized settings for tile-size, planarconfig, metadata "--oiio", "--filter lanczos3", + escape_space(source) ] cmd.extend(args) - cmd.extend(["-o", escape_space(destination), escape_space(source)]) + cmd.extend(["-o", escape_space(destination)]) cmd = " ".join(cmd) From 87cf386a54917adacfd91542cd3613ac0fe4babc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Jul 2022 12:52:22 +0200 Subject: [PATCH 242/432] OP-3283 - implemented for new creator --- .../photoshop/plugins/create/create_image.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index f15068b031..ebb268dc93 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -5,6 +5,10 @@ from openpype.pipeline import ( CreatedInstance, legacy_io ) +from openpype.hosts.photoshop.api.plugin import ( + get_subset_template, + get_subset_name_for_multiple +) class ImageCreator(Creator): @@ -68,7 +72,12 @@ class ImageCreator(Creator): if creating_multiple_groups: # concatenate with layer name to differentiate subsets - subset_name += group.name.title().replace(" ", "") + subset_template = get_subset_template(self.family) + subset_name = get_subset_name_for_multiple(subset_name, + subset_template, + group, + self.family, + data["variant"]) if group.long_name: for directory in group.long_name[::-1]: @@ -143,3 +152,9 @@ class ImageCreator(Creator): def _clean_highlights(self, stub, item): return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON, '') + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + """Called by UI, empty value for layer must be provided.""" + return {"layer": ""} From a03f2b6a1a6ee24692e25710d55fd0af11eecb96 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Jul 2022 12:53:20 +0200 Subject: [PATCH 243/432] OP-3283 - fixed imports --- .../hosts/photoshop/plugins/create/create_legacy_image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 6d0587c20c..d1a54a407e 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -2,7 +2,10 @@ from Qt import QtWidgets from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop -from openpype.hosts.photoshop.api.plugin import get_subset_template, get_subset_name_for_multiple +from openpype.hosts.photoshop.api.plugin import ( + get_subset_template, + get_subset_name_for_multiple +) class CreateImage(create.LegacyCreator): From bc2cec540c8b7962d3b0c0fc8dabe5f6cf54fb36 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 16:29:50 +0200 Subject: [PATCH 244/432] trayp: improving user feedback --- openpype/hosts/traypublisher/api/editorial.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 92ad65a851..7c392ef508 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -55,7 +55,7 @@ class ShotMetadataSolver: return shot_rename_template.format(**data) except KeyError as _E: raise CreatorError(( - "Make sure all keys are correct in settings: \n\n" + "Make sure all keys in settings are correct:: \n\n" f"From template string {shot_rename_template} > " f"`{_E}` has no equivalent in \n" f"{list(data.keys())} input formating keys!" @@ -91,10 +91,13 @@ class ShotMetadataSolver: match = p.findall(search_text) if not match: raise CreatorError(( - "Make sure regex expression is correct: \n\n" - f"From settings '{token_key}' key " - f"with '{pattern}' expression, \n" - f"is not able to find anything in '{search_text}'!" + "Make sure regex expression works with your data: \n\n" + f"'{token_key}' with regex '{pattern}' in your settings\n" + "can't find any match in your clip name " + f"'{search_text}'!\n\nLook to: " + "'project_settings/traypublisher/editorial_creators" + "/editorial_simple/clip_name_tokenizer'\n" + "at your project settings..." )) # QUESTION:how to refactory `match[-1]` to some better way? @@ -129,7 +132,7 @@ class ShotMetadataSolver: } except KeyError as _E: raise CreatorError(( - "Make sure all keys are correct in settings: \n" + "Make sure all keys in settings are correct : \n" f"`{_E}` has no equivalent in \n{list(data.keys())}" )) @@ -146,9 +149,10 @@ class ShotMetadataSolver: **_parent_tokens_formating_data) except KeyError as _E: raise CreatorError(( - "Make sure all keys are correct in settings: \n\n" - f"From template string {shot_hierarchy['parents_path']} > " - f"`{_E}` has no equivalent in \n" + "Make sure all keys in settings are correct : \n\n" + f"`{_E}` from template string " + f"{shot_hierarchy['parents_path']}, " + f" has no equivalent in \n" f"{list(_parent_tokens_formating_data.keys())} parents" )) From 443c5a369619a907f83c9bdb43783ce64d9edc0e Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 28 Jul 2022 17:01:54 +0200 Subject: [PATCH 245/432] Fix: Shot&Sequence name with prefix over appends --- openpype/modules/kitsu/utils/update_op_with_zou.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 02c27382eb..040d6566f7 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -230,9 +230,9 @@ def update_op_assets( if item_type in ["Shot", "Sequence"]: # Name with parents hierarchy "({episode}_){sequence}_{shot}" # to avoid duplicate name issue - item_name = "_".join(item_data["parents"] + [item_doc["name"]]) + item_name = f"{item_data['parents'][-1]}_{item['name']}" else: - item_name = item_doc["name"] + item_name = item["name"] # Set root folders parents item_data["parents"] = entity_parent_folders + item_data["parents"] From 037c5a13cddc3e9426ebfe2c46ec6abc82eb559f Mon Sep 17 00:00:00 2001 From: kaamaurice Date: Thu, 28 Jul 2022 18:46:47 +0200 Subject: [PATCH 246/432] bugfix blender ops for workfiles dialog --- openpype/hosts/blender/api/ops.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index c1b5add518..4f8410da74 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -220,12 +220,9 @@ class LaunchQtApp(bpy.types.Operator): self._app.store_window(self.bl_idname, window) self._window = window - if not isinstance( - self._window, - (QtWidgets.QMainWindow, QtWidgets.QDialog, ModuleType) - ): + if not isinstance(self._window, (QtWidgets.QWidget, ModuleType)): raise AttributeError( - "`window` should be a `QDialog or module`. Got: {}".format( + "`window` should be a `QWidget or module`. Got: {}".format( str(type(window)) ) ) @@ -249,9 +246,9 @@ class LaunchQtApp(bpy.types.Operator): self._window.setWindowFlags(on_top_flags) self._window.show() - if on_top_flags != origin_flags: - self._window.setWindowFlags(origin_flags) - self._window.show() + # if on_top_flags != origin_flags: + # self._window.setWindowFlags(origin_flags) + # self._window.show() return {'FINISHED'} From 44da89dc8669df3c5a26575c9cc80b1e7ca8f5e6 Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 28 Jul 2022 18:59:46 +0200 Subject: [PATCH 247/432] Fix: project with no dedicated task types doesn't take defaults --- openpype/modules/kitsu/utils/update_op_with_zou.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 040d6566f7..8f5566e8ec 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -276,7 +276,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: project_doc = create_project(project_name, project_name, dbcon=dbcon) # Project data and tasks - project_data = project["data"] or {} + project_data = project_doc["data"] or {} # Build project code and update Kitsu project_code = project.get("code") @@ -305,6 +305,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) + or gazu.task.all_task_types() }, "data": project_data, } From ad4aeb0071e7ee92a592e7de53fb24a230a13bc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 10:38:01 +0200 Subject: [PATCH 248/432] use query functions on remaining places --- openpype/hooks/pre_global_host_data.py | 8 +++----- .../hosts/fusion/scripts/fusion_switch_shot.py | 8 -------- openpype/hosts/testhost/api/pipeline.py | 8 ++++---- .../testhost/plugins/create/auto_creator.py | 13 ++++--------- openpype/pipeline/create/context.py | 14 +++++--------- openpype/pipeline/thumbnail.py | 10 +++------- openpype/scripts/fusion_switch_shot.py | 18 ++++++------------ 7 files changed, 25 insertions(+), 54 deletions(-) diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index 6577e37cbe..8a178915fb 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -1,3 +1,4 @@ +from openpype.client import get_project, get_asset_by_name from openpype.lib import ( PreLaunchHook, EnvironmentPrepData, @@ -69,7 +70,7 @@ class GlobalHostDataHook(PreLaunchHook): self.data["dbcon"] = dbcon # Project document - project_doc = dbcon.find_one({"type": "project"}) + project_doc = get_project(project_name) self.data["project_doc"] = project_doc asset_name = self.data.get("asset_name") @@ -79,8 +80,5 @@ class GlobalHostDataHook(PreLaunchHook): ) return - asset_doc = dbcon.find_one({ - "type": "asset", - "name": asset_name - }) + asset_doc = get_asset_by_name(project_name, asset_name) self.data["asset_doc"] = asset_doc diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index 52a157c56e..87ff8e2ffe 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -3,9 +3,7 @@ import re import sys import logging -# Pipeline imports from openpype.client import ( - get_project, get_asset_by_name, get_versions, ) @@ -21,9 +19,6 @@ from openpype.lib.avalon_context import get_workdir_from_session log = logging.getLogger("Update Slap Comp") -self = sys.modules[__name__] -self._project = None - def _format_version_folder(folder): """Format a version folder based on the filepath @@ -212,9 +207,6 @@ def switch(asset_name, filepath=None, new=True): asset = get_asset_by_name(project_name, asset_name) assert asset, "Could not find '%s' in the database" % asset_name - # Get current project - self._project = get_project(project_name) - # Go to comp if not filepath: current_comp = api.get_current_comp() diff --git a/openpype/hosts/testhost/api/pipeline.py b/openpype/hosts/testhost/api/pipeline.py index 285fe8f8d6..1e05f336fb 100644 --- a/openpype/hosts/testhost/api/pipeline.py +++ b/openpype/hosts/testhost/api/pipeline.py @@ -1,6 +1,6 @@ import os import json -from openpype.pipeline import legacy_io +from openpype.client import get_asset_by_name class HostContext: @@ -17,10 +17,10 @@ class HostContext: if not asset_name: return project_name - asset_doc = legacy_io.find_one( - {"type": "asset", "name": asset_name}, - {"data.parents": 1} + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.parents"] ) + parents = asset_doc.get("data", {}).get("parents") or [] hierarchy = [project_name] diff --git a/openpype/hosts/testhost/plugins/create/auto_creator.py b/openpype/hosts/testhost/plugins/create/auto_creator.py index 06b95375b1..8d59fc3242 100644 --- a/openpype/hosts/testhost/plugins/create/auto_creator.py +++ b/openpype/hosts/testhost/plugins/create/auto_creator.py @@ -1,10 +1,11 @@ from openpype.lib import NumberDef -from openpype.hosts.testhost.api import pipeline +from openpype.client import get_asset_by_name from openpype.pipeline import ( legacy_io, AutoCreator, CreatedInstance, ) +from openpype.hosts.testhost.api import pipeline class MyAutoCreator(AutoCreator): @@ -44,10 +45,7 @@ class MyAutoCreator(AutoCreator): host_name = legacy_io.Session["AVALON_APP"] if existing_instance is None: - asset_doc = legacy_io.find_one({ - "type": "asset", - "name": asset_name - }) + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) @@ -69,10 +67,7 @@ class MyAutoCreator(AutoCreator): existing_instance["asset"] != asset_name or existing_instance["task"] != task_name ): - asset_doc = legacy_io.find_one({ - "type": "asset", - "name": asset_name - }) + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 9b55c3b21e..eaaed39357 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -6,6 +6,7 @@ import inspect from uuid import uuid4 from contextlib import contextmanager +from openpype.client import get_assets from openpype.host import INewPublisher from openpype.pipeline import legacy_io from openpype.pipeline.mongodb import ( @@ -1082,15 +1083,10 @@ class CreateContext: for asset_name in task_names_by_asset_name.keys() if asset_name is not None ] - asset_docs = list(self.dbcon.find( - { - "type": "asset", - "name": {"$in": asset_names} - }, - { - "name": True, - "data.tasks": True - } + asset_docs = list(get_assets( + self.project_name, + asset_names=asset_names, + fields=["name", "data.tasks"] )) task_names_by_asset_name = {} diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index ec97b36954..eb383b16d9 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -2,6 +2,7 @@ import os import copy import logging +from openpype.client import get_project from . import legacy_io from .plugin_discover import ( discover, @@ -85,13 +86,8 @@ class TemplateResolver(ThumbnailResolver): self.log.debug("Thumbnail entity does not have set template") return - project = self.dbcon.find_one( - {"type": "project"}, - { - "name": True, - "data.code": True - } - ) + project_name = self.dbcon.active_project() + project = get_project(project_name, fields=["name", "data.code"]) template_data = copy.deepcopy( thumbnail_entity["data"].get("template_data") or {} diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index 245fc665f0..b5d3290e3a 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -3,6 +3,8 @@ import re import sys import logging +from openpype.client import get_project, get_asset_by_name, get_versions + # Pipeline imports from openpype.hosts.fusion import api import openpype.hosts.fusion.api.lib as fusion_lib @@ -19,9 +21,6 @@ from openpype.lib.avalon_context import get_workdir_from_session log = logging.getLogger("Update Slap Comp") -self = sys.modules[__name__] -self._project = None - def _format_version_folder(folder): """Format a version folder based on the filepath @@ -131,8 +130,8 @@ def update_frame_range(comp, representations): """ version_ids = [r["parent"] for r in representations] - versions = legacy_io.find({"type": "version", "_id": {"$in": version_ids}}) - versions = list(versions) + project_name = legacy_io.active_project() + versions = list(get_versions(project_name, version_ids=version_ids)) start = min(v["data"]["frameStart"] for v in versions) end = max(v["data"]["frameEnd"] for v in versions) @@ -162,15 +161,10 @@ def switch(asset_name, filepath=None, new=True): # Assert asset name exists # It is better to do this here then to wait till switch_shot does it - asset = legacy_io.find_one({"type": "asset", "name": asset_name}) + project_name = legacy_io.active_project() + asset = get_asset_by_name(project_name, asset_name) assert asset, "Could not find '%s' in the database" % asset_name - # Get current project - self._project = legacy_io.find_one({ - "type": "project", - "name": legacy_io.Session["AVALON_PROJECT"] - }) - # Go to comp if not filepath: current_comp = api.get_current_comp() From 0f97387032f5698c2142752a2945383aaf18036b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 11:17:00 +0200 Subject: [PATCH 249/432] remove unused import --- openpype/scripts/fusion_switch_shot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index b5d3290e3a..15f189e7cb 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -3,7 +3,7 @@ import re import sys import logging -from openpype.client import get_project, get_asset_by_name, get_versions +from openpype.client import get_asset_by_name, get_versions # Pipeline imports from openpype.hosts.fusion import api From f08008d61ec577f46a61469dc4bfa8a495d3dfbc Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 12:22:20 +0300 Subject: [PATCH 250/432] Revert "Add OCIO submodule." This reverts commit 7adb8453861ce29f095082494ced13b755921fc5. --- .gitmodules | 3 --- vendor/configs/OpenColorIO-Configs | 1 - 2 files changed, 4 deletions(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/.gitmodules b/.gitmodules index bac3132b77..dfd89cdb3c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,6 +5,3 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor url = https://github.com/EvotecIT/PSWriteColor.git -[submodule "vendor/configs/OpenColorIO-Configs"] - path = vendor/configs/OpenColorIO-Configs - url = https://github.com/imageworks/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 1267e9ea921381ca0b5d8907c0a9271352f0c078 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 12:48:01 +0300 Subject: [PATCH 251/432] Revert "Revert "Add OCIO submodule."" This reverts commit f08008d61ec577f46a61469dc4bfa8a495d3dfbc. --- .gitmodules | 3 +++ vendor/configs/OpenColorIO-Configs | 1 + 2 files changed, 4 insertions(+) create mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/.gitmodules b/.gitmodules index dfd89cdb3c..bac3132b77 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor url = https://github.com/EvotecIT/PSWriteColor.git +[submodule "vendor/configs/OpenColorIO-Configs"] + path = vendor/configs/OpenColorIO-Configs + url = https://github.com/imageworks/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 0000000000..0bb079c08b --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From de8668dc351ff77794da22427145fdbc6fc4b679 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 12:13:43 +0200 Subject: [PATCH 252/432] OP-3283 - fix not create separate from multiple selected --- .../photoshop/plugins/create/create_image.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index ebb268dc93..5688fe376e 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -42,17 +42,17 @@ class ImageCreator(Creator): top_level_selected_items = stub.get_selected_layers() if pre_create_data.get("use_selection"): only_single_item_selected = len(top_level_selected_items) == 1 - for selected_item in top_level_selected_items: - if ( - only_single_item_selected or - pre_create_data.get("create_multiple")): + if ( + only_single_item_selected or + pre_create_data.get("create_multiple")): + for selected_item in top_level_selected_items: if selected_item.group: groups_to_create.append(selected_item) else: top_layers_to_wrap.append(selected_item) - else: - group = stub.group_selected_layers(subset_name_from_ui) - groups_to_create.append(group) + else: + group = stub.group_selected_layers(subset_name_from_ui) + groups_to_create.append(group) if not groups_to_create and not top_layers_to_wrap: group = stub.create_group(subset_name_from_ui) @@ -156,5 +156,4 @@ class ImageCreator(Creator): def get_dynamic_data( cls, variant, task_name, asset_id, project_name, host_name ): - """Called by UI, empty value for layer must be provided.""" - return {"layer": ""} + return {"layer": ""} \ No newline at end of file From 831884232cf1c51a76c25470986ac3b01bc44841 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 13:03:00 +0200 Subject: [PATCH 253/432] OP-3283 - fix without select and multiple If creator was configured to not use selection and not create multiple, it failed before. (It should create one wrapping group, eg. instance, around all. Locked background layer cannot be present!) --- openpype/hosts/photoshop/plugins/create/create_image.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 5688fe376e..2b6e5e6448 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -53,6 +53,13 @@ class ImageCreator(Creator): else: group = stub.group_selected_layers(subset_name_from_ui) groups_to_create.append(group) + else: + stub.select_layers(stub.get_layers()) + try: + group = stub.group_selected_layers(subset_name_from_ui) + except: + raise ValueError("Cannot group locked Bakcground layer!") + groups_to_create.append(group) if not groups_to_create and not top_layers_to_wrap: group = stub.create_group(subset_name_from_ui) From 90962d673511c60df44e34e746753208fc359a1c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 14:41:28 +0200 Subject: [PATCH 254/432] OP-3283 - refactored logic Easier solution found without reinventing logic. --- openpype/hosts/photoshop/api/plugin.py | 53 ------------------- .../photoshop/plugins/create/create_image.py | 25 ++++----- .../plugins/create/create_legacy_image.py | 24 ++++----- 3 files changed, 20 insertions(+), 82 deletions(-) diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index ecbfbf91e3..c80e6bbd06 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -2,11 +2,6 @@ import re from openpype.pipeline import LoaderPlugin from .launch_logic import stub -from openpype.pipeline import legacy_io -from openpype.client import get_asset_by_name -from openpype.settings import get_project_settings -from openpype.lib import prepare_template_data -from openpype.lib.profiles_filtering import filter_profiles def get_unique_layer_name(layers, asset_name, subset_name): @@ -38,51 +33,3 @@ class PhotoshopLoader(LoaderPlugin): @staticmethod def get_stub(): return stub() - - -def get_subset_template(family): - """Get subset template name from Settings""" - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["data.tasks"] - ) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - tools_settings = get_project_settings(project_name)["global"]["tools"] - profiles = tools_settings["creator"]["subset_name_profiles"] - filtering_criteria = { - "families": family, - "hosts": "photoshop", - "tasks": task_name, - "task_types": task_type - } - - matching_profile = filter_profiles(profiles, filtering_criteria) - if matching_profile: - return matching_profile["template"] - - -def get_subset_name_for_multiple(subset_name, subset_template, group, - family, variant): - """Update subset name with layer information to differentiate multiple - - subset_template might contain specific way how to format layer name - ({layer},{Layer} or {LAYER}). If subset_template doesn't contain placeholder - at all, fall back to original solution. - """ - if not subset_template or 'layer' not in subset_template.lower(): - subset_name += group.name.title().replace(" ", "") - else: - fill_pairs = { - "family": family, - "variant": variant, - "task": legacy_io.Session["AVALON_TASK"], - "layer": group.name - } - - return subset_template.format(**prepare_template_data(fill_pairs)) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 2b6e5e6448..44a74de650 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -5,10 +5,7 @@ from openpype.pipeline import ( CreatedInstance, legacy_io ) -from openpype.hosts.photoshop.api.plugin import ( - get_subset_template, - get_subset_name_for_multiple -) +from openpype.lib import prepare_template_data class ImageCreator(Creator): @@ -71,6 +68,7 @@ class ImageCreator(Creator): group = stub.group_selected_layers(layer.name) groups_to_create.append(group) + layer_name = '' creating_multiple_groups = len(groups_to_create) > 1 for group in groups_to_create: subset_name = subset_name_from_ui # reset to name from creator UI @@ -78,13 +76,12 @@ class ImageCreator(Creator): created_group_name = self._clean_highlights(stub, group.name) if creating_multiple_groups: - # concatenate with layer name to differentiate subsets - subset_template = get_subset_template(self.family) - subset_name = get_subset_name_for_multiple(subset_name, - subset_template, - group, - self.family, - data["variant"]) + layer_name = group.name + if "{layer}" not in subset_name.lower(): + subset_name += "{Layer}" + + layer_fill = prepare_template_data({"layer": layer_name}) + subset_name = subset_name.format(**layer_fill) if group.long_name: for directory in group.long_name[::-1]: @@ -160,7 +157,5 @@ class ImageCreator(Creator): return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON, '') @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - return {"layer": ""} \ No newline at end of file + def get_dynamic_data(cls, *args, **kwargs): + return {"layer": "{layer}"} diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index d1a54a407e..e465c30abd 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -2,10 +2,7 @@ from Qt import QtWidgets from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop -from openpype.hosts.photoshop.api.plugin import ( - get_subset_template, - get_subset_name_for_multiple -) +from openpype.lib import prepare_template_data class CreateImage(create.LegacyCreator): @@ -80,6 +77,7 @@ class CreateImage(create.LegacyCreator): groups.append(group) creator_subset_name = self.data["subset"] + layer_name = '' for group in groups: long_names = [] group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ @@ -87,12 +85,12 @@ class CreateImage(create.LegacyCreator): subset_name = creator_subset_name if len(groups) > 1: - subset_template = get_subset_template(self.family) - subset_name = get_subset_name_for_multiple(subset_name, - subset_template, - group, - self.family, - self.data["variant"]) + layer_name = group.name + if "{layer}" not in subset_name.lower(): + subset_name += "{Layer}" + + layer_fill = prepare_template_data({"layer": layer_name}) + subset_name = subset_name.format(**layer_fill) if group.long_name: for directory in group.long_name[::-1]: @@ -110,7 +108,5 @@ class CreateImage(create.LegacyCreator): stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - return {"layer": ""} + def get_dynamic_data(cls, *args, **kwargs): + return {"layer": "{layer}"} From 4ac9ed6886ee455640e93c31fac510270ce571bd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 16:24:16 +0200 Subject: [PATCH 255/432] OP-3283 - fix invalid characters in subset name Removal of invalid characters must be done in Create phase to persist. --- openpype/hosts/photoshop/plugins/create/create_image.py | 9 ++++++++- .../photoshop/plugins/create/create_legacy_image.py | 9 ++++++++- .../hosts/photoshop/plugins/publish/validate_naming.py | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 44a74de650..2cfbfa8778 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,3 +1,5 @@ +import re + from openpype.hosts.photoshop import api from openpype.lib import BoolDef from openpype.pipeline import ( @@ -6,6 +8,7 @@ from openpype.pipeline import ( legacy_io ) from openpype.lib import prepare_template_data +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class ImageCreator(Creator): @@ -76,7 +79,11 @@ class ImageCreator(Creator): created_group_name = self._clean_highlights(stub, group.name) if creating_multiple_groups: - layer_name = group.name + layer_name = re.sub( + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), + "", + group.name + ) if "{layer}" not in subset_name.lower(): subset_name += "{Layer}" diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index e465c30abd..2792a775e0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -1,8 +1,11 @@ +import re + from Qt import QtWidgets from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop from openpype.lib import prepare_template_data +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class CreateImage(create.LegacyCreator): @@ -85,7 +88,11 @@ class CreateImage(create.LegacyCreator): subset_name = creator_subset_name if len(groups) > 1: - layer_name = group.name + layer_name = re.sub( + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), + "", + group.name + ) if "{layer}" not in subset_name.lower(): subset_name += "{Layer}" diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index b53f4e8198..8106d6ff16 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -4,6 +4,7 @@ import pyblish.api import openpype.api from openpype.pipeline import PublishXmlValidationError from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class ValidateNamingRepair(pyblish.api.Action): @@ -50,6 +51,13 @@ class ValidateNamingRepair(pyblish.api.Action): subset_name = re.sub(invalid_chars, replace_char, instance.data["subset"]) + # format from Tool Creator + subset_name = re.sub( + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), + "", + subset_name + ) + layer_meta["subset"] = subset_name stub.imprint(instance_id, layer_meta) From d63d0cfb6f40ba7a0cc5b6a0cb3cc8d3057da6ba Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 17:56:39 +0300 Subject: [PATCH 256/432] Remove incorrect code. This reverts commit a26fd8394c71f0f01552f20987ac6618747d1572. --- openpype/hosts/maya/api/menu.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index ed546ba7a8..c3ce8b0227 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,7 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile, get_current_project_settings +from openpype.api import BuildWorkfile from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -99,17 +99,11 @@ def install(): cmds.menuItem(divider=True) - render_settings_flag = get_current_project_settings()["maya"]["RenderSettings"]["apply_render_settings"] # noqa - if render_settings_flag: - cmds.menuItem( - "Set Render Settings", - command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings(), # noqa - enable=True) - else: - cmds.menuItem( - "Set Render Settings", - command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings(), # noqa - enable=False) + cmds.menuItem( + "Set Render Settings", + command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings() # noqa + ) + cmds.menuItem(divider=True) cmds.menuItem( From 5c8eac6b6357fa80859ffbed45be41cf8ae106da Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:07:57 +0200 Subject: [PATCH 257/432] OP-3405 - replaced find with get_representations --- .../modules/sync_server/sync_server_module.py | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 4027561d22..81aff9368f 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -25,6 +25,8 @@ from .providers import lib from .utils import time_function, SyncStatus, SiteAlreadyPresentError +from openpype.client import get_representations + log = PypeLogger.get_logger("SyncServer") @@ -344,6 +346,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): "files.sites.name": site_name } + # TODO currently not possible to replace with get_representations representations = list( self.connection.database[collection].find(query)) if not representations: @@ -391,12 +394,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ self.log.debug("Validation of {} for {} started".format(collection, site_name)) - query = { - "type": "representation" - } - - representations = list( - self.connection.database[collection].find(query)) + representations = list(get_representations(collection)) if not representations: self.log.debug("No repre found") return @@ -1593,14 +1591,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): not 'force' ValueError - other errors (repre not found, misconfiguration) """ - query = { - "_id": ObjectId(representation_id) - } - - representation = self.connection.database[collection].find_one(query) - if not representation: + representations = get_representations(collection, [representation_id]) + if not representations: raise ValueError("Representation {} not found in {}". format(representation_id, collection)) + representation = representations[0] if side and site_name: raise ValueError("Misconfiguration, only one of side and " + "site_name arguments should be passed.") @@ -1808,18 +1803,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): provider_name = self.get_provider_for_site(site=site_name) if provider_name == 'local_drive': - query = { - "_id": ObjectId(representation_id) - } - - representation = list( - self.connection.database[collection].find(query)) - if not representation: + representations = list(get_representations(collection, + [representation_id], + fields=["files"])) + if not representations: self.log.debug("No repre {} found".format( representation_id)) return - representation = representation.pop() + representation = representations.pop() local_file_path = '' for file in representation.get("files"): local_file_path = self.get_local_file_path(collection, From c944ae35c9848045cfb73ccfc1b93f30f7af2989 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:17:03 +0200 Subject: [PATCH 258/432] OP-3405 - replaced find with get_representation_by_id --- openpype/modules/sync_server/tray/models.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 6d1e85c17a..a97797c920 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -11,6 +11,7 @@ from openpype.tools.utils.delegates import pretty_timestamp from openpype.lib import PypeLogger from openpype.api import get_local_site_id +from openpype.client import get_representation_by_id from . import lib @@ -919,8 +920,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): repre_id = self.data(index, Qt.UserRole) - representation = list(self.dbcon.find({"type": "representation", - "_id": repre_id})) + representation = get_representation_by_id(self.project, repre_id) if representation: self.sync_server.update_db(self.project, None, None, representation.pop(), @@ -1357,11 +1357,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): file_id = self.data(index, Qt.UserRole) updated_file = None - # conversion from cursor to list - representations = list(self.dbcon.find({"type": "representation", - "_id": self._id})) + representation = get_representation_by_id(self.project, self._id) + if not representation: + return - representation = representations.pop() for repre_file in representation["files"]: if repre_file["_id"] == file_id: updated_file = repre_file From 292d071f442a494cabd2161512012b13e391a9f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:39:59 +0200 Subject: [PATCH 259/432] OP-3405 - query is required for updates --- openpype/modules/sync_server/sync_server_module.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 81aff9368f..6a3dbf6095 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1611,6 +1611,10 @@ class SyncServerModule(OpenPypeModule, ITrayModule): elem = {"name": site_name} + query = { + "_id": ObjectId(representation_id) + } + if file_id: # reset site for particular file self._reset_site_for_file(collection, query, elem, file_id, site_name) From 0f5ec0f0c4cbd4db8c4968db75f6375b6bdf7f59 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:54:51 +0200 Subject: [PATCH 260/432] OP-3405 - used get_representation_by_id --- .../modules/sync_server/sync_server_module.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 6a3dbf6095..71e35c7839 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -25,7 +25,7 @@ from .providers import lib from .utils import time_function, SyncStatus, SiteAlreadyPresentError -from openpype.client import get_representations +from openpype.client import get_representations, get_representation_by_id log = PypeLogger.get_logger("SyncServer") @@ -1591,11 +1591,12 @@ class SyncServerModule(OpenPypeModule, ITrayModule): not 'force' ValueError - other errors (repre not found, misconfiguration) """ - representations = get_representations(collection, [representation_id]) - if not representations: + representation = get_representation_by_id(collection, + representation_id) + if not representation: raise ValueError("Representation {} not found in {}". format(representation_id, collection)) - representation = representations[0] + if side and site_name: raise ValueError("Misconfiguration, only one of side and " + "site_name arguments should be passed.") @@ -1807,15 +1808,14 @@ class SyncServerModule(OpenPypeModule, ITrayModule): provider_name = self.get_provider_for_site(site=site_name) if provider_name == 'local_drive': - representations = list(get_representations(collection, - [representation_id], - fields=["files"])) - if not representations: + representation = get_representation_by_id(collection, + representation_id, + fields=["files"]) + if not representation: self.log.debug("No repre {} found".format( representation_id)) return - representation = representations.pop() local_file_path = '' for file in representation.get("files"): local_file_path = self.get_local_file_path(collection, From ccdff822a54c6bf146ad1a8a9b2206c319967719 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 18:11:11 +0200 Subject: [PATCH 261/432] moved 'get_project_database' and 'get_project_connection' to mongo --- openpype/client/entities.py | 30 ++---------------------------- openpype/client/mongo.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index dd5d831ecf..0e94b99ae6 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -6,38 +6,12 @@ that has project name as a context (e.g. on 'ProjectEntity'?). + We will need more specific functions doing wery specific queires really fast. """ -import os import collections import six from bson.objectid import ObjectId -from .mongo import OpenPypeMongoConnection - - -def _get_project_database(): - db_name = os.environ.get("AVALON_DB") or "avalon" - return OpenPypeMongoConnection.get_mongo_client()[db_name] - - -def get_project_connection(project_name): - """Direct access to mongo collection. - - We're trying to avoid using direct access to mongo. This should be used - only for Create, Update and Remove operations until there are implemented - api calls for that. - - Args: - project_name(str): Project name for which collection should be - returned. - - Returns: - pymongo.Collection: Collection realated to passed project. - """ - - if not project_name: - raise ValueError("Invalid project name {}".format(str(project_name))) - return _get_project_database()[project_name] +from .mongo import get_project_database, get_project_connection def _prepare_fields(fields, required_fields=None): @@ -72,7 +46,7 @@ def _convert_ids(in_ids): def get_projects(active=True, inactive=False, fields=None): - mongodb = _get_project_database() + mongodb = get_project_database() for project_name in mongodb.collection_names(): if project_name in ("system.indexes",): continue diff --git a/openpype/client/mongo.py b/openpype/client/mongo.py index a747250107..72acbc5476 100644 --- a/openpype/client/mongo.py +++ b/openpype/client/mongo.py @@ -208,3 +208,28 @@ class OpenPypeMongoConnection: mongo_url, time.time() - t1 )) return mongo_client + + +def get_project_database(): + db_name = os.environ.get("AVALON_DB") or "avalon" + return OpenPypeMongoConnection.get_mongo_client()[db_name] + + +def get_project_connection(project_name): + """Direct access to mongo collection. + + We're trying to avoid using direct access to mongo. This should be used + only for Create, Update and Remove operations until there are implemented + api calls for that. + + Args: + project_name(str): Project name for which collection should be + returned. + + Returns: + pymongo.Collection: Collection realated to passed project. + """ + + if not project_name: + raise ValueError("Invalid project name {}".format(str(project_name))) + return get_project_database()[project_name] From c429a41188c614570e9d1d39cd6605897fbfaf38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 18:12:43 +0200 Subject: [PATCH 262/432] added initial variant of operations --- openpype/client/operations.py | 249 ++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 openpype/client/operations.py diff --git a/openpype/client/operations.py b/openpype/client/operations.py new file mode 100644 index 0000000000..365833b318 --- /dev/null +++ b/openpype/client/operations.py @@ -0,0 +1,249 @@ +import uuid +import copy +from abc import ABCMeta, abstractmethod + +import six +from bson.objectid import ObjectId +from pymongo import DeleteOne, InsertOne, UpdateOne + +from .mongo import get_project_connection + +REMOVED_VALUE = object() + + +@six.add_metaclass(ABCMeta) +class AbstractOperation(object): + """Base operation class.""" + + def __init__(self, entity_type): + self._entity_type = entity_type + self._id = uuid.uuid4() + + @property + def id(self): + return self._id + + @property + def entity_type(self): + return self._entity_type + + @abstractmethod + def to_mongo_operation(self): + pass + + +class CreateOperation(AbstractOperation): + def __init__(self, project_name, entity_type, data): + super(CreateOperation, self).__init__(entity_type) + + if not data: + data = {} + else: + data = copy.deepcopy(dict(data)) + + if "_id" not in data: + data["_id"] = ObjectId() + else: + data["_id"] = ObjectId(data["_id"]) + + self._entity_id = data["_id"] + self._data = data + + def __setitem__(self, key, value): + self.set_value(key, value) + + def __getitem__(self, key): + return self.data[key] + + def set_value(self, key, value): + self.data[key] = value + + def get(self, key, *args, **kwargs): + return self.data.get(key, *args, **kwargs) + + @property + def entity_id(self): + return self._entity_id + + @property + def data(self): + return self._data + + def to_mongo_operation(self): + return InsertOne(copy.deepcopy(self._data)) + + def to_data(self): + return { + "operation": "create", + "entity_type": self.entity_type, + "data": copy.deepcopy(self.data) + } + + +class UpdateOperation(AbstractOperation): + def __init__(self, project_name, entity_type, entity_id, update_fields): + super(CreateOperation, self).__init__(entity_type) + + self._entity_id = ObjectId(entity_id) + self._update_fields = update_fields + + @property + def entity_id(self): + return self._entity_id + + @property + def update_fields(self): + return self._update_fields + + def to_mongo_operation(self): + unset_data = {} + set_data = {} + for key, value in self._update_fields.items(): + if value is REMOVED_VALUE: + unset_data[key] = value + else: + set_data[key] = value + + op_data = {} + if unset_data: + op_data["$unset"] = unset_data + if set_data: + op_data["$set"] = set_data + + if not op_data: + return None + + return UpdateOne( + {"_id": self.entity_id}, + op_data + ) + + def to_data(self): + fields = {} + for key, value in self._update_fields.items(): + if value is REMOVED_VALUE: + value = None + fields[key] = value + + return { + "operation": "update", + "entity_type": self.entity_type, + "entity_id": str(self.entity_id), + "fields": fields + } + + +class DeleteOperation(AbstractOperation): + def __init__(self, entity_type, entity_id): + super(DeleteOperation, self).__init__(entity_type) + + self._entity_id = ObjectId(entity_id) + + @property + def entity_id(self): + return self._entity_id + + def to_mongo_operation(self): + return DeleteOne({"_id": self.entity_id}) + + def to_data(self): + return { + "operation": "delete", + "entity_type": self.entity_type, + "entity_id": str(self.entity_id) + } + + +class OperationsSession(object): + """Session storing operations that should happen in an order. + + At this moment does not handle anything special can be sonsidered as + stupid list of operations that will happen after each other. If creation + of same entity is there multiple times it's handled in any way and document + values are not validated. + + All operations must be related to single project. + + Args: + project_name (str): Project name to which are operations related. + """ + + def __init__(self, project_name): + self._project_name = project_name + self._operations = [] + + @property + def project_name(self): + return self._project_name + + def add(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + if not isinstance( + operation, + (CreateOperation, UpdateOperation, DeleteOperation) + ): + raise TypeError("Expected Operation object got {}".format( + str(type(operation)) + )) + + self._operations.append(operation) + + def append(self, operation): + """Add operation to be processed. + + Args: + operation (BaseOperation): Operation that should be processed. + """ + + self.add(operation) + + def extend(self, operations): + """Add operations to be processed. + + Args: + operations (List[BaseOperation]): Operations that should be + processed. + """ + + for operation in operations: + self.add(operation) + + def remove(self, operation): + """Remove operation.""" + + self._operations.remove(operation) + + def clear(self): + """Clear all registered operations.""" + + self._operations = [] + + def to_data(self): + return { + "project_name": self.project_name, + "operations": [ + operation.to_data() + for operation in self._operations + ] + } + + def commit(self): + """Commit session operations.""" + + operations, self._operations = self._operations, [] + if not operations: + return + + bulk_writes = [] + for operation in operations: + mongo_op = operation.to_mongo_operation() + if mongo_op is not None: + bulk_writes.append(mongo_op) + + if bulk_writes: + collection = get_project_connection(self.project_name) + collection.bulk_write(bulk_writes) From 7d23558ac038be8e951682f0f945b6c58a8717b0 Mon Sep 17 00:00:00 2001 From: Felix David Date: Fri, 29 Jul 2022 18:27:52 +0200 Subject: [PATCH 263/432] Kitsu|Fix: Collect entities error cause of Python2 Fix #3552 --- .../modules/kitsu/plugins/publish/collect_kitsu_entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index d28ded06c7..d2a6f3f303 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -39,10 +39,10 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entity = gazu.asset.get_asset(zou_asset_data["id"]) if not kitsu_entity: - raise AssertionError(f"{entity_type} not found in kitsu!") + raise AssertionError("{} not found in kitsu!".format(entity_type)) context.data["kitsu_entity"] = kitsu_entity - self.log.debug(f"Collect kitsu {entity_type}: {kitsu_entity}") + self.log.debug("Collect kitsu {}: {}".format(entity_type, kitsu_entity)) if zou_task_data: kitsu_task = gazu.task.get_task(zou_task_data["id"]) From 49af7d9d2dad8c8deec3226cbb50c4a8ecd38694 Mon Sep 17 00:00:00 2001 From: Felix David Date: Fri, 29 Jul 2022 18:47:32 +0200 Subject: [PATCH 264/432] black line length --- .../modules/kitsu/plugins/publish/collect_kitsu_entities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index d2a6f3f303..c9e78b59eb 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -42,7 +42,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): raise AssertionError("{} not found in kitsu!".format(entity_type)) context.data["kitsu_entity"] = kitsu_entity - self.log.debug("Collect kitsu {}: {}".format(entity_type, kitsu_entity)) + self.log.debug( + "Collect kitsu {}: {}".format(entity_type, kitsu_entity) + ) if zou_task_data: kitsu_task = gazu.task.get_task(zou_task_data["id"]) From f9f53fe19c68302dc90362c668bb5bededf93e36 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:08:15 +0200 Subject: [PATCH 265/432] add missing method which was resolved as part of HiddenCreator --- openpype/hosts/traypublisher/api/plugin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index a0c42a55b1..a3eead51c8 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -92,6 +92,21 @@ class TrayPublishCreator(Creator): for instance in instances: self._remove_instance_from_context(instance) + def _store_new_instance(self, new_instance): + """Tray publisher specific method to store instance. + + Instance is stored into "workfile" of traypublisher and also add it + to CreateContext. + + Args: + new_instance (CreatedInstance): Instance that should be stored. + """ + + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + class SettingsCreator(TrayPublishCreator): create_allow_context_change = True From c0de0d5b89654f38a95f779af7b7e450bf58a5ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:08:46 +0200 Subject: [PATCH 266/432] use '_store_new_instance' in editorial creators --- .../plugins/create/create_editorial.py | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 3bc8f89556..7ca68f39e8 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -75,20 +75,13 @@ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): self.log.info(f"instance_data: {instance_data}") subset_name = instance_data["subset"] - return self._create_instance(subset_name, instance_data) - - def _create_instance(self, subset_name, data): - # Create new instance - new_instance = CreatedInstance(self.family, subset_name, data, self) + new_instance = CreatedInstance( + self.family, subset_name, instance_data, self + ) self.log.info(f"instance_data: {pformat(new_instance.data)}") - # Host implementation of storing metadata about instance - HostContext.add_instance(new_instance.data_to_store()) - # Add instance to current context - self._add_instance_to_context(new_instance) - - return new_instance + self._store_new_instance(new_instance) def get_instance_attr_defs(self): return [ @@ -299,8 +292,10 @@ or updating already created. Publishing will create OTIO file. "editorialSourcePath": media_path, "otioTimeline": otio.adapters.write_to_string(otio_timeline) }) - - self._create_instance(self.family, subset_name, data) + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._store_new_instance(new_instance) def _create_otio_timeline(self, sequence_path, fps): """Creating otio timeline from sequence path @@ -820,23 +815,6 @@ or updating already created. Publishing will create OTIO file. "Please check names in the input sequence files." ) - def _create_instance(self, family, subset_name, instance_data): - """ CreatedInstance object creator - - Args: - family (str): family name - subset_name (str): subset name - instance_data (dict): instance data - """ - # Create new instance - new_instance = CreatedInstance( - family, subset_name, instance_data, self - ) - # Host implementation of storing metadata about instance - HostContext.add_instance(new_instance.data_to_store()) - # Add instance to current context - self._add_instance_to_context(new_instance) - def get_pre_create_attr_defs(self): """ Creating pre-create attributes at creator plugin. From 441f2269d4a3db6a5b8cbb5023d386eb1fee143d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:10:13 +0200 Subject: [PATCH 267/432] removed unused import --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 7ca68f39e8..e9bca79b31 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -29,8 +29,6 @@ from openpype.lib import ( UILabelDef ) -from openpype.hosts.traypublisher.api.pipeline import HostContext - CLIP_ATTR_DEFS = [ EnumDef( From a11ef9f346b1b410ae99483dad3bb53cd187b084 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 20:20:20 +0300 Subject: [PATCH 268/432] Append frame reset feature, handle prefix key properly --- openpype/hosts/maya/api/lib_rendersettings.py | 18 +++++++++++++----- .../defaults/project_settings/maya.json | 3 ++- .../schemas/schema_maya_render_settings.json | 7 ++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 8c09175614..7eae5bbbbf 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -66,12 +66,20 @@ class RenderSettings(object): )] except KeyError: aov_separator = "_" + reset_frame = self._project_settings["maya"]["RenderSettings"]["reset_current_frame"] # noqa - prefix = self._image_prefixes[renderer] - prefix = prefix.replace("{aov_separator}", aov_separator) - cmds.setAttr(self._image_prefix_nodes[renderer], - prefix, - type="string") + if reset_frame: + start_frame = cmds.getAttr("defaultRenderGlobals.startFrame") + cmds.currentTime(start_frame, edit=True) + + if renderer in self._image_prefix_nodes: + prefix = self._image_prefixes[renderer] + prefix = prefix.replace("{aov_separator}", aov_separator) + cmds.setAttr(self._image_prefix_nodes[renderer], + prefix, + type="string") + else: + print("{0} isn't a supported renderer to autoset settings.".format(renderer)) # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 6e50f13418..5f11072b12 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -35,6 +35,7 @@ "apply_render_settings": true, "default_render_image_folder": "", "aov_separator": "underscore", + "reset_current_frame": false, "arnold_renderer": { "image_prefix": "maya///_", "image_format": "exr", @@ -973,4 +974,4 @@ "ValidateNoAnimation": false } } -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 96b67dc66a..9b6b6f1eed 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -26,6 +26,11 @@ {"dot": ". (dot)"} ] }, + { + "key": "reset_current_frame", + "label": "Reset Current Frame", + "type": "boolean" + }, { "type": "dict", "collapsible": true, @@ -408,4 +413,4 @@ ] } ] -} \ No newline at end of file +} From 9967cd0d0aec49122b24ed7a6b388c832e845ca4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 20:20:43 +0300 Subject: [PATCH 269/432] Append settings propagation to render instance creator. --- openpype/hosts/maya/plugins/create/create_render.py | 3 ++- vendor/configs/OpenColorIO-Configs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index d4ad488b32..395984aee0 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -87,7 +87,8 @@ class CreateRender(plugin.Creator): return self._project_settings = get_project_settings( legacy_io.Session["AVALON_PROJECT"]) - + if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: + lib_rendersettings.RenderSettings().set_default_renderer_settings() manager = ModulesManager() self.deadline_module = manager.modules_by_name["deadline"] try: diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 0000000000..0bb079c08b --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From cc5abb15142a7c9d31d5602ba6434f9f534a670e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:20:47 +0200 Subject: [PATCH 270/432] few minor modifications and changes --- openpype/client/operations.py | 125 +++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 46 deletions(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index 365833b318..517a53c27f 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -15,9 +15,14 @@ REMOVED_VALUE = object() class AbstractOperation(object): """Base operation class.""" - def __init__(self, entity_type): + def __init__(self, project_name, entity_type): + self._project_name = project_name self._entity_type = entity_type - self._id = uuid.uuid4() + self._id = str(uuid.uuid4()) + + @property + def project_name(self): + return self._project_name @property def id(self): @@ -27,14 +32,28 @@ class AbstractOperation(object): def entity_type(self): return self._entity_type + @abstractproperty + def operation_name(self): + pass + @abstractmethod def to_mongo_operation(self): pass + def to_data(self): + return { + "id": self._id, + "entity_type": self.entity_type, + "project_name": self.project_name, + "operation": self.operation_name + } + class CreateOperation(AbstractOperation): + operation_name = "create" + def __init__(self, project_name, entity_type, data): - super(CreateOperation, self).__init__(entity_type) + super(CreateOperation, self).__init__(project_name, entity_type) if not data: data = {} @@ -73,32 +92,32 @@ class CreateOperation(AbstractOperation): return InsertOne(copy.deepcopy(self._data)) def to_data(self): - return { - "operation": "create", - "entity_type": self.entity_type, - "data": copy.deepcopy(self.data) - } + output = super(CreateOperation, self).to_data() + output["data"] = copy.deepcopy(self.data) + return output class UpdateOperation(AbstractOperation): - def __init__(self, project_name, entity_type, entity_id, update_fields): - super(CreateOperation, self).__init__(entity_type) + operation_name = "update" + + def __init__(self, project_name, entity_type, entity_id, update_data): + super(UpdateOperation, self).__init__(project_name, entity_type) self._entity_id = ObjectId(entity_id) - self._update_fields = update_fields + self._update_data = update_data @property def entity_id(self): return self._entity_id @property - def update_fields(self): - return self._update_fields + def update_data(self): + return self._update_data def to_mongo_operation(self): unset_data = {} set_data = {} - for key, value in self._update_fields.items(): + for key, value in self._update_data.items(): if value is REMOVED_VALUE: unset_data[key] = value else: @@ -120,22 +139,24 @@ class UpdateOperation(AbstractOperation): def to_data(self): fields = {} - for key, value in self._update_fields.items(): + for key, value in self._update_data.items(): if value is REMOVED_VALUE: value = None fields[key] = value - return { - "operation": "update", - "entity_type": self.entity_type, + output = super(UpdateOperation, self).to_data() + output.update({ "entity_id": str(self.entity_id), "fields": fields - } + }) + return output class DeleteOperation(AbstractOperation): - def __init__(self, entity_type, entity_id): - super(DeleteOperation, self).__init__(entity_type) + operation_name = "delete" + + def __init__(self, project_name, entity_type, entity_id): + super(DeleteOperation, self).__init__(project_name, entity_type) self._entity_id = ObjectId(entity_id) @@ -147,11 +168,9 @@ class DeleteOperation(AbstractOperation): return DeleteOne({"_id": self.entity_id}) def to_data(self): - return { - "operation": "delete", - "entity_type": self.entity_type, - "entity_id": str(self.entity_id) - } + output = super(DeleteOperation, self).to_data() + output["entity_id"] = self.entity_id + return output class OperationsSession(object): @@ -168,14 +187,9 @@ class OperationsSession(object): project_name (str): Project name to which are operations related. """ - def __init__(self, project_name): - self._project_name = project_name + def __init__(self): self._operations = [] - @property - def project_name(self): - return self._project_name - def add(self, operation): """Add operation to be processed. @@ -223,13 +237,10 @@ class OperationsSession(object): self._operations = [] def to_data(self): - return { - "project_name": self.project_name, - "operations": [ - operation.to_data() - for operation in self._operations - ] - } + return [ + operation.to_data() + for operation in self._operations + ] def commit(self): """Commit session operations.""" @@ -238,12 +249,34 @@ class OperationsSession(object): if not operations: return - bulk_writes = [] + operations_by_project = collections.defaultdict(list) for operation in operations: - mongo_op = operation.to_mongo_operation() - if mongo_op is not None: - bulk_writes.append(mongo_op) + operations_by_project[operation.project_name].append(operation) - if bulk_writes: - collection = get_project_connection(self.project_name) - collection.bulk_write(bulk_writes) + for project_name, operations in operations_by_project.items(): + bulk_writes = [] + for operation in operations: + mongo_op = operation.to_mongo_operation() + if mongo_op is not None: + bulk_writes.append(mongo_op) + + if bulk_writes: + collection = get_project_connection(project_name) + collection.bulk_write(bulk_writes) + + def create_entity(self, project_name, entity_type, data): + operation = CreateOperation(project_name, entity_type, data) + self.add(operation) + return operation + + def update_entity(self, project_name, entity_type, entity_id, update_data): + operation = UpdateOperation( + project_name, entity_type, entity_id, update_data + ) + self.add(operation) + return operation + + def delete_entity(self, project_name, entity_type, entity_id): + operation = DeleteOperation(project_name, entity_type, entity_id) + self.add(operation) + return operation From f39623d99138bee79021e87f476c7abca14e1bb2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:22:14 +0200 Subject: [PATCH 271/432] added helper functions to create new documents --- openpype/client/operations.py | 126 +++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index 517a53c27f..db3071abef 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -1,6 +1,8 @@ +import re import uuid import copy -from abc import ABCMeta, abstractmethod +import collections +from abc import ABCMeta, abstractmethod, abstractproperty import six from bson.objectid import ObjectId @@ -10,6 +12,128 @@ from .mongo import get_project_connection REMOVED_VALUE = object() +CURRENT_PROJECT_SCHEMA = "openpype:project-3.0" +CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0" +CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" +CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" +CURRENT_VERSION_SCHEMA = "openpype:version-3.0" +CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" + + +def _create_or_convert_to_mongo_id(mongo_id): + if mongo_id is None: + return ObjectId() + return ObjectId(mongo_id) + + +def new_project_document( + project_name, project_code, config, data=None, entity_id=None +): + if data is None: + data = {} + + data["code"] = project_code + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "name": project_name, + "type": CURRENT_PROJECT_SCHEMA, + "data": data, + "config": config + } + + +def new_asset_document( + name, project_id, parent_id, parents, data=None, entity_id=None +): + if data is None: + data = {} + if parent_id is not None: + parent_id = ObjectId(parent_id) + data["visualParent"] = parent_id + data["parents"] = parents + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "asset", + "name": name, + "parent": ObjectId(project_id), + "data": data, + "schema": CURRENT_ASSET_DOC_SCHEMA + } + + +def new_subset_document(name, family, asset_id, data=None, entity_id=None): + if data is None: + data = {} + data["family"] = family + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_SUBSET_SCHEMA, + "type": "subset", + "name": name, + "data": data, + "parent": asset_id + } + + +def new_version_doc(version, subset_id, data=None, entity_id=None): + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_VERSION_SCHEMA, + "type": "version", + "name": int(version), + "parent": subset_id, + "data": data + } + + +def new_representation_doc( + name, version_id, context, data=None, entity_id=None +): + if data is None: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "schema": CURRENT_REPRESENTATION_SCHEMA, + "type": "representation", + "parent": version_id, + "name": name, + "data": data, + + # Imprint shortcut to context for performance reasons. + "context": context + } + + +def _prepare_update_data(old_doc, new_doc, replace): + changes = {} + for key, value in new_doc.items(): + if key not in old_doc or value != old_doc[key]: + changes[key] = value + + if replace: + for key in old_doc.keys(): + if key not in new_doc: + changes[key] = REMOVED_VALUE + return changes + + +def prepare_subset_update_data(old_doc, new_doc, replace=True): + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_version_update_data(old_doc, new_doc, replace=True): + return _prepare_update_data(old_doc, new_doc, replace) + + +def prepare_representation_update_data(old_doc, new_doc, replace=True): + return _prepare_update_data(old_doc, new_doc, replace) + @six.add_metaclass(ABCMeta) class AbstractOperation(object): From 8b482a0a1f88f7c9931b8ce4f5ad08096c7f896a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 19:22:54 +0200 Subject: [PATCH 272/432] update oprations in integrator --- openpype/plugins/publish/integrate.py | 176 ++++++++++++++------------ 1 file changed, 98 insertions(+), 78 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index d817595888..b7d48fe9cf 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -5,8 +5,16 @@ import copy import clique import six +from openpype.client.operations import ( + OperationsSession, + new_subset_document, + new_version_doc, + new_representation_doc, + prepare_subset_update_data, + prepare_version_update_data, + prepare_representation_update_data, +) from bson.objectid import ObjectId -from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api from openpype.client import ( @@ -282,9 +290,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) - subset, subset_writes = self.prepare_subset(instance, project_name) - version, version_writes = self.prepare_version( - instance, subset, project_name + op_session = OperationsSession() + subset = self.prepare_subset( + instance, op_session, project_name + ) + version = self.prepare_version( + instance, op_session, subset, project_name ) instance.data["versionEntity"] = version @@ -334,7 +345,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Transaction to reduce the chances of another publish trying to # publish to the same version number since that chance can greatly # increase if the file transaction takes a long time. - legacy_io.bulk_write(subset_writes + version_writes) + op_session.commit() + self.log.info("Subset {subset[name]} and Version {version[name]} " "written to database..".format(subset=subset, version=version)) @@ -366,49 +378,49 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources - representation_writes = [] new_repre_names_low = set() for prepared in prepared_representations: - representation = prepared["representation"] + repre_doc = prepared["representation"] + repre_update_data = prepared["repre_doc_update_data"] transfers = prepared["transfers"] destinations = [dst for src, dst in transfers] - representation["files"] = self.get_files_info( + repre_doc["files"] = self.get_files_info( destinations, sites=sites, anatomy=anatomy ) # Add the version resource file infos to each representation - representation["files"] += resource_file_infos + repre_doc["files"] += resource_file_infos # Set up representation for writing to the database. Since # we *might* be overwriting an existing entry if the version # already existed we'll use ReplaceOnce with `upsert=True` - representation_writes.append(ReplaceOne( - filter={"_id": representation["_id"]}, - replacement=representation, - upsert=True - )) + if repre_update_data is None: + op_session.create_entity( + project_name, repre_doc["type"], repre_doc + ) + else: + op_session.update_entity( + project_name, + repre_doc["type"], + repre_doc["_id"], + repre_update_data + ) - new_repre_names_low.add(representation["name"].lower()) + new_repre_names_low.add(repre_doc["name"].lower()) # Delete any existing representations that didn't get any new data # if the instance is not set to append mode if not instance.data.get("append", False): - delete_names = set() for name, existing_repres in existing_repres_by_name.items(): if name not in new_repre_names_low: # We add the exact representation name because `name` is # lowercase for name matching only and not in the database - delete_names.add(existing_repres["name"]) - if delete_names: - representation_writes.append(DeleteMany( - filter={ - "parent": version["_id"], - "name": {"$in": list(delete_names)} - } - )) + op_session.delete_entity( + project_name, "representation", existing_repres["_id"] + ) - # Write representations to the database - legacy_io.bulk_write(representation_writes) + self.log.debug("{}".format(op_session.to_data())) + op_session.commit() # Backwards compatibility # todo: can we avoid the need to store this? @@ -419,13 +431,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.info("Registered {} representations" "".format(len(prepared_representations))) - def prepare_subset(self, instance, project_name): + def prepare_subset(self, instance, op_session, project_name): asset_doc = instance.data["assetEntity"] subset_name = instance.data["subset"] + family = instance.data["family"] self.log.debug("Subset: {}".format(subset_name)) # Get existing subset if it exists - subset_doc = get_subset_by_name( + existing_subset_doc = get_subset_by_name( project_name, subset_name, asset_doc["_id"] ) @@ -438,69 +451,79 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if subset_group: data["subsetGroup"] = subset_group - bulk_writes = [] - if subset_doc is None: + subset_id = None + if existing_subset_doc: + subset_id = existing_subset_doc["_id"] + subset_doc = new_subset_document( + subset_name, family, asset_doc["_id"], data, subset_id + ) + + if existing_subset_doc is None: # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset_doc = { - "_id": ObjectId(), - "schema": "openpype:subset-3.0", - "type": "subset", - "name": subset_name, - "data": data, - "parent": asset_doc["_id"] - } - bulk_writes.append(InsertOne(subset_doc)) + op_session.create_entity( + project_name, subset_doc["type"], subset_doc + ) else: # Update existing subset data with new data and set in database. # We also change the found subset in-place so we don't need to # re-query the subset afterwards subset_doc["data"].update(data) - bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset_doc["_id"]}, - {"$set": { - "data": subset_doc["data"] - }} - )) + update_data = prepare_subset_update_data( + existing_subset_doc, subset_doc + ) + op_session.update_entity( + project_name, + subset_doc["type"], + subset_doc["_id"], + update_data + ) self.log.info("Prepared subset: {}".format(subset_name)) - return subset_doc, bulk_writes + return subset_doc - def prepare_version(self, instance, subset_doc, project_name): + def prepare_version(self, instance, op_session, subset_doc, project_name): version_number = instance.data["version"] - version_doc = { - "schema": "openpype:version-3.0", - "type": "version", - "parent": subset_doc["_id"], - "name": version_number, - "data": self.create_version_data(instance) - } - existing_version = get_version_by_name( project_name, version_number, subset_doc["_id"], fields=["_id"] ) + version_id = None + if existing_version: + version_id = existing_version["_id"] + + version_data = self.create_version_data(instance) + version_doc = new_version_doc( + version_number, + subset_doc["_id"], + version_data, + version_id + ) if existing_version: self.log.debug("Updating existing version ...") - version_doc["_id"] = existing_version["_id"] + update_data = prepare_version_update_data( + existing_version, version_doc + ) + op_session.update_entity( + project_name, + version_doc["type"], + version_doc["_id"], + update_data + ) else: self.log.debug("Creating new version ...") - version_doc["_id"] = ObjectId() - - bulk_writes = [ReplaceOne( - filter={"_id": version_doc["_id"]}, - replacement=version_doc, - upsert=True - )] + op_session.create_entity( + project_name, version_doc["type"], version_doc + ) self.log.info("Prepared version: v{0:03d}".format(version_doc["name"])) - return version_doc, bulk_writes + return version_doc def prepare_representation(self, repre, template_name, @@ -696,10 +719,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Use previous representation's id if there is a name match existing = existing_repres_by_name.get(repre["name"].lower()) + repre_id = None if existing: repre_id = existing["_id"] - else: - repre_id = ObjectId() # Store first transferred destination as published path data # - used primarily for reviews that are integrated to custom modules @@ -713,20 +735,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # and the actual representation entity for the database data = repre.get("data", {}) data.update({"path": published_path, "template": template}) - representation = { - "_id": repre_id, - "schema": "openpype:representation-2.0", - "type": "representation", - "parent": version["_id"], - "name": repre["name"], - "data": data, - - # Imprint shortcut to context for performance reasons. - "context": repre_context - } + repre_doc = new_representation_doc( + repre["name"], version["_id"], repre_context, data, repre_id + ) + update_data = None + if repre_id is not None: + update_data = prepare_representation_update_data( + existing, repre_doc + ) return { - "representation": representation, + "representation": repre_doc, + "repre_doc_update_data": update_data, "anatomy_data": template_data, "transfers": transfers, # todo: avoid the need for 'published_files' used by Integrate Hero From 1f126977fa52d55c9874ae87f3f2b7494ae8eeb2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 21:07:29 +0300 Subject: [PATCH 273/432] Style fixes. --- openpype/hosts/maya/api/lib_rendersettings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 7eae5bbbbf..6154e1ab89 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -76,10 +76,9 @@ class RenderSettings(object): prefix = self._image_prefixes[renderer] prefix = prefix.replace("{aov_separator}", aov_separator) cmds.setAttr(self._image_prefix_nodes[renderer], - prefix, - type="string") + prefix, type="string") # noqa else: - print("{0} isn't a supported renderer to autoset settings.".format(renderer)) + print("{0} isn't a supported renderer to autoset settings.".format(renderer)) # noqa # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") From 487830fbbbedb783375fd9c9eee58e4c4cfb2841 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 21:08:11 +0300 Subject: [PATCH 274/432] Style fix --- openpype/hosts/maya/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 395984aee0..fbe670b1ea 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -87,7 +87,7 @@ class CreateRender(plugin.Creator): return self._project_settings = get_project_settings( legacy_io.Session["AVALON_PROJECT"]) - if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: + if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: # noqa lib_rendersettings.RenderSettings().set_default_renderer_settings() manager = ModulesManager() self.deadline_module = manager.modules_by_name["deadline"] From 3426bd74d89c7dfb71de0b2adf1fc06078fc763c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 30 Jul 2022 04:04:29 +0000 Subject: [PATCH 275/432] [Automated] Bump version --- CHANGELOG.md | 48 ++++++++++++++++++++++++++------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4fc1d59ca..eab4e5e45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,34 @@ # Changelog +## [3.12.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...HEAD) + +**🆕 New features** + +- Traypublisher: simple editorial publishing [\#3492](https://github.com/pypeclub/OpenPype/pull/3492) + +**🚀 Enhancements** + +- Kitsu: Shot&Sequence name with prefix over appends [\#3593](https://github.com/pypeclub/OpenPype/pull/3593) +- Ftrack: Update ftrack api to 2.3.3 [\#3588](https://github.com/pypeclub/OpenPype/pull/3588) +- General: New Integrator small fixes [\#3583](https://github.com/pypeclub/OpenPype/pull/3583) + +**🐛 Bug fixes** + +- Editorial publishing workflow improvements [\#3580](https://github.com/pypeclub/OpenPype/pull/3580) +- Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) +- Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) +- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) + +**🔀 Refactored code** + +- General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) +- General: Lib cleanup [\#3571](https://github.com/pypeclub/OpenPype/pull/3571) + ## [3.12.2](https://github.com/pypeclub/OpenPype/tree/3.12.2) (2022-07-27) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...3.12.2) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.2-nightly.4...3.12.2) ### 📖 Documentation @@ -38,9 +64,9 @@ - General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) - Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) -- NewPublisher: Publish attributes are properly collected [\#3510](https://github.com/pypeclub/OpenPype/pull/3510) - TrayPublisher: Make sure host name is filled [\#3504](https://github.com/pypeclub/OpenPype/pull/3504) - NewPublisher: Groups work and enum multivalue [\#3501](https://github.com/pypeclub/OpenPype/pull/3501) +- Resolve: removed few bugs [\#3464](https://github.com/pypeclub/OpenPype/pull/3464) **🔀 Refactored code** @@ -61,10 +87,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.1-nightly.6...3.12.1) -### 📖 Documentation - -- Docs: Added minimal permissions for MongoDB [\#3441](https://github.com/pypeclub/OpenPype/pull/3441) - **🚀 Enhancements** - TrayPublisher: Added more options for grouping of instances [\#3494](https://github.com/pypeclub/OpenPype/pull/3494) @@ -73,7 +95,6 @@ - General: Creator Plugins have access to project [\#3476](https://github.com/pypeclub/OpenPype/pull/3476) - General: Better arguments order in creator init [\#3475](https://github.com/pypeclub/OpenPype/pull/3475) - Ftrack: Trigger custom ftrack events on project creation and preparation [\#3465](https://github.com/pypeclub/OpenPype/pull/3465) -- Windows installer: Clean old files and add version subfolder [\#3445](https://github.com/pypeclub/OpenPype/pull/3445) **🐛 Bug fixes** @@ -86,25 +107,12 @@ - Kitsu: bugfix with sync-service ans publish plugins [\#3473](https://github.com/pypeclub/OpenPype/pull/3473) - Flame: solved problem with multi-selected loading [\#3470](https://github.com/pypeclub/OpenPype/pull/3470) - General: Fix query function in update logic [\#3468](https://github.com/pypeclub/OpenPype/pull/3468) -- Resolve: removed few bugs [\#3464](https://github.com/pypeclub/OpenPype/pull/3464) - General: Delete old versions is safer when ftrack is disabled [\#3462](https://github.com/pypeclub/OpenPype/pull/3462) -- Nuke: fixing metadata slate TC difference [\#3455](https://github.com/pypeclub/OpenPype/pull/3455) -- Nuke: prerender reviewable fails [\#3450](https://github.com/pypeclub/OpenPype/pull/3450) -- Maya: fix hashing in Python 3 for tile rendering [\#3447](https://github.com/pypeclub/OpenPype/pull/3447) -- LogViewer: Escape html characters in log message [\#3443](https://github.com/pypeclub/OpenPype/pull/3443) **🔀 Refactored code** - Maya: Merge animation + pointcache extractor logic [\#3461](https://github.com/pypeclub/OpenPype/pull/3461) - Maya: Re-use `maintained\_time` from lib [\#3460](https://github.com/pypeclub/OpenPype/pull/3460) -- General: Use query functions in global plugins [\#3459](https://github.com/pypeclub/OpenPype/pull/3459) -- Clockify: Use query functions in clockify actions [\#3458](https://github.com/pypeclub/OpenPype/pull/3458) -- General: Use query functions in rest api calls [\#3457](https://github.com/pypeclub/OpenPype/pull/3457) -- General: Use query functions in openpype lib functions [\#3454](https://github.com/pypeclub/OpenPype/pull/3454) -- General: Use query functions in load utils [\#3446](https://github.com/pypeclub/OpenPype/pull/3446) -- General: Move publish plugin and publish render abstractions [\#3442](https://github.com/pypeclub/OpenPype/pull/3442) -- General: Use Anatomy after move to pipeline [\#3436](https://github.com/pypeclub/OpenPype/pull/3436) -- General: Anatomy moved to pipeline [\#3435](https://github.com/pypeclub/OpenPype/pull/3435) ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) diff --git a/openpype/version.py b/openpype/version.py index 5c39e9e630..03fd5fb96e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.2" +__version__ = "3.12.3-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 175e72be24..118355395a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.2" # OpenPype +version = "3.12.3-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 23866fee29fd3eded8a9c6c5e82442f20ca5a596 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 12:43:27 +0200 Subject: [PATCH 276/432] added some docstrings --- openpype/client/operations.py | 180 +++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index db3071abef..908566fca6 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -29,6 +29,24 @@ def _create_or_convert_to_mongo_id(mongo_id): def new_project_document( project_name, project_code, config, data=None, entity_id=None ): + """Create skeleton data of project document. + + Args: + project_name (str): Name of project. Used as identifier of a project. + project_code (str): Shorter version of projet without spaces and + special characters (in most of cases). Should be also considered + as unique name across projects. + config (Dic[str, Any]): Project config consist of roots, templates, + applications and other project Anatomy related data. + data (Dict[str, Any]): Project data with information about it's + attributes (e.g. 'fps' etc.) or integration specific keys. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of project document. + """ + if data is None: data = {} @@ -46,6 +64,22 @@ def new_project_document( def new_asset_document( name, project_id, parent_id, parents, data=None, entity_id=None ): + """Create skeleton data of asset document. + + Args: + name (str): Is considered as unique identifier of asset in project. + project_id (Union[str, ObjectId]): Id of project doument. + parent_id (Union[str, ObjectId]): Id of parent asset. + parents (List[str]): List of parent assets names. + data (Dict[str, Any]): Asset document data. Empty dictionary is used + if not passed. Value of 'parent_id' is used to fill 'visualParent'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of asset document. + """ + if data is None: data = {} if parent_id is not None: @@ -64,6 +98,21 @@ def new_asset_document( def new_subset_document(name, family, asset_id, data=None, entity_id=None): + """Create skeleton data of subset document. + + Args: + name (str): Is considered as unique identifier of subset under asset. + family (str): Subset's family. + asset_id (Union[str, ObjectId]): Id of parent asset. + data (Dict[str, Any]): Subset document data. Empty dictionary is used + if not passed. Value of 'family' is used to fill 'family'. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of subset document. + """ + if data is None: data = {} data["family"] = family @@ -78,6 +127,20 @@ def new_subset_document(name, family, asset_id, data=None, entity_id=None): def new_version_doc(version, subset_id, data=None, entity_id=None): + """Create skeleton data of version document. + + Args: + version (int): Is considered as unique identifier of version + under subset. + subset_id (Union[str, ObjectId]): Id of parent subset. + data (Dict[str, Any]): Version document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + if data is None: data = {} @@ -94,6 +157,22 @@ def new_version_doc(version, subset_id, data=None, entity_id=None): def new_representation_doc( name, version_id, context, data=None, entity_id=None ): + """Create skeleton data of asset document. + + Args: + version (int): Is considered as unique identifier of version + under subset. + version_id (Union[str, ObjectId]): Id of parent version. + context (Dict[str, Any]): Representation context used for fill template + of to query. + data (Dict[str, Any]): Representation document data. + entity_id (Union[str, ObjectId]): Predefined id of document. New id is + created if not passed. + + Returns: + Dict[str, Any]: Skeleton of version document. + """ + if data is None: data = {} @@ -124,20 +203,59 @@ def _prepare_update_data(old_doc, new_doc, replace): def prepare_subset_update_data(old_doc, new_doc, replace=True): + """Compare two subset documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + return _prepare_update_data(old_doc, new_doc, replace) def prepare_version_update_data(old_doc, new_doc, replace=True): + """Compare two version documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + return _prepare_update_data(old_doc, new_doc, replace) def prepare_representation_update_data(old_doc, new_doc, replace=True): + """Compare two representation documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + return _prepare_update_data(old_doc, new_doc, replace) @six.add_metaclass(ABCMeta) class AbstractOperation(object): - """Base operation class.""" + """Base operation class. + + Opration represent a call into database. The call can create, change or + remove data. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + """ def __init__(self, project_name, entity_type): self._project_name = project_name @@ -150,6 +268,8 @@ class AbstractOperation(object): @property def id(self): + """Identifier of operation.""" + return self._id @property @@ -158,13 +278,23 @@ class AbstractOperation(object): @abstractproperty def operation_name(self): + """Stringified type of operation.""" + pass @abstractmethod def to_mongo_operation(self): + """Convert operation to Mongo batch operation.""" + pass def to_data(self): + """Convert opration to data that can be converted to json or others. + + Returns: + Dict[str, Any]: Description of operation. + """ + return { "id": self._id, "entity_type": self.entity_type, @@ -174,6 +304,15 @@ class AbstractOperation(object): class CreateOperation(AbstractOperation): + """Opeartion to create an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + data (Dict[str, Any]): Data of entity that will be created. + """ + operation_name = "create" def __init__(self, project_name, entity_type, data): @@ -222,6 +361,18 @@ class CreateOperation(AbstractOperation): class UpdateOperation(AbstractOperation): + """Opeartion to update an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Identifier of an entity. + update_data (Dict[str, Any]): Key -> value changes that will be set in + database. If value is set to 'REMOVED_VALUE' the key will be + removed. Only first level of dictionary is checked (on purpose). + """ + operation_name = "update" def __init__(self, project_name, entity_type, entity_id, update_data): @@ -277,6 +428,15 @@ class UpdateOperation(AbstractOperation): class DeleteOperation(AbstractOperation): + """Opeartion to delete an entity. + + Args: + project_name (str): On which project operation will happen. + entity_type (str): Type of entity on which change happens. + e.g. 'asset', 'representation' etc. + entity_id (Union[str, ObjectId]): Entity id that will be removed. + """ + operation_name = "delete" def __init__(self, project_name, entity_type, entity_id): @@ -389,11 +549,23 @@ class OperationsSession(object): collection.bulk_write(bulk_writes) def create_entity(self, project_name, entity_type, data): + """Fast access to 'CreateOperation'. + + Returns: + CreateOperation: Object of update operation. + """ + operation = CreateOperation(project_name, entity_type, data) self.add(operation) return operation def update_entity(self, project_name, entity_type, entity_id, update_data): + """Fast access to 'UpdateOperation'. + + Returns: + UpdateOperation: Object of update operation. + """ + operation = UpdateOperation( project_name, entity_type, entity_id, update_data ) @@ -401,6 +573,12 @@ class OperationsSession(object): return operation def delete_entity(self, project_name, entity_type, entity_id): + """Fast access to 'DeleteOperation'. + + Returns: + DeleteOperation: Object of delete operation. + """ + operation = DeleteOperation(project_name, entity_type, entity_id) self.add(operation) return operation From 7de3d76075937309b4e07da3c7383650ebdd5c0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 12:46:44 +0200 Subject: [PATCH 277/432] removed unused import --- openpype/client/operations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index 908566fca6..dfb1d8c4dd 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -1,4 +1,3 @@ -import re import uuid import copy import collections From a29766385b07ef09837d611b4583583177a57da4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 12:52:22 +0200 Subject: [PATCH 278/432] return created instance --- openpype/hosts/traypublisher/plugins/create/create_editorial.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index e9bca79b31..28a115629e 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -81,6 +81,8 @@ class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): self._store_new_instance(new_instance) + return new_instance + def get_instance_attr_defs(self): return [ BoolDef( From 5f5aba7ae3a37ee27db59f4b651f7f85d1316a51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 13:38:00 +0200 Subject: [PATCH 279/432] loader plugins can handle settings on their own --- openpype/pipeline/load/plugins.py | 49 +++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index a30a2188a4..233aace035 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -1,6 +1,7 @@ +import os import logging -from openpype.lib import set_plugin_attributes_from_settings +from openpype.settings import get_system_settings, get_project_settings from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -37,6 +38,46 @@ class LoaderPlugin(list): def __init__(self, context): self.fname = self.filepath_from_context(context) + @classmethod + def apply_settings(cls, project_settings, system_settings): + host_name = os.environ.get("AVALON_APP") + plugin_type = "load" + plugin_type_settings = ( + project_settings + .get(host_name, {}) + .get(plugin_type, {}) + ) + global_type_settings = ( + project_settings + .get("global", {}) + .get(plugin_type, {}) + ) + if not global_type_settings and not plugin_type_settings: + return + + plugin_name = cls.__name__ + + plugin_settings = None + # Look for plugin settings in host specific settings + if plugin_name in plugin_type_settings: + plugin_settings = plugin_type_settings[plugin_name] + + # Look for plugin settings in global settings + elif plugin_name in global_type_settings: + plugin_settings = global_type_settings[plugin_name] + + if not plugin_settings: + return + + print(">>> We have preset for {}".format(plugin_name)) + for option, value in plugin_settings.items(): + if option == "enabled" and value is False: + setattr(cls, "active", False) + print(" - is disabled by preset") + else: + setattr(cls, option, value) + print(" - setting `{}`: `{}`".format(option, value)) + @classmethod def get_representations(cls): return cls.representations @@ -112,7 +153,11 @@ class SubsetLoaderPlugin(LoaderPlugin): def discover_loader_plugins(): plugins = discover(LoaderPlugin) - set_plugin_attributes_from_settings(plugins, LoaderPlugin) + project_name = os.environ.get("AVALON_PROJECT") + system_settings = get_system_settings() + project_settings = get_project_settings(project_name) + for plugin in plugins: + plugin.apply_settings(project_settings, system_settings) return plugins From b2d5146288a6b4c9ca9e0c3fc0adf339a902ec35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 13:38:18 +0200 Subject: [PATCH 280/432] LegacyCreator plugin can handle settings on their own --- openpype/pipeline/create/creator_plugins.py | 13 ++++--- openpype/pipeline/create/legacy_create.py | 43 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 8cb161de20..4a1630d8ef 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,3 +1,4 @@ +import os import copy from abc import ( @@ -7,10 +8,8 @@ from abc import ( ) import six -from openpype.lib import ( - get_subset_name_with_asset_doc, - set_plugin_attributes_from_settings, -) +from openpype.settings import get_system_settings, get_project_settings +from openpype.lib import get_subset_name_with_asset_doc from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -439,7 +438,11 @@ def discover_creator_plugins(): def discover_legacy_creator_plugins(): plugins = discover(LegacyCreator) - set_plugin_attributes_from_settings(plugins, LegacyCreator) + project_name = os.environ.get("AVALON_PROJECT") + system_settings = get_system_settings() + project_settings = get_project_settings(project_name) + for plugin in plugins: + plugin.apply_settings(project_settings, system_settings) return plugins diff --git a/openpype/pipeline/create/legacy_create.py b/openpype/pipeline/create/legacy_create.py index 46e0e3d663..2764b3cb95 100644 --- a/openpype/pipeline/create/legacy_create.py +++ b/openpype/pipeline/create/legacy_create.py @@ -5,6 +5,7 @@ Renamed classes and functions - 'create' -> 'legacy_create' """ +import os import logging import collections @@ -37,6 +38,48 @@ class LegacyCreator(object): self.data.update(data or {}) + @classmethod + def apply_settings(cls, project_settings, system_settings): + """Apply OpenPype settings to a plugin class.""" + + host_name = os.environ.get("AVALON_APP") + plugin_type = "create" + plugin_type_settings = ( + project_settings + .get(host_name, {}) + .get(plugin_type, {}) + ) + global_type_settings = ( + project_settings + .get("global", {}) + .get(plugin_type, {}) + ) + if not global_type_settings and not plugin_type_settings: + return + + plugin_name = cls.__name__ + + plugin_settings = None + # Look for plugin settings in host specific settings + if plugin_name in plugin_type_settings: + plugin_settings = plugin_type_settings[plugin_name] + + # Look for plugin settings in global settings + elif plugin_name in global_type_settings: + plugin_settings = global_type_settings[plugin_name] + + if not plugin_settings: + return + + print(">>> We have preset for {}".format(plugin_name)) + for option, value in plugin_settings.items(): + if option == "enabled" and value is False: + setattr(cls, "active", False) + print(" - is disabled by preset") + else: + setattr(cls, option, value) + print(" - setting `{}`: `{}`".format(option, value)) + def process(self): pass From acb4b28b975c8e276602a32237de7ce37773342b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 14:14:33 +0200 Subject: [PATCH 281/432] moved filter pyblish plugins function to openpype.pipeline.publish.lib --- openpype/pipeline/context_tools.py | 2 +- openpype/pipeline/publish/lib.py | 93 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 0535ce5d54..c8c70e5ea8 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -18,8 +18,8 @@ from openpype.client import ( ) from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings -from openpype.lib import filter_pyblish_plugins +from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy from .template_data import get_template_data_with_names from . import ( diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 739b2c8806..d5494cd8a4 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -6,6 +6,10 @@ import xml.etree.ElementTree import six import pyblish.plugin +import pyblish.api + +from openpype.lib import Logger +from openpype.settings import get_project_settings, get_system_settings class DiscoverResult: @@ -180,3 +184,92 @@ def publish_plugins_discover(paths=None): result.plugins = plugins return result + + +def filter_pyblish_plugins(plugins): + """Pyblish plugin filter which applies OpenPype settings. + + Apply OpenPype settings on discovered plugins. On plugin with implemented + class method 'def apply_settings(cls, project_settings, system_settings)' + is called the method. Default behavior looks for plugin name and current + host name to look for + + Args: + plugins (List[pyblish.plugin.Plugin]): Discovered plugins on which + are applied settings. + """ + + log = Logger.get_logger("filter_pyblish_plugins") + + # TODO: Don't use host from 'pyblish.api' but from defined host by us. + # - kept becau on farm is probably used host 'shell' which propably + # affect how settings are applied there + host = pyblish.api.current_host() + project_name = os.environ.get("AVALON_PROJECT") + + project_setting = get_project_settings(project_name) + system_settings = get_system_settings() + + # iterate over plugins + for plugin in plugins[:]: + if hasattr(plugin, "apply_settings"): + try: + # Use classmethod 'apply_settings' + # - can be used to target settings from custom settings place + # - skip default behavior when successful + plugin.apply_settings(project_setting, system_settings) + continue + + except Exception: + log.warning( + ( + "Failed to apply settings on plugin {}" + ).format(plugin.__name__), + exc_info=True + ) + + try: + config_data = ( + project_setting + [host] + ["publish"] + [plugin.__name__] + ) + except KeyError: + # host determined from path + file = os.path.normpath(inspect.getsourcefile(plugin)) + file = os.path.normpath(file) + + split_path = file.split(os.path.sep) + if len(split_path) < 4: + log.warning( + 'plugin path too short to extract host {}'.format(file) + ) + continue + + host_from_file = split_path[-4] + plugin_kind = split_path[-2] + + # TODO: change after all plugins are moved one level up + if host_from_file == "openpype": + host_from_file = "global" + + try: + config_data = ( + project_setting + [host_from_file] + [plugin_kind] + [plugin.__name__] + ) + except KeyError: + continue + + for option, value in config_data.items(): + if option == "enabled" and value is False: + log.info('removing plugin {}'.format(plugin.__name__)) + plugins.remove(plugin) + else: + log.info('setting {}:{} on plugin {}'.format( + option, value, plugin.__name__)) + + setattr(plugin, option, value) From cf42e8fa44bb61fe1d6f80f8e122b52fb8cc022b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 14:15:26 +0200 Subject: [PATCH 282/432] mark functions in openpype.lib as deprecated --- openpype/lib/plugin_tools.py | 101 +++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 1d3c1eec6b..c94d1251fc 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- """Avalon/Pyblish plugin tools.""" import os -import inspect import logging import re import json +import warnings +import functools + from openpype.client import get_asset_by_id from openpype.settings import get_project_settings @@ -17,6 +19,51 @@ log = logging.getLogger(__name__) DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" +class PluginToolsDeprecatedWarning(DeprecationWarning): + pass + + +def deprecated(new_destination): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + func = None + if callable(new_destination): + func = new_destination + new_destination = None + + def _decorator(decorated_func): + if new_destination is None: + warning_message = ( + " Please check content of deprecated function to figure out" + " possible replacement." + ) + else: + warning_message = " Please replace your usage with '{}'.".format( + new_destination + ) + + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + warnings.simplefilter("always", PluginToolsDeprecatedWarning) + warnings.warn( + ( + "Call to deprecated function '{}'" + "\nFunction was moved or removed.{}" + ).format(decorated_func.__name__, warning_message), + category=PluginToolsDeprecatedWarning, + stacklevel=4 + ) + return decorated_func(*args, **kwargs) + return wrapper- + + if func is None: + return _decorator + return _decorator(func) + + class TaskNotSetError(KeyError): def __init__(self, msg=None): if not msg: @@ -197,6 +244,7 @@ def prepare_template_data(fill_pairs): return fill_data +@deprecated("openpype.pipeline.publish.lib.filter_pyblish_plugins") def filter_pyblish_plugins(plugins): """Filter pyblish plugins by presets. @@ -206,57 +254,14 @@ def filter_pyblish_plugins(plugins): Args: plugins (dict): Dictionary of plugins produced by :mod:`pyblish-base` `discover()` method. - """ - from pyblish import api - host = api.current_host() + from openpype.pipeline.publish.lib import filter_pyblish_plugins - presets = get_project_settings(os.environ['AVALON_PROJECT']) or {} - # skip if there are no presets to process - if not presets: - return - - # iterate over plugins - for plugin in plugins[:]: - - try: - config_data = presets[host]["publish"][plugin.__name__] - except KeyError: - # host determined from path - file = os.path.normpath(inspect.getsourcefile(plugin)) - file = os.path.normpath(file) - - split_path = file.split(os.path.sep) - if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(file) - ) - continue - - host_from_file = split_path[-4] - plugin_kind = split_path[-2] - - # TODO: change after all plugins are moved one level up - if host_from_file == "openpype": - host_from_file = "global" - - try: - config_data = presets[host_from_file][plugin_kind][plugin.__name__] # noqa: E501 - except KeyError: - continue - - for option, value in config_data.items(): - if option == "enabled" and value is False: - log.info('removing plugin {}'.format(plugin.__name__)) - plugins.remove(plugin) - else: - log.info('setting {}:{} on plugin {}'.format( - option, value, plugin.__name__)) - - setattr(plugin, option, value) + filter_pyblish_plugins(plugins) +@deprecated def set_plugin_attributes_from_settings( plugins, superclass, host_name=None, project_name=None ): @@ -273,6 +278,8 @@ def set_plugin_attributes_from_settings( project_name (str): Name of project for which settings will be loaded. Value from environment `AVALON_PROJECT` is used if not entered. """ + + # Function is not used anymore from openpype.pipeline import LegacyCreator, LoaderPlugin # determine host application to use for finding presets From 498ee1d85066ca40659b73196f58886682b1e186 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 15:15:50 +0300 Subject: [PATCH 283/432] Fix schema to store as lists --- .../projects_schema/schemas/schema_maya_render_settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 9b6b6f1eed..af197604f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -275,6 +275,7 @@ }, { "type": "dict-modifiable", + "store_as_list": true, "key": "additional_options", "label": "Additional Renderer Options", "use_label_wrap": true, @@ -403,6 +404,7 @@ }, { "type": "dict-modifiable", + "store_as_list": true, "key": "additional_options", "label": "Additional Renderer Options", "use_label_wrap": true, From 84a6c144c72928d252e04d3c378eef2926e3fdfa Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 15:16:50 +0300 Subject: [PATCH 284/432] Handle additional attributes --- openpype/hosts/maya/api/lib_rendersettings.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 6154e1ab89..9aea55a03b 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -139,6 +139,7 @@ class RenderSettings(object): # allow fullstops in custom attributes. Then checks for # type of MtoA attribute passed to adjust the `setAttr` # command accordingly. + self._additional_attribs_setter(additional_options) for item in additional_options: attribute, value = item if (cmds.getAttr(str(attribute), type=True)) == "long": @@ -157,18 +158,28 @@ class RenderSettings(object): ["RenderSettings"] ["redshift_renderer"] ) - img_ext = redshift_render_presets.get("image_format") + additional_options = redshift_render_presets["additional_options"] + ext = redshift_render_presets["image_format"] + img_exts = ["iff", "exr", "tif", "png", "tga", "jpg"] + img_ext = img_exts.index(ext) + self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) + self._additional_attribs_setter(additional_options) def _set_vray_settings(self, aov_separator, width, height): # type: (str, int, int) -> None """Sets important settings for Vray.""" settings = cmds.ls(type="VRaySettingsNode") node = settings[0] if settings else cmds.createNode("VRaySettingsNode") - + vray_render_presets = ( + self._project_settings + ["maya"] + ["RenderSettings"] + ["vray_renderer"] + ) # Set aov separator # First we need to explicitly set the UI items in Render Settings # because that is also what V-Ray updates to when that Render Settings @@ -207,6 +218,10 @@ class RenderSettings(object): cmds.setAttr("{}.width".format(node), width) cmds.setAttr("{}.height".format(node), height) + additional_options = vray_render_presets["additional_options"] + + self._additional_attribs_setter(additional_options) + @staticmethod def _set_global_output_settings(): # enable animation @@ -214,3 +229,14 @@ class RenderSettings(object): cmds.setAttr("defaultRenderGlobals.animation", 1) cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) + + def _additional_attribs_setter(self, additional_attribs): + print(additional_attribs) + for item in additional_attribs: + attribute, value = item + if (cmds.getAttr(str(attribute), type=True)) == "long": + cmds.setAttr(str(attribute), int(value)) + elif (cmds.getAttr(str(attribute), type=True)) == "bool": + cmds.setAttr(str(attribute), int(value)) # noqa + elif (cmds.getAttr(str(attribute), type=True)) == "string": + cmds.setAttr(str(attribute), str(value), type = "string") # noqa From bb10fdd041c499f30e5ffa7dd4069828b9f42239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Aug 2022 18:00:14 +0200 Subject: [PATCH 285/432] :rotating_light: f-strings and cosmetic issues --- igniter/bootstrap_repos.py | 10 +++---- start.py | 55 +++++++++++++++++--------------------- tools/create_zip.py | 2 +- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 08333885c0..8888440f90 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -122,7 +122,7 @@ class OpenPypeVersion(semver.VersionInfo): if self.staging: if kwargs.get("build"): if "staging" not in kwargs.get("build"): - kwargs["build"] = "{}-staging".format(kwargs.get("build")) + kwargs["build"] = f"{kwargs.get('build')}-staging" else: kwargs["build"] = "staging" @@ -136,8 +136,7 @@ class OpenPypeVersion(semver.VersionInfo): return bool(result and self.staging == other.staging) def __repr__(self): - return "<{}: {} - path={}>".format( - self.__class__.__name__, str(self), self.path) + return f"<{self.__class__.__name__}: {str(self)} - path={self.path}>" def __lt__(self, other: OpenPypeVersion): result = super().__lt__(other) @@ -232,10 +231,7 @@ class OpenPypeVersion(semver.VersionInfo): return openpype_version def __hash__(self): - if self.path: - return hash(self.path) - else: - return hash(str(self)) + return hash(self.path) if self.path else hash(str(self)) @staticmethod def is_version_in_dir( diff --git a/start.py b/start.py index cbf8ffd178..37cc4c063d 100644 --- a/start.py +++ b/start.py @@ -187,9 +187,8 @@ else: if "--headless" in sys.argv: os.environ["OPENPYPE_HEADLESS_MODE"] = "1" sys.argv.remove("--headless") -else: - if os.getenv("OPENPYPE_HEADLESS_MODE") != "1": - os.environ.pop("OPENPYPE_HEADLESS_MODE", None) +elif os.getenv("OPENPYPE_HEADLESS_MODE") != "1": + os.environ.pop("OPENPYPE_HEADLESS_MODE", None) # Enabled logging debug mode when "--debug" is passed if "--verbose" in sys.argv: @@ -203,8 +202,8 @@ if "--verbose" in sys.argv: value = sys.argv.pop(idx) else: raise RuntimeError(( - "Expect value after \"--verbose\" argument. {}" - ).format(expected_values)) + f"Expect value after \"--verbose\" argument. {expected_values}" + )) log_level = None low_value = value.lower() @@ -225,8 +224,9 @@ if "--verbose" in sys.argv: if log_level is None: raise RuntimeError(( - "Unexpected value after \"--verbose\" argument \"{}\". {}" - ).format(value, expected_values)) + "Unexpected value after \"--verbose\" " + f"argument \"{value}\". {expected_values}" + )) os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level) @@ -336,34 +336,33 @@ def run_disk_mapping_commands(settings): destination = destination.rstrip('/') source = source.rstrip('/') - if low_platform == "windows": - args = ["subst", destination, source] - elif low_platform == "darwin": - scr = "do shell script \"ln -s {} {}\" with administrator privileges".format(source, destination) # noqa: E501 + if low_platform == "darwin": + scr = f'do shell script "ln -s {source} {destination}" with administrator privileges' # noqa + args = ["osascript", "-e", scr] + elif low_platform == "windows": + args = ["subst", destination, source] else: args = ["sudo", "ln", "-s", source, destination] - _print("disk mapping args:: {}".format(args)) + _print(f"*** disk mapping arguments: {args}") try: if not os.path.exists(destination): output = subprocess.Popen(args) if output.returncode and output.returncode != 0: - exc_msg = "Executing was not successful: \"{}\"".format( - args) + exc_msg = f'Executing was not successful: "{args}"' raise RuntimeError(exc_msg) except TypeError as exc: - _print("Error {} in mapping drive {}, {}".format(str(exc), - source, - destination)) + _print( + f"Error {str(exc)} in mapping drive {source}, {destination}") raise def set_avalon_environments(): """Set avalon specific environments. - These are non modifiable environments for avalon workflow that must be set + These are non-modifiable environments for avalon workflow that must be set before avalon module is imported because avalon works with globals set with environment variables. """ @@ -508,7 +507,7 @@ def _process_arguments() -> tuple: ) if m and m.group('version'): use_version = m.group('version') - _print(">>> Requested version [ {} ]".format(use_version)) + _print(f">>> Requested version [ {use_version} ]") if "+staging" in use_version: use_staging = True break @@ -614,8 +613,8 @@ def _determine_mongodb() -> str: try: openpype_mongo = bootstrap.secure_registry.get_item( "openPypeMongo") - except ValueError: - raise RuntimeError("Missing MongoDB url") + except ValueError as e: + raise RuntimeError("Missing MongoDB url") from e return openpype_mongo @@ -816,11 +815,8 @@ def _bootstrap_from_code(use_version, use_staging): use_version, use_staging ) if version_to_use is None: - raise OpenPypeVersionNotFound( - "Requested version \"{}\" was not found.".format( - use_version - ) - ) + raise OpenPypeVersionIncompatible( + f"Requested version \"{use_version}\" was not found.") else: # Staging version should be used version_to_use = bootstrap.find_latest_openpype_version( @@ -906,7 +902,7 @@ def _boot_validate_versions(use_version, local_version): use_version, openpype_versions ) valid, message = bootstrap.validate_openpype_version(version_path) - _print("{}{}".format(">>> " if valid else "!!! ", message)) + _print(f'{">>> " if valid else "!!! "}{message}') def _boot_print_versions(use_staging, local_version, openpype_root): @@ -1043,7 +1039,7 @@ def boot(): if not result[0]: _print(f"!!! Invalid version: {result[1]}") sys.exit(1) - _print(f"--- version is valid") + _print("--- version is valid") else: try: version_path = _bootstrap_from_code(use_version, use_staging) @@ -1164,8 +1160,7 @@ def get_info(use_staging=None) -> list: formatted = [] for info in inf: padding = (maximum - len(info[0])) + 1 - formatted.append( - "... {}:{}[ {} ]".format(info[0], " " * padding, info[1])) + formatted.append(f'... {info[0]}:{" " * padding}[ {info[1]} ]') return formatted diff --git a/tools/create_zip.py b/tools/create_zip.py index 2fc351469a..6392428f58 100644 --- a/tools/create_zip.py +++ b/tools/create_zip.py @@ -61,7 +61,7 @@ def _print(msg: str, message_type: int = 0) -> None: else: header = term.darkolivegreen3("--- ") - print("{}{}".format(header, msg)) + print(f"{header}{msg}") if __name__ == "__main__": From a9f910ac227fd0f90a589ba9035d232c0c62e6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Aug 2022 18:01:03 +0200 Subject: [PATCH 286/432] :recycle: add openpype version env var to deadline job --- .../deadline/plugins/publish/submit_aftereffects_deadline.py | 3 ++- .../deadline/plugins/publish/submit_harmony_deadline.py | 3 ++- .../deadline/plugins/publish/submit_houdini_remote_publish.py | 1 + .../deadline/plugins/publish/submit_houdini_render_deadline.py | 1 + .../modules/deadline/plugins/publish/submit_maya_deadline.py | 3 ++- .../plugins/publish/submit_maya_remote_publish_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 8 files changed, 14 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index de8df3dd9e..c55f85c8da 100644 --- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -80,7 +80,8 @@ class AfterEffectsSubmitDeadline( "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "OPENPYPE_LOG_NO_COLORS" + "OPENPYPE_LOG_NO_COLORS", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if self._instance.context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index a1ee5e0957..3f9c09b592 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -274,7 +274,8 @@ class HarmonySubmitDeadline( "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "OPENPYPE_LOG_NO_COLORS" + "OPENPYPE_LOG_NO_COLORS", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if self._instance.context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py index fdf67b51bc..95856137e2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py @@ -130,6 +130,7 @@ class HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin): # this application with so the Render Slave can build its own # similar environment using it, e.g. "houdini17.5;pluginx2.3" "AVALON_TOOLS", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index aca88c7440..beda753723 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -101,6 +101,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): # this application with so the Render Slave can build its own # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" "AVALON_TOOLS", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 145b6d795f..f253ceb21a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -525,7 +525,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "OPENPYPE_LOG_NO_COLORS" + "OPENPYPE_LOG_NO_COLORS", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if instance.context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 57572fcb24..9b1852392b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -102,7 +102,8 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): keys = [ "FTRACK_API_USER", "FTRACK_API_KEY", - "FTRACK_SERVER" + "FTRACK_SERVER", + "OPENPYPE_VERSION" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 93fb511a34..a328c3633d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -261,7 +261,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "PYBLISHPLUGINPATH", "NUKE_PATH", "TOOL_ENV", - "FOUNDRY_LICENSE" + "FOUNDRY_LICENSE", + "OPENPYPE_VERSION" ] # Add mongo url if it's enabled if instance.context.data.get("deadlinePassMongoUrl"): diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 43ea64e565..5c7998465b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -141,7 +141,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "OPENPYPE_USERNAME", "OPENPYPE_RENDER_JOB", "OPENPYPE_PUBLISH_JOB", - "OPENPYPE_MONGO" + "OPENPYPE_MONGO", + "OPENPYPE_VERSION" ] # custom deadline attributes From 0e126a2d829e814d39747b4073cac2fb2cbc7b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Aug 2022 18:01:25 +0200 Subject: [PATCH 287/432] :recycle: handle multiple versions --- igniter/tools.py | 5 +++ openpype/cli.py | 23 ++++++++++++++ start.py | 83 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index 57159b5e52..a9d592acf0 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -21,6 +21,11 @@ class OpenPypeVersionNotFound(Exception): pass +class OpenPypeVersionIncompatible(Exception): + """OpenPype version is not compatible with the installed one (build).""" + pass + + def should_add_certificate_path_to_mongo_url(mongo_url): """Check if should add ca certificate to mongo url. diff --git a/openpype/cli.py b/openpype/cli.py index 9a2dfaa141..ffe288040e 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -443,3 +443,26 @@ def interactive(): __version__, sys.version, sys.platform ) code.interact(banner) + + +@main.command() +@click.option("--build", help="Print only build version", + is_flag=True, default=False) +def version(build): + """Print OpenPype version.""" + + from openpype.version import __version__ + from igniter.bootstrap_repos import BootstrapRepos, OpenPypeVersion + from pathlib import Path + import os + + if getattr(sys, 'frozen', False): + local_version = BootstrapRepos.get_version( + Path(os.getenv("OPENPYPE_ROOT"))) + else: + local_version = OpenPypeVersion.get_installed_version_str() + + if build: + print(local_version) + return + print(f"{__version__} (booted: {local_version})") diff --git a/start.py b/start.py index 37cc4c063d..5cdffafb6e 100644 --- a/start.py +++ b/start.py @@ -103,6 +103,9 @@ import site import distutils.spawn from pathlib import Path + +silent_mode = False + # OPENPYPE_ROOT is variable pointing to build (or code) directory # WARNING `OPENPYPE_ROOT` must be defined before igniter import # - igniter changes cwd which cause that filepath of this script won't lead @@ -138,40 +141,44 @@ if sys.__stdout__: term = blessed.Terminal() def _print(message: str): + if silent_mode: + return if message.startswith("!!! "): - print("{}{}".format(term.orangered2("!!! "), message[4:])) + print(f'{term.orangered2("!!! ")}{message[4:]}') return if message.startswith(">>> "): - print("{}{}".format(term.aquamarine3(">>> "), message[4:])) + print(f'{term.aquamarine3(">>> ")}{message[4:]}') return if message.startswith("--- "): - print("{}{}".format(term.darkolivegreen3("--- "), message[4:])) + print(f'{term.darkolivegreen3("--- ")}{message[4:]}') return if message.startswith("*** "): - print("{}{}".format(term.gold("*** "), message[4:])) + print(f'{term.gold("*** ")}{message[4:]}') return if message.startswith(" - "): - print("{}{}".format(term.wheat(" - "), message[4:])) + print(f'{term.wheat(" - ")}{message[4:]}') return if message.startswith(" . "): - print("{}{}".format(term.tan(" . "), message[4:])) + print(f'{term.tan(" . ")}{message[4:]}') return if message.startswith(" - "): - print("{}{}".format(term.seagreen3(" - "), message[7:])) + print(f'{term.seagreen3(" - ")}{message[7:]}') return if message.startswith(" ! "): - print("{}{}".format(term.goldenrod(" ! "), message[7:])) + print(f'{term.goldenrod(" ! ")}{message[7:]}') return if message.startswith(" * "): - print("{}{}".format(term.aquamarine1(" * "), message[7:])) + print(f'{term.aquamarine1(" * ")}{message[7:]}') return if message.startswith(" "): - print("{}{}".format(term.darkseagreen3(" "), message[4:])) + print(f'{term.darkseagreen3(" ")}{message[4:]}') return print(message) else: def _print(message: str): + if silent_mode: + return print(message) @@ -242,13 +249,14 @@ from igniter.tools import ( get_openpype_global_settings, get_openpype_path_from_settings, validate_mongo_connection, - OpenPypeVersionNotFound + OpenPypeVersionNotFound, + OpenPypeVersionIncompatible ) # noqa from igniter.bootstrap_repos import OpenPypeVersion # noqa: E402 bootstrap = BootstrapRepos() silent_commands = {"run", "igniter", "standalonepublisher", - "extractenvironments"} + "extractenvironments", "version"} def list_versions(openpype_versions: list, local_version=None) -> None: @@ -686,40 +694,47 @@ def _find_frozen_openpype(use_version: str = None, # Specific version is defined if use_version.lower() == "latest": # Version says to use latest version - _print("Finding latest version defined by use version") + _print(">>> Finding latest version defined by use version") openpype_version = bootstrap.find_latest_openpype_version( - use_staging + use_staging, compatible_with=installed_version ) else: - _print("Finding specified version \"{}\"".format(use_version)) + _print(f">>> Finding specified version \"{use_version}\"") openpype_version = bootstrap.find_openpype_version( use_version, use_staging ) if openpype_version is None: raise OpenPypeVersionNotFound( - "Requested version \"{}\" was not found.".format( - use_version - ) + f"Requested version \"{use_version}\" was not found." ) + if not openpype_version.is_compatible(installed_version): + raise OpenPypeVersionIncompatible(( + f"Requested version \"{use_version}\" is not compatible " + f"with installed version \"{installed_version}\"" + )) + elif studio_version is not None: # Studio has defined a version to use - _print("Finding studio version \"{}\"".format(studio_version)) + _print(f">>> Finding studio version \"{studio_version}\"") openpype_version = bootstrap.find_openpype_version( - studio_version, use_staging + studio_version, use_staging, compatible_with=installed_version ) if openpype_version is None: raise OpenPypeVersionNotFound(( - "Requested OpenPype version \"{}\" defined by settings" + "Requested OpenPype version " + f"\"{studio_version}\" defined by settings" " was not found." - ).format(studio_version)) + )) else: # Default behavior to use latest version - _print("Finding latest version") + _print(( + ">>> Finding latest version compatible " + f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( - use_staging + use_staging, compatible_with=installed_version ) if openpype_version is None: if use_staging: @@ -800,7 +815,7 @@ def _bootstrap_from_code(use_version, use_staging): if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) - switch_str = f" - will switch to {use_version}" if use_version else "" + switch_str = f" - will switch to {use_version}" if use_version and use_version != local_version else "" # noqa _print(f" - booting version: {local_version}{switch_str}") assert local_version else: @@ -913,13 +928,24 @@ def _boot_print_versions(use_staging, local_version, openpype_root): _print("--- This will list only staging versions detected.") _print(" To see other version, omit --use-staging argument.") - openpype_versions = bootstrap.find_openpype(include_zips=True, - staging=use_staging) if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(openpype_root)) else: local_version = OpenPypeVersion.get_installed_version_str() + compatible_with = OpenPypeVersion(version=local_version) + if "--all" in sys.argv: + compatible_with = None + _print("--- Showing all version (even those not compatible).") + else: + _print(("--- Showing only compatible versions " + f"with [ {compatible_with.major}.{compatible_with.minor} ]")) + + openpype_versions = bootstrap.find_openpype( + include_zips=True, + staging=use_staging, + compatible_with=compatible_with) + list_versions(openpype_versions, local_version) @@ -936,6 +962,9 @@ def _boot_handle_missing_version(local_version, use_staging, message): def boot(): """Bootstrap OpenPype.""" + global silent_mode + if any(arg in silent_commands for arg in sys.argv): + silent_mode = True # ------------------------------------------------------------------------ # Set environment to OpenPype root path From 9205d4bde12baf8901a2ba675157cc0b4ad65919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Aug 2022 18:02:24 +0200 Subject: [PATCH 288/432] :recycle: changes in bootstrapping for multiple versions --- igniter/bootstrap_repos.py | 196 ++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 38 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 8888440f90..47f2525952 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -380,7 +380,8 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_local_versions( - cls, production: bool = None, staging: bool = None + cls, production: bool = None, + staging: bool = None, compatible_with: OpenPypeVersion = None ) -> List: """Get all versions available on this machine. @@ -390,6 +391,8 @@ class OpenPypeVersion(semver.VersionInfo): Args: production (bool): Return production versions. staging (bool): Return staging versions. + compatible_with (OpenPypeVersion): Return only those compatible + with specified version. """ # Return all local versions if arguments are set to None if production is None and staging is None: @@ -406,10 +409,19 @@ class OpenPypeVersion(semver.VersionInfo): if not production and not staging: return [] + # DEPRECATED: backwards compatible way to look for versions in root dir_to_search = Path(user_data_dir("openpype", "pypeclub")) versions = OpenPypeVersion.get_versions_from_directory( - dir_to_search + dir_to_search, compatible_with=compatible_with ) + if compatible_with: + dir_to_search = Path( + user_data_dir("openpype", "pypeclub")) / f"{compatible_with.major}.{compatible_with.minor}" # noqa + versions += OpenPypeVersion.get_versions_from_directory( + dir_to_search, compatible_with=compatible_with + ) + + filtered_versions = [] for version in versions: if version.is_staging(): @@ -421,7 +433,8 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_remote_versions( - cls, production: bool = None, staging: bool = None + cls, production: bool = None, + staging: bool = None, compatible_with: OpenPypeVersion = None ) -> List: """Get all versions available in OpenPype Path. @@ -431,6 +444,8 @@ class OpenPypeVersion(semver.VersionInfo): Args: production (bool): Return production versions. staging (bool): Return staging versions. + compatible_with (OpenPypeVersion): Return only those compatible + with specified version. """ # Return all local versions if arguments are set to None if production is None and staging is None: @@ -464,7 +479,14 @@ class OpenPypeVersion(semver.VersionInfo): if not dir_to_search: return [] - versions = cls.get_versions_from_directory(dir_to_search) + # DEPRECATED: look for version in root directory + versions = cls.get_versions_from_directory( + dir_to_search, compatible_with=compatible_with) + if compatible_with: + dir_to_search = dir_to_search / f"{compatible_with.major}.{compatible_with.minor}" # noqa + versions += cls.get_versions_from_directory( + dir_to_search, compatible_with=compatible_with) + filtered_versions = [] for version in versions: if version.is_staging(): @@ -475,11 +497,15 @@ class OpenPypeVersion(semver.VersionInfo): return list(sorted(set(filtered_versions))) @staticmethod - def get_versions_from_directory(openpype_dir: Path) -> List: + def get_versions_from_directory( + openpype_dir: Path, + compatible_with: OpenPypeVersion = None) -> List: """Get all detected OpenPype versions in directory. Args: openpype_dir (Path): Directory to scan. + compatible_with (OpenPypeVersion): Return only versions compatible + with build version specified as OpenPypeVersion. Returns: list of OpenPypeVersion @@ -514,6 +540,10 @@ class OpenPypeVersion(semver.VersionInfo): )[0]: continue + if compatible_with and not detected_version.is_compatible( + compatible_with): + continue + detected_version.path = item _openpype_versions.append(detected_version) @@ -545,8 +575,9 @@ class OpenPypeVersion(semver.VersionInfo): def get_latest_version( staging: bool = False, local: bool = None, - remote: bool = None - ) -> OpenPypeVersion: + remote: bool = None, + compatible_with: OpenPypeVersion = None + ) -> Union[OpenPypeVersion, None]: """Get latest available version. The version does not contain information about path and source. @@ -564,6 +595,9 @@ class OpenPypeVersion(semver.VersionInfo): staging (bool, optional): List staging versions if True. local (bool, optional): List local versions if True. remote (bool, optional): List remote versions if True. + compatible_with (OpenPypeVersion, optional) Return only version + compatible with compatible_with. + """ if local is None and remote is None: local = True @@ -594,7 +628,12 @@ class OpenPypeVersion(semver.VersionInfo): return None all_versions.sort() - return all_versions[-1] + latest_version: OpenPypeVersion + latest_version = all_versions[-1] + if compatible_with and not latest_version.is_compatible( + compatible_with): + return None + return latest_version @classmethod def get_expected_studio_version(cls, staging=False, global_settings=None): @@ -617,6 +656,21 @@ class OpenPypeVersion(semver.VersionInfo): return None return OpenPypeVersion(version=result) + def is_compatible(self, version: OpenPypeVersion): + """Test build compatibility. + + This will simply compare major and minor versions (ignoring patch + and the rest). + + Args: + version (OpenPypeVersion): Version to check compatibility with. + + Returns: + bool: if the version is compatible + + """ + return self.major == version.major and self.minor == version.minor + class BootstrapRepos: """Class for bootstrapping local OpenPype installation. @@ -737,8 +791,9 @@ class BootstrapRepos: return # create destination directory - if not self.data_dir.exists(): - self.data_dir.mkdir(parents=True) + destination = self.data_dir / f"{installed_version.major}.{installed_version.minor}" # noqa + if not destination.exists(): + destination.mkdir(parents=True) # create zip inside temporary directory. with tempfile.TemporaryDirectory() as temp_dir: @@ -766,7 +821,9 @@ class BootstrapRepos: Path to moved zip on success. """ - destination = self.data_dir / zip_file.name + version = OpenPypeVersion.version_in_str(zip_file.name) + destination_dir = self.data_dir / f"{version.major}.{version.minor}" + destination = destination_dir / zip_file.name if destination.exists(): self._print( @@ -778,7 +835,7 @@ class BootstrapRepos: self._print(str(e), LOG_ERROR, exc_info=True) return None try: - shutil.move(zip_file.as_posix(), self.data_dir.as_posix()) + shutil.move(zip_file.as_posix(), destination_dir.as_posix()) except shutil.Error as e: self._print(str(e), LOG_ERROR, exc_info=True) return None @@ -991,6 +1048,16 @@ class BootstrapRepos: @staticmethod def _validate_dir(path: Path) -> tuple: + """Validate checksums in a given path. + + Args: + path (Path): path to folder to validate. + + Returns: + tuple(bool, str): returns status and reason as a bool + and str in a tuple. + + """ checksums_file = Path(path / "checksums") if not checksums_file.exists(): # FIXME: This should be set to False sometimes in the future @@ -1072,7 +1139,20 @@ class BootstrapRepos: sys.path.insert(0, directory.as_posix()) @staticmethod - def find_openpype_version(version, staging): + def find_openpype_version( + version: Union[str, OpenPypeVersion], + staging: bool, + compatible_with: OpenPypeVersion = None + ) -> Union[OpenPypeVersion, None]: + """Find location of specified OpenPype version. + + Args: + version (Union[str, OpenPypeVersion): Version to find. + staging (bool): Filter staging versions. + compatible_with (OpenPypeVersion, optional): Find only + versions compatible with specified one. + + """ if isinstance(version, str): version = OpenPypeVersion(version=version) @@ -1081,7 +1161,8 @@ class BootstrapRepos: return installed_version local_versions = OpenPypeVersion.get_local_versions( - staging=staging, production=not staging + staging=staging, production=not staging, + compatible_with=compatible_with ) zip_version = None for local_version in local_versions: @@ -1095,7 +1176,8 @@ class BootstrapRepos: return zip_version remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, production=not staging + staging=staging, production=not staging, + compatible_with=compatible_with ) for remote_version in remote_versions: if remote_version == version: @@ -1103,13 +1185,14 @@ class BootstrapRepos: return None @staticmethod - def find_latest_openpype_version(staging): + def find_latest_openpype_version( + staging, compatible_with: OpenPypeVersion = None): installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions( - staging=staging + staging=staging, compatible_with=compatible_with ) remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging + staging=staging, compatible_with=compatible_with ) all_versions = local_versions + remote_versions if not staging: @@ -1134,7 +1217,9 @@ class BootstrapRepos: self, openpype_path: Union[Path, str] = None, staging: bool = False, - include_zips: bool = False) -> Union[List[OpenPypeVersion], None]: + include_zips: bool = False, + compatible_with: OpenPypeVersion = None + ) -> Union[List[OpenPypeVersion], None]: """Get ordered dict of detected OpenPype version. Resolution order for OpenPype is following: @@ -1150,6 +1235,8 @@ class BootstrapRepos: otherwise. include_zips (bool, optional): If set True it will try to find OpenPype in zip files in given directory. + compatible_with (OpenPypeVersion, optional): Find only those + versions compatible with the one specified. Returns: dict of Path: Dictionary of detected OpenPype version. @@ -1168,30 +1255,56 @@ class BootstrapRepos: ("Finding OpenPype in non-filesystem locations is" " not implemented yet.")) - dir_to_search = self.data_dir - user_versions = self.get_openpype_versions(self.data_dir, staging) - # if we have openpype_path specified, search only there. + version_dir = "" + if compatible_with: + version_dir = f"{compatible_with.major}.{compatible_with.minor}" + + # if checks bellow for OPENPYPE_PATH and registry fails, use data_dir + # DEPRECATED: lookup in root of this folder is deprecated in favour + # of major.minor sub-folders. + dirs_to_search = [ + self.data_dir + ] + if compatible_with: + dirs_to_search.append(self.data_dir / version_dir) + if openpype_path: - dir_to_search = openpype_path + dirs_to_search = [openpype_path] + + if compatible_with: + dirs_to_search.append(openpype_path / version_dir) else: - if os.getenv("OPENPYPE_PATH"): - if Path(os.getenv("OPENPYPE_PATH")).exists(): - dir_to_search = Path(os.getenv("OPENPYPE_PATH")) + # first try OPENPYPE_PATH and if that is not available, + # try registry. + if os.getenv("OPENPYPE_PATH") \ + and Path(os.getenv("OPENPYPE_PATH")).exists(): + dirs_to_search = [Path(os.getenv("OPENPYPE_PATH"))] + + if compatible_with: + dirs_to_search.append( + Path(os.getenv("OPENPYPE_PATH")) / version_dir) else: try: registry_dir = Path( str(self.registry.get_item("openPypePath"))) if registry_dir.exists(): - dir_to_search = registry_dir + dirs_to_search = [registry_dir] + if compatible_with: + dirs_to_search.append(registry_dir / version_dir) except ValueError: # nothing found in registry, we'll use data dir pass - openpype_versions = self.get_openpype_versions(dir_to_search, staging) - openpype_versions += user_versions + openpype_versions = [] + for dir_to_search in dirs_to_search: + try: + openpype_versions += self.get_openpype_versions( + dir_to_search, staging, compatible_with=compatible_with) + except ValueError: + # location is invalid, skip it + pass - # remove zip file version if needed. if not include_zips: openpype_versions = [ v for v in openpype_versions if v.path.suffix != ".zip" @@ -1304,9 +1417,8 @@ class BootstrapRepos: raise ValueError( f"version {version} is not associated with any file") - destination = self.data_dir / version.path.stem - if destination.exists(): - assert destination.is_dir() + destination = self.data_dir / f"{version.major}.{version.minor}" / version.path.stem # noqa + if destination.exists() and destination.is_dir(): try: shutil.rmtree(destination) except OSError as e: @@ -1375,7 +1487,7 @@ class BootstrapRepos: else: dir_name = openpype_version.path.stem - destination = self.data_dir / dir_name + destination = self.data_dir / f"{openpype_version.major}.{openpype_version.minor}" / dir_name # noqa # test if destination directory already exist, if so lets delete it. if destination.exists() and force: @@ -1553,14 +1665,18 @@ class BootstrapRepos: return False return True - def get_openpype_versions(self, - openpype_dir: Path, - staging: bool = False) -> list: + def get_openpype_versions( + self, + openpype_dir: Path, + staging: bool = False, + compatible_with: OpenPypeVersion = None) -> list: """Get all detected OpenPype versions in directory. Args: openpype_dir (Path): Directory to scan. staging (bool, optional): Find staging versions if True. + compatible_with (OpenPypeVersion, optional): Get only versions + compatible with the one specified. Returns: list of OpenPypeVersion @@ -1570,7 +1686,7 @@ class BootstrapRepos: """ if not openpype_dir.exists() and not openpype_dir.is_dir(): - raise ValueError("specified directory is invalid") + raise ValueError(f"specified directory {openpype_dir} is invalid") _openpype_versions = [] # iterate over directory in first level and find all that might @@ -1595,6 +1711,10 @@ class BootstrapRepos: ): continue + if compatible_with and \ + not detected_version.is_compatible(compatible_with): + continue + detected_version.path = item if staging and detected_version.is_staging(): _openpype_versions.append(detected_version) From de70521f562084bf5a0cef20179ad2b73efa3bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Aug 2022 18:02:53 +0200 Subject: [PATCH 289/432] :recycle: deadline plugin support for job specific OP versions --- .../custom/plugins/GlobalJobPreLoad.py | 87 +++++++++++++++++-- .../custom/plugins/OpenPype/OpenPype.param | 11 ++- .../custom/plugins/OpenPype/OpenPype.py | 86 +++++++++++++++++- 3 files changed, 171 insertions(+), 13 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index bcd853f374..a43c6c7733 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -6,13 +6,29 @@ import subprocess import json import platform import uuid -from Deadline.Scripting import RepositoryUtils, FileUtils +import re +from Deadline.Scripting import RepositoryUtils, FileUtils, DirectoryUtils + + +def get_openpype_version_from_path(path): + version_file = os.path.join(path, "openpype", "version.py") + if not os.path.isfile(version_file): + return None + version = {} + with open(version_file, "r") as vf: + exec(vf.read(), version) + + version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) + return version_match[1] def get_openpype_executable(): """Return OpenPype Executable from Event Plug-in Settings""" config = RepositoryUtils.GetPluginConfig("OpenPype") - return config.GetConfigEntryWithDefault("OpenPypeExecutable", "") + exe_list = config.GetConfigEntryWithDefault("OpenPypeExecutable", "") + dir_list = config.GetConfigEntryWithDefault( + "OpenPypeInstallationDirs", "") + return exe_list, dir_list def inject_openpype_environment(deadlinePlugin): @@ -25,16 +41,71 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Injecting OpenPype environments ...") try: print(">>> Getting OpenPype executable ...") - exe_list = get_openpype_executable() - openpype_app = FileUtils.SearchFileList(exe_list) - if openpype_app == "": + exe_list, dir_list = get_openpype_executable() + openpype_versions = [] + # if the job requires specific OpenPype version, + # lets go over all available and find compatible build. + requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") + if requested_version: + print(("Scanning for compatible requested " + f"version {requested_version}")) + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if dir: + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = get_openpype_version_from_path(subdir) + if not version: + continue + openpype_versions.append((version, subdir)) + + exe = FileUtils.SearchFileList(exe_list) + if openpype_versions: + # if looking for requested compatible version, + # add the implicitly specified to the list too. + version = get_openpype_version_from_path( + os.path.dirname(exe)) + if version: + openpype_versions.append((version, os.path.dirname(exe))) + + if requested_version: + # sort detected versions + if openpype_versions: + openpype_versions.sort(key=lambda ver: ver[0]) + requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 + compatible_versions = [] + for version in openpype_versions: + v = version[0].split(".")[:3] + if v[0] == requested_major and v[1] == requested_minor: + compatible_versions.append(version) + if not compatible_versions: + raise RuntimeError( + ("Cannot find compatible version available " + "for version {} requested by the job. " + "Please add it through plugin configuration " + "in Deadline or install it to configured " + "directory.").format(requested_version)) + # sort compatible versions nad pick the last one + compatible_versions.sort(key=lambda ver: ver[0]) + # create list of executables for different platform and let + # Deadline decide. + exe_list = [ + os.path.join( + compatible_versions[-1][1], "openpype_console.exe"), + os.path.join( + compatible_versions[-1][1], "openpype_console") + ] + exe = FileUtils.SearchFileList(";".join(exe_list)) + if exe == "": raise RuntimeError( "OpenPype executable was not found " + "in the semicolon separated list \"" + exe_list + "\". " + "The path to the render executable can be configured " + "from the Plugin Configuration in the Deadline Monitor.") - print("--- OpenPype executable: {}".format(openpype_app)) + print("--- OpenPype executable: {}".format(exe)) # tempfile.TemporaryFile cannot be used because of locking temp_file_name = "{}_{}.json".format( @@ -45,7 +116,7 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Temporary path: {}".format(export_url)) args = [ - openpype_app, + exe, "--headless", 'extractenvironments', export_url @@ -77,7 +148,7 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Executing: {}".format(args)) std_output = subprocess.check_output(args, - cwd=os.path.dirname(openpype_app), + cwd=os.path.dirname(exe), env=env) print(">>> Process result {}".format(std_output)) diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param index 8bd6dce12d..b3ac18e20c 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param @@ -7,11 +7,20 @@ Index=0 Default=OpenPype Plugin for Deadline Description=Not configurable +[OpenPypeInstallationDirs] +Type=multilinemultifolder +Label=Directories where OpenPype versions are installed +Category=OpenPype Installation Directories +CategoryOrder=0 +Index=0 +Default=C:\Program Files (x86)\OpenPype +Description=Path or paths to directories where multiple versions of OpenPype might be installed. Enter every such path on separate lines. + [OpenPypeExecutable] Type=multilinemultifilename Label=OpenPype Executable Category=OpenPype Executables -CategoryOrder=0 +CategoryOrder=1 Index=0 Default= Description=The path to the OpenPype executable. Enter alternative paths on separate lines. diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 451d71fb63..b84560f175 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -1,10 +1,18 @@ +#!/usr/bin/env python3 + from System.IO import Path from System.Text.RegularExpressions import Regex from Deadline.Plugins import PluginType, DeadlinePlugin -from Deadline.Scripting import StringUtils, FileUtils, RepositoryUtils +from Deadline.Scripting import ( + StringUtils, + FileUtils, + DirectoryUtils, + RepositoryUtils +) import re +import os ###################################################################### @@ -52,13 +60,83 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): self.AddStdoutHandlerCallback( ".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress + @staticmethod + def get_openpype_version_from_path(path): + version_file = os.path.join(path, "openpype", "version.py") + if not os.path.isfile(version_file): + return None + version = {} + with open(version_file, "r") as vf: + exec(vf.read(), version) + + version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) + return version_match[1] + def RenderExecutable(self): - exeList = self.GetConfigEntry("OpenPypeExecutable") - exe = FileUtils.SearchFileList(exeList) + job = self.GetJob() + openpype_versions = [] + # if the job requires specific OpenPype version, + # lets go over all available and find compatible build. + requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") + if requested_version: + self.LogInfo(( + "Scanning for compatible requested " + f"version {requested_version}")) + dir_list = self.GetConfigEntry("OpenPypeInstallationDirs") + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if dir: + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = self.get_openpype_version_from_path(subdir) + if not version: + continue + openpype_versions.append((version, subdir)) + + exe_list = self.GetConfigEntry("OpenPypeExecutable") + exe = FileUtils.SearchFileList(exe_list) + if openpype_versions: + # if looking for requested compatible version, + # add the implicitly specified to the list too. + version = self.get_openpype_version_from_path( + os.path.dirname(exe)) + if version: + openpype_versions.append((version, os.path.dirname(exe))) + + if requested_version: + # sort detected versions + if openpype_versions: + openpype_versions.sort(key=lambda ver: ver[0]) + requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 + compatible_versions = [] + for version in openpype_versions: + v = version[0].split(".")[:3] + if v[0] == requested_major and v[1] == requested_minor: + compatible_versions.append(version) + if not compatible_versions: + self.FailRender(("Cannot find compatible version available " + "for version {} requested by the job. " + "Please add it through plugin configuration " + "in Deadline or install it to configured " + "directory.").format(requested_version)) + # sort compatible versions nad pick the last one + compatible_versions.sort(key=lambda ver: ver[0]) + # create list of executables for different platform and let + # Deadline decide. + exe_list = [ + os.path.join( + compatible_versions[-1][1], "openpype_console.exe"), + os.path.join( + compatible_versions[-1][1], "openpype_console") + ] + exe = FileUtils.SearchFileList(";".join(exe_list)) + if exe == "": self.FailRender( "OpenPype executable was not found " + - "in the semicolon separated list \"" + exeList + "\". " + + "in the semicolon separated list \"" + exe_list + "\". " + "The path to the render executable can be configured " + "from the Plugin Configuration in the Deadline Monitor.") return exe From 8a55a83d7dc835da2d5f6416aa66686aedb922d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 18:38:54 +0200 Subject: [PATCH 290/432] added settings to be able fill empty intent and define it's label --- .../settings/defaults/system_settings/modules.json | 5 +++-- .../module_settings/schema_ftrack.json | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 8cd4114cb0..a3cf98f3ed 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -26,13 +26,14 @@ "linux": [] }, "intent": { + "allow_empty_intent": true, + "empty_intent_label": "", "items": { - "-": "-", "wip": "WIP", "final": "Final", "test": "Test" }, - "default": "-" + "default": "" }, "custom_attributes": { "show": { diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 654ddf2938..7c5774415c 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -50,8 +50,15 @@ "is_group": true, "children": [ { - "type": "label", - "label": "Intent" + "type": "boolean", + "key": "allow_empty_intent", + "label": "Allow empty intent" + }, + { + "type": "text", + "key": "empty_intent_label", + "label": "Empty item label", + "placeholder": "< Not set >" }, { "type": "dict-modifiable", @@ -64,7 +71,8 @@ { "key": "default", "type": "text", - "label": "Default Intent" + "label": "Default Intent", + "placeholder": "< First available >" }, { "type": "separator" From a591ea92efd534baf14d5f9fc549ba65dabc9894 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 18:39:45 +0200 Subject: [PATCH 291/432] changed model in pype publisher to use new settings --- openpype/tools/pyblish_pype/model.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 2931a379b3..31aa63677e 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -86,7 +86,7 @@ class IntentModel(QtGui.QStandardItemModel): First and default value is {"< Not Set >": None} """ - default_item = {"< Not Set >": None} + default_empty_label = "< Not set >" def __init__(self, parent=None): super(IntentModel, self).__init__(parent) @@ -102,27 +102,39 @@ class IntentModel(QtGui.QStandardItemModel): self._item_count = 0 self.default_index = 0 - intents_preset = ( + intent_settings = ( get_system_settings() .get("modules", {}) .get("ftrack", {}) .get("intent", {}) ) - default = intents_preset.get("default") - items = intents_preset.get("items", {}) + items = intent_settings.get("items", {}) if not items: return - for idx, item_value in enumerate(items.keys()): + allow_empty_intent = intent_settings.get("allow_empty_intent", True) + empty_intent_label = ( + intent_settings.get("empty_intent_label") + or self.default_empty_label + ) + listed_items = list(items.items()) + if allow_empty_intent: + listed_items.insert(0, ("", empty_intent_label)) + + default = intent_settings.get("default") + + for idx, item in enumerate(listed_items): + item_value = item[0] if item_value == default: self.default_index = idx break - self.add_items(items) + self._add_items(listed_items) - def add_items(self, items): - for value, label in items.items(): + def _add_items(self, items): + for item in items: + value, label = item new_item = QtGui.QStandardItem() new_item.setData(label, QtCore.Qt.DisplayRole) new_item.setData(value, Roles.IntentItemValue) From 23601cb2448437be40ac215ef1584080de2a5205 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Aug 2022 18:40:28 +0200 Subject: [PATCH 292/432] unset intent from context if empty item is used --- openpype/tools/pyblish_pype/window.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/tools/pyblish_pype/window.py b/openpype/tools/pyblish_pype/window.py index 78590259bc..e167405325 100644 --- a/openpype/tools/pyblish_pype/window.py +++ b/openpype/tools/pyblish_pype/window.py @@ -523,6 +523,7 @@ class Window(QtWidgets.QDialog): instance_item.setData(enable_value, Roles.IsEnabledRole) def _add_intent_to_context(self): + context_value = None if ( self.intent_model.has_items and "intent" not in self.controller.context.data @@ -530,11 +531,17 @@ class Window(QtWidgets.QDialog): idx = self.intent_model.index(self.intent_box.currentIndex(), 0) intent_value = self.intent_model.data(idx, Roles.IntentItemValue) intent_label = self.intent_model.data(idx, QtCore.Qt.DisplayRole) + if intent_value: + context_value = { + "value": intent_value, + "label": intent_label + } - self.controller.context.data["intent"] = { - "value": intent_value, - "label": intent_label - } + # Unset intent if is set to empty value + if context_value is None: + self.controller.context.data.pop("intent", None) + else: + self.controller.context.data["intent"] = context_value def on_instance_toggle(self, index, state=None): """An item is requesting to be toggled""" From 845d04686f0d586671e12b8bfdeda5b605dc438d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 15:05:13 +0800 Subject: [PATCH 293/432] bugfix for validating look data contents with custom attribute on group --- .../hosts/maya/plugins/publish/validate_look_contents.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 443a0ad719..8aa88a75d3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -76,12 +76,12 @@ class ValidateLookContents(pyblish.api.InstancePlugin): "`relationships`" % instance.name) invalid.add(instance.name) - # Check if attributes are on a node with an ID, crucial for rebuild! + # Check if attributes are on a node with a name and an ID, crucial for rebuild! for attr_changes in lookdata["attributes"]: - if not attr_changes["uuid"]: + if not attr_changes["uuid"] and not attr_changes["name"]: cls.log.error("Node '%s' has no cbId, please set the " - "attributes to its children if it has any" - % attr_changes["name"]) + "attributes to its children if it has any" + % attr_changes["name"]) invalid.add(instance.name) return list(invalid) From 674b3900ac56607392e28be1e7f444a62e24b2ac Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 15:07:38 +0800 Subject: [PATCH 294/432] bugfix for validating look data contents with custom attribute on group --- openpype/hosts/maya/plugins/publish/validate_look_contents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 8aa88a75d3..9eb965970a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -76,9 +76,9 @@ class ValidateLookContents(pyblish.api.InstancePlugin): "`relationships`" % instance.name) invalid.add(instance.name) - # Check if attributes are on a node with a name and an ID, crucial for rebuild! + # Check if attributes are on a node with an ID, crucial for rebuild! for attr_changes in lookdata["attributes"]: - if not attr_changes["uuid"] and not attr_changes["name"]: + if not attr_changes["uuid"] and not attr_changes["attributes"]: cls.log.error("Node '%s' has no cbId, please set the " "attributes to its children if it has any" % attr_changes["name"]) From 8120e9d66bbd911a4e4722e6a1fb5c06a572af71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 15:31:33 +0800 Subject: [PATCH 295/432] bugfix for validating look data contents with custom attribute on group --- .../hosts/maya/plugins/publish/validate_look_contents.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 9eb965970a..01d7a9ef2f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -76,16 +76,15 @@ class ValidateLookContents(pyblish.api.InstancePlugin): "`relationships`" % instance.name) invalid.add(instance.name) - # Check if attributes are on a node with an ID, crucial for rebuild! + # Check if attributes are on a node with an attirbute and an ID, crucial for rebuild! for attr_changes in lookdata["attributes"]: if not attr_changes["uuid"] and not attr_changes["attributes"]: cls.log.error("Node '%s' has no cbId, please set the " - "attributes to its children if it has any" - % attr_changes["name"]) + "attributes to its children if it has any" + % attr_changes["name"]) invalid.add(instance.name) return list(invalid) - @classmethod def validate_looks(cls, instance): From 39975a7335f1c27c3764518a37ac0c304b347363 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 15:32:35 +0800 Subject: [PATCH 296/432] bugfix for validating look data contents with custom attribute on group --- openpype/hosts/maya/plugins/publish/validate_look_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 01d7a9ef2f..b1e1d5416b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -76,7 +76,7 @@ class ValidateLookContents(pyblish.api.InstancePlugin): "`relationships`" % instance.name) invalid.add(instance.name) - # Check if attributes are on a node with an attirbute and an ID, crucial for rebuild! + # Check if attributes are on a node with an ID, crucial for rebuild! for attr_changes in lookdata["attributes"]: if not attr_changes["uuid"] and not attr_changes["attributes"]: cls.log.error("Node '%s' has no cbId, please set the " From 3d7e1953075809af9323951046fc3d321da8352b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 2 Aug 2022 11:26:33 +0200 Subject: [PATCH 297/432] :recycle: skip non-existent local path when finding local version, stop crashing if directory to search doesn't exist - this will allow to just use build version --- igniter/bootstrap_repos.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 47f2525952..750b2f1bf7 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -514,10 +514,10 @@ class OpenPypeVersion(semver.VersionInfo): ValueError: if invalid path is specified. """ - if not openpype_dir.exists() and not openpype_dir.is_dir(): - raise ValueError("specified directory is invalid") - _openpype_versions = [] + if not openpype_dir.exists() and not openpype_dir.is_dir(): + return _openpype_versions + # iterate over directory in first level and find all that might # contain OpenPype. for item in openpype_dir.iterdir(): From 89bd23856c30e39f2493d99b2c743d3b918cccda Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Aug 2022 12:25:51 +0200 Subject: [PATCH 298/432] OP-3405 - refactor - updated methods signature Renamed collection to project_name as when we are leaving MongoDB, collection doesnt make much sense. --- .../providers/abstract_provider.py | 8 +- .../modules/sync_server/providers/dropbox.py | 12 +- .../modules/sync_server/providers/gdrive.py | 16 +- .../sync_server/providers/local_drive.py | 12 +- .../modules/sync_server/providers/sftp.py | 16 +- openpype/modules/sync_server/sync_server.py | 71 +++---- .../modules/sync_server/sync_server_module.py | 189 +++++++++--------- openpype/modules/sync_server/tray/models.py | 2 +- 8 files changed, 164 insertions(+), 162 deletions(-) diff --git a/openpype/modules/sync_server/providers/abstract_provider.py b/openpype/modules/sync_server/providers/abstract_provider.py index 688a17f14f..8c2fe1cad9 100644 --- a/openpype/modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/sync_server/providers/abstract_provider.py @@ -62,7 +62,7 @@ class AbstractProvider: @abc.abstractmethod def upload_file(self, source_path, path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Copy file from 'source_path' to 'target_path' on provider. @@ -75,7 +75,7 @@ class AbstractProvider: arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): name of project_name file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -87,7 +87,7 @@ class AbstractProvider: @abc.abstractmethod def download_file(self, source_path, local_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Download file from provider into local system @@ -99,7 +99,7 @@ class AbstractProvider: arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name diff --git a/openpype/modules/sync_server/providers/dropbox.py b/openpype/modules/sync_server/providers/dropbox.py index dfc42fed75..89d6990841 100644 --- a/openpype/modules/sync_server/providers/dropbox.py +++ b/openpype/modules/sync_server/providers/dropbox.py @@ -224,7 +224,7 @@ class DropboxHandler(AbstractProvider): return False def upload_file(self, source_path, path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Copy file from 'source_path' to 'target_path' on provider. @@ -237,7 +237,7 @@ class DropboxHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -290,7 +290,7 @@ class DropboxHandler(AbstractProvider): cursor.offset = f.tell() server.update_db( - collection=collection, + project_name=project_name, new_file_id=None, file=file, representation=representation, @@ -301,7 +301,7 @@ class DropboxHandler(AbstractProvider): return path def download_file(self, source_path, local_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Download file from provider into local system @@ -313,7 +313,7 @@ class DropboxHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -337,7 +337,7 @@ class DropboxHandler(AbstractProvider): self.dbx.files_download_to_file(local_path, source_path) server.update_db( - collection=collection, + project_name=project_name, new_file_id=None, file=file, representation=representation, diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index aa7329b104..bef707788b 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -251,7 +251,7 @@ class GDriveHandler(AbstractProvider): return folder_id def upload_file(self, source_path, path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Uploads single file from 'source_path' to destination 'path'. @@ -264,7 +264,7 @@ class GDriveHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -324,7 +324,7 @@ class GDriveHandler(AbstractProvider): while response is None: if server.is_representation_paused(representation['_id'], check_parents=True, - project_name=collection): + project_name=project_name): raise ValueError("Paused during process, please redo.") if status: status_val = float(status.progress()) @@ -333,7 +333,7 @@ class GDriveHandler(AbstractProvider): last_tick = time.time() log.debug("Uploaded %d%%." % int(status_val * 100)) - server.update_db(collection=collection, + server.update_db(project_name=project_name, new_file_id=None, file=file, representation=representation, @@ -358,7 +358,7 @@ class GDriveHandler(AbstractProvider): return response['id'] def download_file(self, source_path, local_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Downloads single file from 'source_path' (remote) to 'local_path'. @@ -372,7 +372,7 @@ class GDriveHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -410,7 +410,7 @@ class GDriveHandler(AbstractProvider): while response is None: if server.is_representation_paused(representation['_id'], check_parents=True, - project_name=collection): + project_name=project_name): raise ValueError("Paused during process, please redo.") if status: status_val = float(status.progress()) @@ -419,7 +419,7 @@ class GDriveHandler(AbstractProvider): last_tick = time.time() log.debug("Downloaded %d%%." % int(status_val * 100)) - server.update_db(collection=collection, + server.update_db(project_name=project_name, new_file_id=None, file=file, representation=representation, diff --git a/openpype/modules/sync_server/providers/local_drive.py b/openpype/modules/sync_server/providers/local_drive.py index 172cb338cf..4951ef4d1a 100644 --- a/openpype/modules/sync_server/providers/local_drive.py +++ b/openpype/modules/sync_server/providers/local_drive.py @@ -82,7 +82,7 @@ class LocalDriveHandler(AbstractProvider): return editable def upload_file(self, source_path, target_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False, direction="Upload"): """ Copies file from 'source_path' to 'target_path' @@ -95,7 +95,7 @@ class LocalDriveHandler(AbstractProvider): thread = threading.Thread(target=self._copy, args=(source_path, target_path)) thread.start() - self._mark_progress(collection, file, representation, server, + self._mark_progress(project_name, file, representation, server, site, source_path, target_path, direction) else: if os.path.exists(target_path): @@ -105,13 +105,13 @@ class LocalDriveHandler(AbstractProvider): return os.path.basename(target_path) def download_file(self, source_path, local_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Download a file form 'source_path' to 'local_path' """ return self.upload_file(source_path, local_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite, direction="Download") def delete_file(self, path): @@ -188,7 +188,7 @@ class LocalDriveHandler(AbstractProvider): except shutil.SameFileError: print("same files, skipping") - def _mark_progress(self, collection, file, representation, server, site, + def _mark_progress(self, project_name, file, representation, server, site, source_path, target_path, direction): """ Updates progress field in DB by values 0-1. @@ -204,7 +204,7 @@ class LocalDriveHandler(AbstractProvider): status_val = target_file_size / source_file_size last_tick = time.time() log.debug(direction + "ed %d%%." % int(status_val * 100)) - server.update_db(collection=collection, + server.update_db(project_name=project_name, new_file_id=None, file=file, representation=representation, diff --git a/openpype/modules/sync_server/providers/sftp.py b/openpype/modules/sync_server/providers/sftp.py index 49b87b14ec..302ffae3e6 100644 --- a/openpype/modules/sync_server/providers/sftp.py +++ b/openpype/modules/sync_server/providers/sftp.py @@ -222,7 +222,7 @@ class SFTPHandler(AbstractProvider): return os.path.basename(path) def upload_file(self, source_path, target_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Uploads single file from 'source_path' to destination 'path'. @@ -235,7 +235,7 @@ class SFTPHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -256,7 +256,7 @@ class SFTPHandler(AbstractProvider): thread = threading.Thread(target=self._upload, args=(source_path, target_path)) thread.start() - self._mark_progress(collection, file, representation, server, + self._mark_progress(project_name, file, representation, server, site, source_path, target_path, "upload") return os.path.basename(target_path) @@ -267,7 +267,7 @@ class SFTPHandler(AbstractProvider): conn.put(source_path, target_path) def download_file(self, source_path, target_path, - server, collection, file, representation, site, + server, project_name, file, representation, site, overwrite=False): """ Downloads single file from 'source_path' (remote) to 'target_path'. @@ -281,7 +281,7 @@ class SFTPHandler(AbstractProvider): arguments for saving progress: server (SyncServer): server instance to call update_db on - collection (str): name of collection + project_name (str): file (dict): info about uploaded file (matches structure from db) representation (dict): complete repre containing 'file' site (str): site name @@ -302,7 +302,7 @@ class SFTPHandler(AbstractProvider): thread = threading.Thread(target=self._download, args=(source_path, target_path)) thread.start() - self._mark_progress(collection, file, representation, server, + self._mark_progress(project_name, file, representation, server, site, source_path, target_path, "download") return os.path.basename(target_path) @@ -425,7 +425,7 @@ class SFTPHandler(AbstractProvider): pysftp.exceptions.ConnectionException): log.warning("Couldn't connect", exc_info=True) - def _mark_progress(self, collection, file, representation, server, site, + def _mark_progress(self, project_name, file, representation, server, site, source_path, target_path, direction): """ Updates progress field in DB by values 0-1. @@ -446,7 +446,7 @@ class SFTPHandler(AbstractProvider): status_val = target_file_size / source_file_size last_tick = time.time() log.debug(direction + "ed %d%%." % int(status_val * 100)) - server.update_db(collection=collection, + server.update_db(project_name=project_name, new_file_id=None, file=file, representation=representation, diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 356a75f99d..9cc55ec562 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -14,7 +14,7 @@ from .utils import SyncStatus, ResumableError log = PypeLogger().get_logger("SyncServer") -async def upload(module, collection, file, representation, provider_name, +async def upload(module, project_name, file, representation, provider_name, remote_site_name, tree=None, preset=None): """ Upload single 'file' of a 'representation' to 'provider'. @@ -31,7 +31,7 @@ async def upload(module, collection, file, representation, provider_name, Args: module(SyncServerModule): object to run SyncServerModule API - collection (str): source collection + project_name (str): source db file (dictionary): of file from representation in Mongo representation (dictionary): of representation provider_name (string): gdrive, gdc etc. @@ -47,7 +47,7 @@ async def upload(module, collection, file, representation, provider_name, # thread can do that at a time, upload/download to prepared # structure should be run in parallel remote_handler = lib.factory.get_provider(provider_name, - collection, + project_name, remote_site_name, tree=tree, presets=preset) @@ -55,7 +55,7 @@ async def upload(module, collection, file, representation, provider_name, file_path = file.get("path", "") try: local_file_path, remote_file_path = resolve_paths(module, - file_path, collection, remote_site_name, remote_handler + file_path, project_name, remote_site_name, remote_handler ) except Exception as exp: print(exp) @@ -74,27 +74,28 @@ async def upload(module, collection, file, representation, provider_name, local_file_path, remote_file_path, module, - collection, + project_name, file, representation, remote_site_name, True ) - module.handle_alternate_site(collection, representation, remote_site_name, + module.handle_alternate_site(project_name, representation, + remote_site_name, file["_id"], file_id) return file_id -async def download(module, collection, file, representation, provider_name, +async def download(module, project_name, file, representation, provider_name, remote_site_name, tree=None, preset=None): """ Downloads file to local folder denoted in representation.Context. Args: module(SyncServerModule): object to run SyncServerModule API - collection (str): source collection + project_name (str): source file (dictionary) : info about processed file representation (dictionary): repr that 'file' belongs to provider_name (string): 'gdrive' etc @@ -108,20 +109,20 @@ async def download(module, collection, file, representation, provider_name, """ with module.lock: remote_handler = lib.factory.get_provider(provider_name, - collection, + project_name, remote_site_name, tree=tree, presets=preset) file_path = file.get("path", "") local_file_path, remote_file_path = resolve_paths( - module, file_path, collection, remote_site_name, remote_handler + module, file_path, project_name, remote_site_name, remote_handler ) local_folder = os.path.dirname(local_file_path) os.makedirs(local_folder, exist_ok=True) - local_site = module.get_active_site(collection) + local_site = module.get_active_site(project_name) loop = asyncio.get_running_loop() file_id = await loop.run_in_executor(None, @@ -129,20 +130,20 @@ async def download(module, collection, file, representation, provider_name, remote_file_path, local_file_path, module, - collection, + project_name, file, representation, local_site, True ) - module.handle_alternate_site(collection, representation, local_site, + module.handle_alternate_site(project_name, representation, local_site, file["_id"], file_id) return file_id -def resolve_paths(module, file_path, collection, +def resolve_paths(module, file_path, project_name, remote_site_name=None, remote_handler=None): """ Returns tuple of local and remote file paths with {root} @@ -153,7 +154,7 @@ def resolve_paths(module, file_path, collection, Args: module(SyncServerModule): object to run SyncServerModule API file_path(string): path with {root} - collection(string): project name + project_name(string): project name remote_site_name(string): remote site remote_handler(AbstractProvider): implementation Returns: @@ -164,7 +165,7 @@ def resolve_paths(module, file_path, collection, remote_file_path = remote_handler.resolve_path(file_path) local_handler = lib.factory.get_provider( - 'local_drive', collection, module.get_active_site(collection)) + 'local_drive', project_name, module.get_active_site(project_name)) local_file_path = local_handler.resolve_path(file_path) return local_file_path, remote_file_path @@ -269,7 +270,7 @@ class SyncServerThread(threading.Thread): - gets list of collections in DB - gets list of active remote providers (has configuration, credentials) - - for each collection it looks for representations that should + - for each project_name it looks for representations that should be synced - synchronize found collections - update representations - fills error messages for exceptions @@ -282,17 +283,17 @@ class SyncServerThread(threading.Thread): import time start_time = time.time() self.module.set_sync_project_settings() # clean cache - collection = None + project_name = None enabled_projects = self.module.get_enabled_projects() - for collection in enabled_projects: - preset = self.module.sync_project_settings[collection] + for project_name in enabled_projects: + preset = self.module.sync_project_settings[project_name] - local_site, remote_site = self._working_sites(collection) + local_site, remote_site = self._working_sites(project_name) if not all([local_site, remote_site]): continue sync_repres = self.module.get_sync_representations( - collection, + project_name, local_site, remote_site ) @@ -310,7 +311,7 @@ class SyncServerThread(threading.Thread): remote_provider = \ self.module.get_provider_for_site(site=remote_site) handler = lib.factory.get_provider(remote_provider, - collection, + project_name, remote_site, presets=site_preset) limit = lib.factory.get_provider_batch_limit( @@ -341,7 +342,7 @@ class SyncServerThread(threading.Thread): limit -= 1 task = asyncio.create_task( upload(self.module, - collection, + project_name, file, sync, remote_provider, @@ -353,7 +354,7 @@ class SyncServerThread(threading.Thread): files_processed_info.append((file, sync, remote_site, - collection + project_name )) processed_file_path.add(file_path) if status == SyncStatus.DO_DOWNLOAD: @@ -361,7 +362,7 @@ class SyncServerThread(threading.Thread): limit -= 1 task = asyncio.create_task( download(self.module, - collection, + project_name, file, sync, remote_provider, @@ -373,7 +374,7 @@ class SyncServerThread(threading.Thread): files_processed_info.append((file, sync, local_site, - collection + project_name )) processed_file_path.add(file_path) @@ -384,12 +385,12 @@ class SyncServerThread(threading.Thread): return_exceptions=True) for file_id, info in zip(files_created, files_processed_info): - file, representation, site, collection = info + file, representation, site, project_name = info error = None if isinstance(file_id, BaseException): error = str(file_id) file_id = None - self.module.update_db(collection, + self.module.update_db(project_name, file_id, file, representation, @@ -399,7 +400,7 @@ class SyncServerThread(threading.Thread): duration = time.time() - start_time log.debug("One loop took {:.2f}s".format(duration)) - delay = self.module.get_loop_delay(collection) + delay = self.module.get_loop_delay(project_name) log.debug("Waiting for {} seconds to new loop".format(delay)) self.timer = asyncio.create_task(self.run_timer(delay)) await asyncio.gather(self.timer) @@ -458,19 +459,19 @@ class SyncServerThread(threading.Thread): self.timer.cancel() self.timer = None - def _working_sites(self, collection): - if self.module.is_project_paused(collection): + def _working_sites(self, project_name): + if self.module.is_project_paused(project_name): log.debug("Both sites same, skipping") return None, None - local_site = self.module.get_active_site(collection) - remote_site = self.module.get_remote_site(collection) + local_site = self.module.get_active_site(project_name) + remote_site = self.module.get_remote_site(project_name) if local_site == remote_site: log.debug("{}-{} sites same, skipping".format(local_site, remote_site)) return None, None - configured_sites = _get_configured_sites(self.module, collection) + configured_sites = _get_configured_sites(self.module, project_name) if not all([local_site in configured_sites, remote_site in configured_sites]): log.debug("Some of the sites {} - {} is not ".format(local_site, diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 71e35c7839..c4d90416bb 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -130,12 +130,12 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.projects_processed = set() """ Start of Public API """ - def add_site(self, collection, representation_id, site_name=None, + def add_site(self, project_name, representation_id, site_name=None, force=False): """ Adds new site to representation to be synced. - 'collection' must have synchronization enabled (globally or + 'project_name' must have synchronization enabled (globally or project only) Used as a API endpoint from outside applications (Loader etc). @@ -143,7 +143,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Use 'force' to reset existing site. Args: - collection (string): project name (must match DB) + project_name (string): project name (must match DB) representation_id (string): MongoDB _id value site_name (string): name of configured and active site force (bool): reset site if exists @@ -153,25 +153,25 @@ class SyncServerModule(OpenPypeModule, ITrayModule): not 'force' ValueError - other errors (repre not found, misconfiguration) """ - if not self.get_sync_project_setting(collection): + if not self.get_sync_project_setting(project_name): raise ValueError("Project not configured") if not site_name: site_name = self.DEFAULT_SITE - self.reset_site_on_representation(collection, + self.reset_site_on_representation(project_name, representation_id, site_name=site_name, force=force) - def remove_site(self, collection, representation_id, site_name, + def remove_site(self, project_name, representation_id, site_name, remove_local_files=False): """ Removes 'site_name' for particular 'representation_id' on - 'collection' + 'project_name' Args: - collection (string): project name (must match DB) + project_name (string): project name (must match DB) representation_id (string): MongoDB _id value site_name (string): name of configured and active site remove_local_files (bool): remove only files for 'local_id' @@ -180,15 +180,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Returns: throws ValueError if any issue """ - if not self.get_sync_project_setting(collection): + if not self.get_sync_project_setting(project_name): raise ValueError("Project not configured") - self.reset_site_on_representation(collection, + self.reset_site_on_representation(project_name, representation_id, site_name=site_name, remove=True) if remove_local_files: - self._remove_local_file(collection, representation_id, site_name) + self._remove_local_file(project_name, representation_id, site_name) def compute_resource_sync_sites(self, project_name): """Get available resource sync sites state for publish process. @@ -335,9 +335,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return alt_site_pairs - def clear_project(self, collection, site_name): + def clear_project(self, project_name, site_name): """ - Clear 'collection' of 'site_name' and its local files + Clear 'project_name' of 'site_name' and its local files Works only on real local sites, not on 'studio' """ @@ -348,15 +348,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # TODO currently not possible to replace with get_representations representations = list( - self.connection.database[collection].find(query)) + self.connection.database[project_name].find(query)) if not representations: self.log.debug("No repre found") return for repre in representations: - self.remove_site(collection, repre.get("_id"), site_name, True) + self.remove_site(project_name, repre.get("_id"), site_name, True) - def create_validate_project_task(self, collection, site_name): + def create_validate_project_task(self, project_name, site_name): """Adds metadata about project files validation on a queue. This process will loop through all representation and check if @@ -373,28 +373,28 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ task = { "type": "validate", - "project_name": collection, - "func": lambda: self.validate_project(collection, site_name, + "project_name": project_name, + "func": lambda: self.validate_project(project_name, site_name, reset_missing=True) } - self.projects_processed.add(collection) + self.projects_processed.add(project_name) self.long_running_tasks.append(task) - def validate_project(self, collection, site_name, reset_missing=False): - """Validate 'collection' of 'site_name' and its local files + def validate_project(self, project_name, site_name, reset_missing=False): + """Validate 'project_name' of 'site_name' and its local files If file present and not marked with a 'site_name' in DB, DB is updated with site name and file modified date. Args: - collection (string): project name + project_name (string): project name site_name (string): active site name reset_missing (bool): if True reset site in DB if missing physically """ - self.log.debug("Validation of {} for {} started".format(collection, + self.log.debug("Validation of {} for {} started".format(project_name, site_name)) - representations = list(get_representations(collection)) + representations = list(get_representations(project_name)) if not representations: self.log.debug("No repre found") return @@ -414,7 +414,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): continue file_path = repre_file.get("path", "") - local_file_path = self.get_local_file_path(collection, + local_file_path = self.get_local_file_path(project_name, site_name, file_path) @@ -426,14 +426,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): "Adding site {} for {}".format(site_name, repre_id)) - query = { - "_id": repre_id - } created_dt = datetime.fromtimestamp( os.path.getmtime(local_file_path)) elem = {"name": site_name, "created_dt": created_dt} - self._add_site(collection, query, repre, elem, + self._add_site(project_name, repre, elem, site_name=site_name, file_id=repre_file["_id"], force=True) @@ -443,41 +440,42 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.log.debug("Resetting site {} for {}". format(site_name, repre_id)) self.reset_site_on_representation( - collection, repre_id, site_name=site_name, + project_name, repre_id, site_name=site_name, file_id=repre_file["_id"]) sites_reset += 1 if sites_added % 100 == 0: self.log.debug("Sites added {}".format(sites_added)) - self.log.debug("Validation of {} for {} ended".format(collection, + self.log.debug("Validation of {} for {} ended".format(project_name, site_name)) self.log.info("Sites added {}, sites reset {}".format(sites_added, reset_missing)) - def pause_representation(self, collection, representation_id, site_name): + def pause_representation(self, project_name, representation_id, site_name): """ Sets 'representation_id' as paused, eg. no syncing should be happening on it. Args: - collection (string): project name + project_name (string): project name representation_id (string): MongoDB objectId value site_name (string): 'gdrive', 'studio' etc. """ log.info("Pausing SyncServer for {}".format(representation_id)) self._paused_representations.add(representation_id) - self.reset_site_on_representation(collection, representation_id, + self.reset_site_on_representation(project_name, representation_id, site_name=site_name, pause=True) - def unpause_representation(self, collection, representation_id, site_name): + def unpause_representation(self, project_name, + representation_id, site_name): """ Sets 'representation_id' as unpaused. Does not fail or warn if repre wasn't paused. Args: - collection (string): project name + project_name (string): project name representation_id (string): MongoDB objectId value site_name (string): 'gdrive', 'studio' etc. """ @@ -487,7 +485,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): except KeyError: pass # self.paused_representations is not persistent - self.reset_site_on_representation(collection, representation_id, + self.reset_site_on_representation(project_name, representation_id, site_name=site_name, pause=False) def is_representation_paused(self, representation_id, @@ -518,7 +516,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): happening on all representation inside. Args: - project_name (string): collection name + project_name (string): project_name name """ log.info("Pausing SyncServer for {}".format(project_name)) self._paused_projects.add(project_name) @@ -530,7 +528,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Does not fail or warn if project wasn't paused. Args: - project_name (string): collection name + project_name (string): """ log.info("Unpausing SyncServer for {}".format(project_name)) try: @@ -543,7 +541,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Returns if 'project_name' is paused or not. Args: - project_name (string): collection name + project_name (string): check_parents (bool): check if server itself is not paused Returns: @@ -942,8 +940,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return True return False - def handle_alternate_site(self, collection, representation, processed_site, - file_id, synced_file_id): + def handle_alternate_site(self, project_name, representation, + processed_site, file_id, synced_file_id): """ For special use cases where one site vendors another. @@ -956,7 +954,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): same location >> file is accesible on 'sftp' site right away. Args: - collection (str): name of project + project_name (str): name of project representation (dict) processed_site (str): real site_name of published/uploaded file file_id (ObjectId): DB id of file handled @@ -980,26 +978,23 @@ class SyncServerModule(OpenPypeModule, ITrayModule): alternate_sites = set(alternate_sites) for alt_site in alternate_sites: - query = { - "_id": representation["_id"] - } elem = {"name": alt_site, "created_dt": datetime.now(), "id": synced_file_id} self.log.debug("Adding alternate {} to {}".format( alt_site, representation["_id"])) - self._add_site(collection, query, + self._add_site(project_name, representation, elem, alt_site, file_id=file_id, force=True) """ End of Public API """ - def get_local_file_path(self, collection, site_name, file_path): + def get_local_file_path(self, project_name, site_name, file_path): """ Externalized for app """ - handler = LocalDriveHandler(collection, site_name) + handler = LocalDriveHandler(project_name, site_name) local_file_path = handler.resolve_path(file_path) return local_file_path @@ -1286,7 +1281,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return sites.get(site, 'N/A') @time_function - def get_sync_representations(self, collection, active_site, remote_site): + def get_sync_representations(self, project_name, active_site, remote_site): """ Get representations that should be synced, these could be recognised by presence of document in 'files.sites', where key is @@ -1297,8 +1292,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): better performance. Goal is to get as few representations as possible. Args: - collection (string): name of collection (in most cases matches - project name + project_name (string): active_site (string): identifier of current active site (could be 'local_0' when working from home, 'studio' when working in the studio (default) @@ -1307,10 +1301,10 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Returns: (list) of dictionaries """ - log.debug("Check representations for : {}".format(collection)) - self.connection.Session["AVALON_PROJECT"] = collection + log.debug("Check representations for : {}".format(project_name)) + self.connection.Session["AVALON_PROJECT"] = project_name # retry_cnt - number of attempts to sync specific file before giving up - retries_arr = self._get_retries_arr(collection) + retries_arr = self._get_retries_arr(project_name) match = { "type": "representation", "$or": [ @@ -1447,14 +1441,14 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return SyncStatus.DO_NOTHING - def update_db(self, collection, new_file_id, file, representation, + def update_db(self, project_name, new_file_id, file, representation, site, error=None, progress=None, priority=None): """ Update 'provider' portion of records in DB with success (file_id) or error (exception) Args: - collection (string): name of project - force to db connection as + project_name (string): name of project - force to db connection as each file might come from different collection new_file_id (string): file (dictionary): info about processed file (pulled from DB) @@ -1497,7 +1491,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if file_id: arr_filter.append({'f._id': ObjectId(file_id)}) - self.connection.database[collection].update_one( + self.connection.database[project_name].update_one( query, update, upsert=True, @@ -1560,7 +1554,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return -1, None - def reset_site_on_representation(self, collection, representation_id, + def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, remove=False, pause=None, force=False): """ @@ -1577,7 +1571,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Should be used when repre should be synced to new site. Args: - collection (string): name of project (eg. collection) in DB + project_name (string): name of project (eg. collection) in DB representation_id(string): _id of representation file_id (string): file _id in representation side (string): local or remote side @@ -1591,18 +1585,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule): not 'force' ValueError - other errors (repre not found, misconfiguration) """ - representation = get_representation_by_id(collection, + representation = get_representation_by_id(project_name, representation_id) if not representation: raise ValueError("Representation {} not found in {}". - format(representation_id, collection)) + format(representation_id, project_name)) if side and site_name: raise ValueError("Misconfiguration, only one of side and " + "site_name arguments should be passed.") - local_site = self.get_active_site(collection) - remote_site = self.get_remote_site(collection) + local_site = self.get_active_site(project_name) + remote_site = self.get_remote_site(project_name) if side: if side == 'local': @@ -1612,42 +1606,44 @@ class SyncServerModule(OpenPypeModule, ITrayModule): elem = {"name": site_name} - query = { - "_id": ObjectId(representation_id) - } - if file_id: # reset site for particular file - self._reset_site_for_file(collection, query, + self._reset_site_for_file(project_name, representation_id, elem, file_id, site_name) elif side: # reset site for whole representation - self._reset_site(collection, query, elem, site_name) + self._reset_site(project_name, representation_id, elem, site_name) elif remove: # remove site for whole representation - self._remove_site(collection, query, representation, site_name) + self._remove_site(project_name, + representation, site_name) elif pause is not None: - self._pause_unpause_site(collection, query, + self._pause_unpause_site(project_name, representation, site_name, pause) else: # add new site to all files for representation - self._add_site(collection, query, representation, elem, site_name, + self._add_site(project_name, representation, elem, site_name, force=force) - def _update_site(self, collection, query, update, arr_filter): + def _update_site(self, project_name, representation_id, + update, arr_filter): """ Auxiliary method to call update_one function on DB Used for refactoring ugly reset_provider_for_file """ - self.connection.database[collection].update_one( + query = { + "_id": ObjectId(representation_id) + } + + self.connection.database[project_name].update_one( query, update, upsert=True, array_filters=arr_filter ) - def _reset_site_for_file(self, collection, query, + def _reset_site_for_file(self, project_name, representation_id, elem, file_id, site_name): """ Resets 'site_name' for 'file_id' on representation in 'query' on - 'collection' + 'project_name' """ update = { "$set": {"files.$[f].sites.$[s]": elem} @@ -1660,9 +1656,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): {'f._id': file_id} ] - self._update_site(collection, query, update, arr_filter) + self._update_site(project_name, representation_id, update, arr_filter) - def _reset_site(self, collection, query, elem, site_name): + def _reset_site(self, project_name, representation_id, elem, site_name): """ Resets 'site_name' for all files of representation in 'query' """ @@ -1674,9 +1670,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): {'s.name': site_name} ] - self._update_site(collection, query, update, arr_filter) + self._update_site(project_name, representation_id, update, arr_filter) - def _remove_site(self, collection, query, representation, site_name): + def _remove_site(self, project_name, representation, site_name): """ Removes 'site_name' for 'representation' in 'query' @@ -1698,10 +1694,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): } arr_filter = [] - self._update_site(collection, query, update, arr_filter) + self._update_site(project_name, representation["_id"], + update, arr_filter) - def _pause_unpause_site(self, collection, query, - representation, site_name, pause): + def _pause_unpause_site(self, project_name, representation, + site_name, pause): """ Pauses/unpauses all files for 'representation' based on 'pause' @@ -1733,12 +1730,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): {'s.name': site_name} ] - self._update_site(collection, query, update, arr_filter) + self._update_site(project_name, representation["_id"], + update, arr_filter) - def _add_site(self, collection, query, representation, elem, site_name, + def _add_site(self, project_name, representation, elem, site_name, force=False, file_id=None): """ - Adds 'site_name' to 'representation' on 'collection' + Adds 'site_name' to 'representation' on 'project_name' Args: representation (dict) @@ -1746,10 +1744,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Use 'force' to remove existing or raises ValueError """ + representation_id = representation["_id"] reset_existing = False files = representation.get("files", []) if not files: - log.debug("No files for {}".format(representation["_id"])) + log.debug("No files for {}".format(representation_id)) return for repre_file in files: @@ -1759,7 +1758,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): for site in repre_file.get("sites"): if site["name"] == site_name: if force or site.get("error"): - self._reset_site_for_file(collection, query, + self._reset_site_for_file(project_name, + representation_id, elem, repre_file["_id"], site_name) reset_existing = True @@ -1785,14 +1785,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): {'f._id': file_id} ] - self._update_site(collection, query, update, arr_filter) + self._update_site(project_name, representation_id, + update, arr_filter) - def _remove_local_file(self, collection, representation_id, site_name): + def _remove_local_file(self, project_name, representation_id, site_name): """ Removes all local files for 'site_name' of 'representation_id' Args: - collection (string): project name (must match DB) + project_name (string): project name (must match DB) representation_id (string): MongoDB _id value site_name (string): name of configured and active site @@ -1808,7 +1809,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): provider_name = self.get_provider_for_site(site=site_name) if provider_name == 'local_drive': - representation = get_representation_by_id(collection, + representation = get_representation_by_id(project_name, representation_id, fields=["files"]) if not representation: @@ -1818,7 +1819,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): local_file_path = '' for file in representation.get("files"): - local_file_path = self.get_local_file_path(collection, + local_file_path = self.get_local_file_path(project_name, site_name, file.get("path", "") ) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index a97797c920..f05a5bd8ea 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -441,7 +441,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): full text filtering. Allows pagination, most of heavy lifting is being done on DB side. - Single model matches to single collection. When project is changed, + Single model matches to single project. When project is changed, model is reset and refreshed. Args: From 2a0e377ff4288a47efa184e51dd64a5158eeee62 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 20:08:37 +0800 Subject: [PATCH 299/432] introduce a condition to exclude the unneccessary node attributes during collecting looks --- openpype/hosts/maya/plugins/publish/collect_look.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index ec583bcce7..4a14fc4451 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -551,7 +551,11 @@ class CollectLook(pyblish.api.InstancePlugin): if cmds.getAttr(attribute, type=True) == "message": continue node_attributes[attr] = cmds.getAttr(attribute) - + + # Only include if there are any properties we care about + if not node_attributes: + continue + attributes.append({"name": node, "uuid": lib.get_id(node), "attributes": node_attributes}) From 7f356587e38051dfb2ffb515af896a5bd916105c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Aug 2022 20:09:58 +0800 Subject: [PATCH 300/432] introduce a condition to exclude the unneccessary node attributes during collecting looks --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 4a14fc4451..157be5717b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -551,11 +551,9 @@ class CollectLook(pyblish.api.InstancePlugin): if cmds.getAttr(attribute, type=True) == "message": continue node_attributes[attr] = cmds.getAttr(attribute) - # Only include if there are any properties we care about if not node_attributes: continue - attributes.append({"name": node, "uuid": lib.get_id(node), "attributes": node_attributes}) From eb2c82558888fe5650bdab4bee1a60a498b685fa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Aug 2022 16:09:59 +0200 Subject: [PATCH 301/432] OP-3405 - extracted aggregate query from Loader to Site Sync module --- .../modules/sync_server/sync_server_module.py | 89 +++++++++++++++++ openpype/tools/loader/model.py | 95 ++----------------- 2 files changed, 98 insertions(+), 86 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index c4d90416bb..8fdfab9c2e 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -988,6 +988,95 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation, elem, alt_site, file_id=file_id, force=True) + def get_repre_info_for_versions(self, project_name, version_ids, + active_site, remote_site): + """Returns representation documents for versions and sites combi + + Args: + project_name (str) + version_ids (list): of version[_id] + active_site (string): 'local', 'studio' etc + remote_site (string): dtto + Returns: + + """ + self.connection.Session["AVALON_PROJECT"] = project_name + query = [ + {"$match": {"parent": {"$in": version_ids}, + "type": "representation", + "files.sites.name": {"$exists": 1}}}, + {"$unwind": "$files"}, + {'$addFields': { + 'order_local': { + '$filter': { + 'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', active_site]} + } + } + }}, + {'$addFields': { + 'order_remote': { + '$filter': { + 'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', remote_site]} + } + } + }}, + {'$addFields': { + 'progress_local': {"$arrayElemAt": [{ + '$cond': [ + {'$size': "$order_local.progress"}, + "$order_local.progress", + # if exists created_dt count is as available + {'$cond': [ + {'$size': "$order_local.created_dt"}, + [1], + [0] + ]} + ]}, + 0 + ]} + }}, + {'$addFields': { + 'progress_remote': {"$arrayElemAt": [{ + '$cond': [ + {'$size': "$order_remote.progress"}, + "$order_remote.progress", + # if exists created_dt count is as available + {'$cond': [ + {'$size': "$order_remote.created_dt"}, + [1], + [0] + ]} + ]}, + 0 + ]} + }}, + {'$group': { # first group by repre + '_id': '$_id', + 'parent': {'$first': '$parent'}, + 'avail_ratio_local': { + '$first': { + '$divide': [{'$sum': "$progress_local"}, {'$sum': 1}] + } + }, + 'avail_ratio_remote': { + '$first': { + '$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}] + } + } + }}, + {'$group': { # second group by parent, eg version_id + '_id': '$parent', + 'repre_count': {'$sum': 1}, # total representations + # fully available representation for site + 'avail_repre_local': {'$sum': "$avail_ratio_local"}, + 'avail_repre_remote': {'$sum': "$avail_ratio_remote"}, + }}, + ] + # docs = list(self.connection.aggregate(query)) + return self.connection.aggregate(query) + """ End of Public API """ def get_local_file_path(self, project_name, site_name, file_path): diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index a5174bd804..3ce44ea6c8 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -272,15 +272,15 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): # update availability on active site when version changes if self.sync_server.enabled and version_doc: - query = self._repre_per_version_pipeline( + repre_info = self.sync_server.get_repre_info_for_versions( + project_name, [version_doc["_id"]], self.active_site, self.remote_site ) - docs = list(self.dbcon.aggregate(query)) - if docs: - repre = docs.pop() - version_doc["data"].update(self._get_repre_dict(repre)) + if repre_info: + version_doc["data"].update( + self._get_repre_dict(repre_info[0])) self.set_version(index, version_doc) @@ -478,16 +478,16 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): for _subset_id, doc in last_versions_by_subset_id.items(): version_ids.add(doc["_id"]) - query = self._repre_per_version_pipeline( + repres = self.sync_server.get_repre_info_for_versions( + project_name, list(version_ids), self.active_site, self.remote_site ) - - for doc in self.dbcon.aggregate(query): + for repre in repres: if self._doc_fetching_stop: return doc["active_provider"] = self.active_provider doc["remote_provider"] = self.remote_provider - repre_info[doc["_id"]] = doc + repre_info[repre["_id"]] = repre self._doc_payload = { "asset_docs_by_id": asset_docs_by_id, @@ -827,83 +827,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): return data - def _repre_per_version_pipeline(self, version_ids, - active_site, remote_site): - query = [ - {"$match": {"parent": {"$in": version_ids}, - "type": "representation", - "files.sites.name": {"$exists": 1}}}, - {"$unwind": "$files"}, - {'$addFields': { - 'order_local': { - '$filter': { - 'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', active_site]} - } - } - }}, - {'$addFields': { - 'order_remote': { - '$filter': { - 'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', remote_site]} - } - } - }}, - {'$addFields': { - 'progress_local': {"$arrayElemAt": [{ - '$cond': [ - {'$size': "$order_local.progress"}, - "$order_local.progress", - # if exists created_dt count is as available - {'$cond': [ - {'$size': "$order_local.created_dt"}, - [1], - [0] - ]} - ]}, - 0 - ]} - }}, - {'$addFields': { - 'progress_remote': {"$arrayElemAt": [{ - '$cond': [ - {'$size': "$order_remote.progress"}, - "$order_remote.progress", - # if exists created_dt count is as available - {'$cond': [ - {'$size': "$order_remote.created_dt"}, - [1], - [0] - ]} - ]}, - 0 - ]} - }}, - {'$group': { # first group by repre - '_id': '$_id', - 'parent': {'$first': '$parent'}, - 'avail_ratio_local': { - '$first': { - '$divide': [{'$sum': "$progress_local"}, {'$sum': 1}] - } - }, - 'avail_ratio_remote': { - '$first': { - '$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}] - } - } - }}, - {'$group': { # second group by parent, eg version_id - '_id': '$parent', - 'repre_count': {'$sum': 1}, # total representations - # fully available representation for site - 'avail_repre_local': {'$sum': "$avail_ratio_local"}, - 'avail_repre_remote': {'$sum': "$avail_ratio_remote"}, - }}, - ] - return query - class GroupMemberFilterProxyModel(QtCore.QSortFilterProxyModel): """Provide the feature of filtering group by the acceptance of members From 26c4a0f8ca19eeb4faaa85ceac1524c3bed71b7d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Aug 2022 16:15:17 +0200 Subject: [PATCH 302/432] OP-3405 - Hound --- openpype/modules/sync_server/providers/local_drive.py | 3 ++- openpype/modules/sync_server/sync_server.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/providers/local_drive.py b/openpype/modules/sync_server/providers/local_drive.py index 4951ef4d1a..01bc891d08 100644 --- a/openpype/modules/sync_server/providers/local_drive.py +++ b/openpype/modules/sync_server/providers/local_drive.py @@ -111,7 +111,8 @@ class LocalDriveHandler(AbstractProvider): Download a file form 'source_path' to 'local_path' """ return self.upload_file(source_path, local_path, - server, project_name, file, representation, site, + server, project_name, file, + representation, site, overwrite, direction="Download") def delete_file(self, path): diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 9cc55ec562..97538fcd4e 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -54,8 +54,9 @@ async def upload(module, project_name, file, representation, provider_name, file_path = file.get("path", "") try: - local_file_path, remote_file_path = resolve_paths(module, - file_path, project_name, remote_site_name, remote_handler + local_file_path, remote_file_path = resolve_paths( + module, file_path, project_name, + remote_site_name, remote_handler ) except Exception as exp: print(exp) @@ -270,8 +271,8 @@ class SyncServerThread(threading.Thread): - gets list of collections in DB - gets list of active remote providers (has configuration, credentials) - - for each project_name it looks for representations that should - be synced + - for each project_name it looks for representations that + should be synced - synchronize found collections - update representations - fills error messages for exceptions - waits X seconds and repeat From 80b6ef981a5bc43bf2f2eea5ce06895057472a9a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Aug 2022 18:13:39 +0200 Subject: [PATCH 303/432] OP-3684 - fix for new publisher New publisher expects frames in file names in '.0000.' format, AE by default provides ('_0000.'). Locally rendered files need to be renamed to appropriate format. --- .../plugins/publish/extract_local_render.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 7323a0b125..67a89ba9df 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -1,7 +1,8 @@ import os import sys import six - +import re +import shutil import openpype.api from openpype.hosts.aftereffects.api import get_stub @@ -22,15 +23,26 @@ class ExtractLocalRender(openpype.api.Extractor): # pull file name from Render Queue Output module render_q = stub.get_render_info() stub.render(staging_dir) + render_q_file_name = render_q.file_name if not render_q: raise ValueError("No file extension set in Render Queue") - _, ext = os.path.splitext(os.path.basename(render_q.file_name)) + _, ext = os.path.splitext(os.path.basename(render_q_file_name)) ext = ext[1:] + replace_frames_format = self._get_replace_format(render_q_file_name) + first_file_path = None files = [] - self.log.info("files::{}".format(os.listdir(staging_dir))) for file_name in os.listdir(staging_dir): + _, found_ext = os.path.splitext(file_name) + if found_ext[1:] != ext: + continue + + if replace_frames_format: + file_name = self._translate_frames(file_name, + replace_frames_format, + staging_dir) + files.append(file_name) if first_file_path is None: first_file_path = os.path.join(staging_dir, @@ -78,3 +90,23 @@ class ExtractLocalRender(openpype.api.Extractor): "stagingDir": staging_dir, "tags": ["thumbnail"] }) + + def _translate_frames(self, file_name, replace_frames_format, staging_dir): + orig_file_name = file_name + + found_frames = re.search(replace_frames_format, file_name) + if found_frames: + new_frames = found_frames.group(0).replace('_', '.') + file_name = file_name.replace(found_frames.group(0), new_frames) + shutil.move(os.path.join(staging_dir, orig_file_name), + os.path.join(staging_dir, file_name)) + + return file_name + + def _get_replace_format(self, file_name): + # replace delimiter for frames to one integrate is expecting (.0000.) + # returns frame format to be replaced + hashes_found = re.search(r"(_%5B[#]*%5D.)", file_name) + if hashes_found: + hashes = re.sub("[^#]", '', hashes_found.group(0)) + return "_[0-9]{{{0}}}.".format(len(hashes)) From a605cba4b99d056e1f797c426482292b34c31415 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 3 Aug 2022 04:07:35 +0000 Subject: [PATCH 304/432] [Automated] Bump version --- CHANGELOG.md | 24 ++++++++++-------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab4e5e45e..2c9671c8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.12.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...HEAD) @@ -11,21 +11,27 @@ **🚀 Enhancements** - Kitsu: Shot&Sequence name with prefix over appends [\#3593](https://github.com/pypeclub/OpenPype/pull/3593) -- Ftrack: Update ftrack api to 2.3.3 [\#3588](https://github.com/pypeclub/OpenPype/pull/3588) +- Photoshop: implemented {layer} placeholder in subset template [\#3591](https://github.com/pypeclub/OpenPype/pull/3591) - General: New Integrator small fixes [\#3583](https://github.com/pypeclub/OpenPype/pull/3583) **🐛 Bug fixes** +- TrayPublisher: Fix wrong conflict merge [\#3600](https://github.com/pypeclub/OpenPype/pull/3600) +- Bugfix: Add OCIO as submodule to prepare for handling `maketx` color space conversion. [\#3590](https://github.com/pypeclub/OpenPype/pull/3590) - Editorial publishing workflow improvements [\#3580](https://github.com/pypeclub/OpenPype/pull/3580) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) - Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) -- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** +- General: Use query functions in general code [\#3596](https://github.com/pypeclub/OpenPype/pull/3596) - General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Lib cleanup [\#3571](https://github.com/pypeclub/OpenPype/pull/3571) +**Merged pull requests:** + +- Enable write color sets on animation publish automatically [\#3582](https://github.com/pypeclub/OpenPype/pull/3582) + ## [3.12.2](https://github.com/pypeclub/OpenPype/tree/3.12.2) (2022-07-27) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.2-nightly.4...3.12.2) @@ -54,6 +60,7 @@ - NewPublisher: Python 2 compatible html escape [\#3559](https://github.com/pypeclub/OpenPype/pull/3559) - Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) - General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) +- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) - Settings: Clean default values of environments [\#3550](https://github.com/pypeclub/OpenPype/pull/3550) - Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) - Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) @@ -66,7 +73,6 @@ - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) - TrayPublisher: Make sure host name is filled [\#3504](https://github.com/pypeclub/OpenPype/pull/3504) - NewPublisher: Groups work and enum multivalue [\#3501](https://github.com/pypeclub/OpenPype/pull/3501) -- Resolve: removed few bugs [\#3464](https://github.com/pypeclub/OpenPype/pull/3464) **🔀 Refactored code** @@ -77,7 +83,6 @@ - General: Move load related functions into pipeline [\#3527](https://github.com/pypeclub/OpenPype/pull/3527) - General: Get current context document functions [\#3522](https://github.com/pypeclub/OpenPype/pull/3522) - Kitsu: Use query function from client [\#3496](https://github.com/pypeclub/OpenPype/pull/3496) -- Deadline: Use query functions [\#3466](https://github.com/pypeclub/OpenPype/pull/3466) **Merged pull requests:** @@ -94,7 +99,6 @@ - NewPublisher: Added ability to use label of instance [\#3484](https://github.com/pypeclub/OpenPype/pull/3484) - General: Creator Plugins have access to project [\#3476](https://github.com/pypeclub/OpenPype/pull/3476) - General: Better arguments order in creator init [\#3475](https://github.com/pypeclub/OpenPype/pull/3475) -- Ftrack: Trigger custom ftrack events on project creation and preparation [\#3465](https://github.com/pypeclub/OpenPype/pull/3465) **🐛 Bug fixes** @@ -105,14 +109,6 @@ - New Publisher: Disabled context change allows creation [\#3478](https://github.com/pypeclub/OpenPype/pull/3478) - General: thumbnail extractor fix [\#3474](https://github.com/pypeclub/OpenPype/pull/3474) - Kitsu: bugfix with sync-service ans publish plugins [\#3473](https://github.com/pypeclub/OpenPype/pull/3473) -- Flame: solved problem with multi-selected loading [\#3470](https://github.com/pypeclub/OpenPype/pull/3470) -- General: Fix query function in update logic [\#3468](https://github.com/pypeclub/OpenPype/pull/3468) -- General: Delete old versions is safer when ftrack is disabled [\#3462](https://github.com/pypeclub/OpenPype/pull/3462) - -**🔀 Refactored code** - -- Maya: Merge animation + pointcache extractor logic [\#3461](https://github.com/pypeclub/OpenPype/pull/3461) -- Maya: Re-use `maintained\_time` from lib [\#3460](https://github.com/pypeclub/OpenPype/pull/3460) ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) diff --git a/openpype/version.py b/openpype/version.py index 03fd5fb96e..636dff5930 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.3-nightly.1" +__version__ = "3.12.3-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index 118355395a..9ab2fd4513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.3-nightly.1" # OpenPype +version = "3.12.3-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 0761ba4bc3b029cc5a130f3cde5dcedebefa0d7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 13:34:24 +0200 Subject: [PATCH 305/432] OP-3684 - fix output compare for automatic testing --- tests/lib/testing_classes.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index f991f02227..aa366cd005 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -314,30 +314,21 @@ class PublishTest(ModuleUnitTest): Compares only presence, not size nor content! """ - published_dir_base = download_test_data - published_dir = os.path.join(output_folder_url, - self.PROJECT, - self.ASSET, - self.TASK, - "**") - expected_dir_base = os.path.join(published_dir_base, + published_dir_base = output_folder_url + expected_dir_base = os.path.join(download_test_data, "expected") - expected_dir = os.path.join(expected_dir_base, - self.PROJECT, - self.ASSET, - self.TASK, - "**") - print("Comparing published:'{}' : expected:'{}'".format(published_dir, - expected_dir)) + + print("Comparing published:'{}' : expected:'{}'".format(published_dir_base, + expected_dir_base)) published = set(f.replace(published_dir_base, '') for f in - glob.glob(published_dir, recursive=True) if + glob.glob(published_dir_base + "\\**", recursive=True) if f != published_dir_base and os.path.exists(f)) expected = set(f.replace(expected_dir_base, '') for f in - glob.glob(expected_dir, recursive=True) if + glob.glob(expected_dir_base + "\\**", recursive=True) if f != expected_dir_base and os.path.exists(f)) - not_matched = expected.difference(published) - assert not not_matched, "Missing {} files".format(not_matched) + not_matched = expected.symmetric_difference(published) + assert not not_matched, "Missing {} files".format("\n".join(sorted(not_matched))) class HostFixtures(PublishTest): From 67b9946f2057fb44133edfd2fa70fddfe9ce2de3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 13:36:20 +0200 Subject: [PATCH 306/432] OP-3684 - added new testing class for multiframe AE publish Previous test published only single frame, didn't catch issue in new integrate. --- ...test_publish_in_aftereffects_multiframe.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/integration/hosts/aftereffects/test_publish_in_aftereffects_multiframe.py diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects_multiframe.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects_multiframe.py new file mode 100644 index 0000000000..c882e0f9b2 --- /dev/null +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects_multiframe.py @@ -0,0 +1,64 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.aftereffects.lib import AfterEffectsTestClass + +log = logging.getLogger("test_publish_in_aftereffects") + + +class TestPublishInAfterEffects(AfterEffectsTestClass): + """Basic test case for publishing in AfterEffects + + Should publish 5 frames + """ + PERSIST = True + + TEST_FILES = [ + ("12aSDRjthn4X3yw83gz_0FZJcRRiVDEYT", + "test_aftereffects_publish_multiframe.zip", + "") + ] + + APP = "aftereffects" + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 2)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="imageMainBackgroundcopy")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="reviewTesttask")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 4)) + + additional_args = {"context.subset": "renderTestTaskDefault", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInAfterEffects() From 9ed329aebe6e114d47195e6dc456898569e0d404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Aug 2022 14:26:05 +0200 Subject: [PATCH 307/432] :bug: filter out non-build versions and fixing the error message --- .../custom/plugins/GlobalJobPreLoad.py | 18 ++++++++++++++++-- .../custom/plugins/OpenPype/OpenPype.py | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index a43c6c7733..5e923eb09a 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -10,10 +10,23 @@ import re from Deadline.Scripting import RepositoryUtils, FileUtils, DirectoryUtils -def get_openpype_version_from_path(path): +def get_openpype_version_from_path(path, build=True): + """Get OpenPype version from provided path. + path (str): Path to scan. + build (bool, optional): Get only builds, not sources + + Returns: + str or None: version of OpenPype if found. + + """ version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): return None + # skip if the version is not build + if not build and \ + (not os.path.isfile(os.path.join(path, "openpype_console")) or + not os.path.isfile(os.path.join(path, "openpype_console.exe"))): + return None version = {} with open(version_file, "r") as vf: exec(vf.read(), version) @@ -101,7 +114,8 @@ def inject_openpype_environment(deadlinePlugin): if exe == "": raise RuntimeError( "OpenPype executable was not found " + - "in the semicolon separated list \"" + exe_list + "\". " + + "in the semicolon separated list " + + "\"" + ";".join(exe_list) + "\". " + "The path to the render executable can be configured " + "from the Plugin Configuration in the Deadline Monitor.") diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index b84560f175..764dc4c4ba 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -61,10 +61,23 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): ".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress @staticmethod - def get_openpype_version_from_path(path): + def get_openpype_version_from_path(path, build=True): + """Get OpenPype version from provided path. + path (str): Path to scan. + build (bool, optional): Get only builds, not sources + + Returns: + str or None: version of OpenPype if found. + + """ version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): return None + # skip if the version is not build + if not build and \ + (not os.path.isfile(os.path.join(path, "openpype_console")) or + not os.path.isfile(os.path.join(path, "openpype_console.exe"))): + return None version = {} with open(version_file, "r") as vf: exec(vf.read(), version) @@ -136,7 +149,8 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): if exe == "": self.FailRender( "OpenPype executable was not found " + - "in the semicolon separated list \"" + exe_list + "\". " + + "in the semicolon separated list " + + "\"" + ";".join(exe_list) + "\". " + "The path to the render executable can be configured " + "from the Plugin Configuration in the Deadline Monitor.") return exe From ef60744d9c1aedea3f442792733d844ed0d845b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 14:58:36 +0200 Subject: [PATCH 308/432] OP-3684 - added default to Integrate Setting to skip render.farm New publisher requires main family as 'render', so there will be need to skip 'render.farm' which should not be integrated during initial publish. (Currently only affecting AE.) --- openpype/plugins/publish/integrate.py | 11 ++++++----- .../settings/defaults/project_settings/global.json | 11 ++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index d817595888..70ab9f611e 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -167,7 +167,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): skip_host_families = [] def process(self, instance): - if self._temp_skip_instance_by_settings(instance): + if self.skip_instance_by_settings(instance): return # Mark instance as processed for legacy integrator @@ -203,11 +203,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def _temp_skip_instance_by_settings(self, instance): - """Decide if instance will be processed with new or legacy integrator. + def skip_instance_by_settings(self, instance): + """Decide if instance will be processed with new integrator. - This is temporary solution until we test all usecases with new (this) - integrator plugin. + This might be temporary solution for broken publishing for any families + (therefore it should fallback into legacy publish plugin) OR this + could replace 'exclude_families' in legacy plugin (host is required). """ host_name = instance.context.data["hostName"] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index e509db2791..d349066924 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -225,7 +225,16 @@ ] }, "IntegrateAsset": { - "skip_host_families": [] + "skip_host_families": [ + { + "host": [ + "aftereffects" + ], + "families": [ + "render.farm" + ] + } + ] }, "IntegrateHeroVersion": { "enabled": true, From d0ac6bc9b0b55cbe4897d9ba129412316202d6eb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 15:08:13 +0200 Subject: [PATCH 309/432] OP-3684 - Hound --- tests/lib/testing_classes.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index aa366cd005..2b4d7deb48 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -318,17 +318,18 @@ class PublishTest(ModuleUnitTest): expected_dir_base = os.path.join(download_test_data, "expected") - print("Comparing published:'{}' : expected:'{}'".format(published_dir_base, - expected_dir_base)) + print("Comparing published:'{}' : expected:'{}'".format( + published_dir_base, expected_dir_base)) published = set(f.replace(published_dir_base, '') for f in - glob.glob(published_dir_base + "\\**", recursive=True) if - f != published_dir_base and os.path.exists(f)) + glob.glob(published_dir_base + "\\**", recursive=True) + if f != published_dir_base and os.path.exists(f)) expected = set(f.replace(expected_dir_base, '') for f in - glob.glob(expected_dir_base + "\\**", recursive=True) if - f != expected_dir_base and os.path.exists(f)) + glob.glob(expected_dir_base + "\\**", recursive=True) + if f != expected_dir_base and os.path.exists(f)) not_matched = expected.symmetric_difference(published) - assert not not_matched, "Missing {} files".format("\n".join(sorted(not_matched))) + assert not not_matched, "Missing {} files".format( + "\n".join(sorted(not_matched))) class HostFixtures(PublishTest): From fec91f054d6766ca4d597faa225ab1d783515026 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 15:33:38 +0200 Subject: [PATCH 310/432] don't force to have dot before frame in new integrator --- openpype/plugins/publish/integrate.py | 70 ++++++++------------------- 1 file changed, 19 insertions(+), 51 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index d817595888..f7f5ca2aeb 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -23,41 +23,6 @@ from openpype.pipeline.publish import KnownPublishError log = logging.getLogger(__name__) -def assemble(files): - """Convenience `clique.assemble` wrapper for files of a single collection. - - Unlike `clique.assemble` this wrapper does not allow more than a single - Collection nor any remainder files. Errors will be raised when not only - a single collection is assembled. - - Returns: - clique.Collection: A single sequence Collection - - Raises: - ValueError: Error is raised when files do not result in a single - collected Collection. - - """ - # todo: move this to lib? - # Get the sequence as a collection. The files must be of a single - # sequence and have no remainder outside of the collections. - patterns = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble(files, - minimum_items=1, - patterns=patterns) - if not collections: - raise ValueError("No collections found in files: " - "{}".format(files)) - if remainder: - raise ValueError("Files found not detected as part" - " of a sequence: {}".format(remainder)) - if len(collections) > 1: - raise ValueError("Files in sequence are not part of a" - " single sequence collection: " - "{}".format(collections)) - return collections[0] - - def get_instance_families(instance): """Get all families of the instance""" # todo: move this to lib? @@ -576,7 +541,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if any(os.path.isabs(fname) for fname in files): raise KnownPublishError("Given file names contain full paths") - src_collection = assemble(files) + src_collection = clique.assemble(files) destination_indexes = list(src_collection.indexes) # Use last frame for minimum padding @@ -609,31 +574,34 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # a Frame or UDIM tile set for the template data. We use the first # index of the destination for that because that could've shifted # from the source indexes, etc. - first_index_padded = get_frame_padded(frame=destination_indexes[0], - padding=destination_padding) - if is_udim: - # UDIM representations handle ranges in a different manner - template_data["udim"] = first_index_padded - else: - template_data["frame"] = first_index_padded + first_index_padded = get_frame_padded( + frame=destination_indexes[0], + padding=destination_padding + ) # Construct destination collection from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values + repre_context = None + dst_filepaths = [] + for index in destination_indexes: + if is_udim: + template_data["udim"] = index + else: + template_data["frame"] = index + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + dst_filepaths.append(template_filled) + if repre_context is None: + repre_context = template_filled.used_value + self.log.debug("Template filled: {}".format(str(template_filled))) # Make sure context contains frame # NOTE: Frame would not be available only if template does not # contain '{frame}' in template -> Do we want support it? if not is_udim: repre_context["frame"] = first_index_padded - self.log.debug("Template filled: {}".format(str(template_filled))) - dst_collection = assemble([os.path.normpath(template_filled)]) - # Update the destination indexes and padding - dst_collection.indexes.clear() - dst_collection.indexes.update(set(destination_indexes)) + dst_collection = clique.assemble(dst_filepaths) dst_collection.padding = destination_padding if len(src_collection.indexes) != len(dst_collection.indexes): raise KnownPublishError(( From 573d0a5ae12daddaef84f1b1d5c46f0048a780c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 16:00:50 +0200 Subject: [PATCH 311/432] add fps to newly created representation --- openpype/plugins/publish/extract_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1b6e2a1d61..533a87acb4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -360,6 +360,7 @@ class ExtractReview(pyblish.api.InstancePlugin): os.unlink(f) new_repre.update({ + "fps": temp_data["fps"], "name": "{}_{}".format(output_name, output_ext), "outputName": output_name, "outputDef": output_def, From bd4ebab60d5ba8bbca96eaea15080d79cc29d5e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 16:03:48 +0200 Subject: [PATCH 312/432] make sure ftrackreview-image is renamed to thumbnail if there is ftrackreview-mp4 to be able play it --- .../plugins/publish/integrate_ftrack_api.py | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 58591bacfd..20a69e060c 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -87,6 +87,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): asset_versions_data_by_id = {} used_asset_versions = [] + # Iterate over components and publish for data in component_list: self.log.debug("data: {}".format(data)) @@ -116,9 +117,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): asset_version_status_ids_by_name ) - # Component - self.create_component(session, asset_version_entity, data) - # Store asset version and components items that were version_id = asset_version_entity["id"] if version_id not in asset_versions_data_by_id: @@ -135,6 +133,8 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): if asset_version_entity not in used_asset_versions: used_asset_versions.append(asset_version_entity) + self._create_components(session, asset_versions_data_by_id) + instance.data["ftrackIntegratedAssetVersionsData"] = ( asset_versions_data_by_id ) @@ -623,3 +623,40 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session.rollback() session._configure_locations() six.reraise(tp, value, tb) + + def _create_components(self, session, asset_versions_data_by_id): + for item in asset_versions_data_by_id.values(): + asset_version_entity = item["asset_version"] + component_items = item["component_items"] + + component_entities = session.query( + ( + "select id, name from Component where version_id is \"{}\"" + ).format(asset_version_entity["id"]) + ).all() + + existing_component_names = { + component["name"] + for component in component_entities + } + + contain_review = "ftrackreview-mp4" in existing_component_names + thumbnail_component_item = None + for component_item in component_items: + component_data = component_item.get("component_data") or {} + component_name = component_data.get("name") + if component_name == "ftrackreview-mp4": + contain_review = True + elif component_name == "ftrackreview-image": + thumbnail_component_item = component_item + + if contain_review and thumbnail_component_item: + thumbnail_component_item["component_data"]["name"] = ( + "thumbnail" + ) + + # Component + for component_item in component_items: + self.create_component( + session, asset_version_entity, component_item + ) From c64925fb665ee3bcb49837dcb2fff7f03a7390f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Aug 2022 16:12:21 +0200 Subject: [PATCH 313/432] :rotating_light: I hate you Hound so much --- .../deadline/repository/custom/plugins/OpenPype/OpenPype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 764dc4c4ba..79101bb90c 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -76,7 +76,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): # skip if the version is not build if not build and \ (not os.path.isfile(os.path.join(path, "openpype_console")) or - not os.path.isfile(os.path.join(path, "openpype_console.exe"))): + not os.path.isfile(os.path.join(path, "openpype_console.exe"))): # noqa: E501 return None version = {} with open(version_file, "r") as vf: From 502a8c6ee7f55c7c98dfa1fd2033cf286116f9bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 16:12:52 +0200 Subject: [PATCH 314/432] add more metadata to ftrack components --- .../publish/integrate_ftrack_instances.py | 151 ++++++++++++++---- 1 file changed, 121 insertions(+), 30 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index d937e64790..4c0e5127fa 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,7 +3,10 @@ import json import copy import pyblish.api -from openpype.lib import get_ffprobe_streams +from openpype.lib.transcoding import ( + get_ffprobe_streams, + convert_ffprobe_fps_to_float, +) from openpype.lib.profiles_filtering import filter_profiles @@ -79,11 +82,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ).format(family)) return - # Prepare FPS - instance_fps = instance.data.get("fps") - if instance_fps is None: - instance_fps = instance.context.data["fps"] - status_name = self._get_asset_version_status_name(instance) # Base of component item data @@ -168,10 +166,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add item to component list component_list.append(thumbnail_item) - if ( - not review_representations - and first_thumbnail_component is not None - ): + if first_thumbnail_component is not None: width = first_thumbnail_component_repre.get("width") height = first_thumbnail_component_repre.get("height") if not width or not height: @@ -253,20 +248,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component[ "asset_data"]["name"] = extended_asset_name - frame_start = repre.get("frameStartFtrack") - frame_end = repre.get("frameEndFtrack") - if frame_start is None or frame_end is None: - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - # Frame end of uploaded video file should be duration in frames - # - frame start is always 0 - # - frame end is duration in frames - duration = frame_end - frame_start + 1 - - fps = repre.get("fps") - if fps is None: - fps = instance_fps + component_meta = self._prepare_component_metadata( + instance, repre, repre_path, True + ) # Change location review_item["component_path"] = repre_path @@ -275,11 +259,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Default component name is "main". "name": "ftrackreview-mp4", "metadata": { - "ftr_meta": json.dumps({ - "frameIn": 0, - "frameOut": int(duration), - "frameRate": float(fps) - }) + "ftr_meta": json.dumps(component_meta) } } @@ -339,9 +319,17 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ): other_item["asset_data"]["name"] = extended_asset_name - other_item["component_data"] = { + component_meta = self._prepare_component_metadata( + instance, repre, published_path, False + ) + component_data = { "name": repre["name"] } + if component_meta: + component_data["metadata"] = { + "ftr_meta": json.dumps(component_meta) + } + other_item["component_data"] = component_data other_item["component_location_name"] = unmanaged_location_name other_item["component_path"] = published_path component_list.append(other_item) @@ -424,3 +412,106 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): return None return matching_profile["status"] or None + + def _prepare_component_metadata( + self, instance, repre, component_path, is_review + ): + extension = os.path.splitext(component_path)[-1] + streams = [] + try: + streams = get_ffprobe_streams(component_path) + except Exception: + self.log.debug(( + "Failed to retrieve information about intput {}" + ).format(component_path)) + + # Find video streams + video_streams = [ + stream + for stream in streams + if stream["codec_type"] == "video" + ] + # Skip if there are not video streams + # - exr is special case which can have issues with reading through + # ffmpegh but we want to set fps for it + if not video_streams and extension not in [".exr"]: + return {} + + stream_width = None + stream_height = None + stream_fps = None + frame_out = None + for video_stream in video_streams: + input_framerate = video_stream.get("r_frame_rate") + duration = video_stream.get("duration") + tmp_width = video_stream.get("width") + tmp_height = video_stream.get("height") + if input_framerate is None or duration is None: + if tmp_width and tmp_height: + stream_width = int(tmp_width) + stream_height = int(tmp_height) + continue + try: + stream_fps = convert_ffprobe_fps_to_float( + input_framerate + ) + except ValueError: + self.log.warning(( + "Could not convert ffprobe fps to float \"{}\"" + ).format(input_framerate)) + continue + + stream_width = tmp_width + stream_height = tmp_height + + self.log.debug("FPS from stream is {} and duration is {}".format( + input_framerate, duration + )) + frame_out = float(duration) * stream_fps + break + + # Prepare FPS + instance_fps = instance.data.get("fps") + if instance_fps is None: + instance_fps = instance.context.data["fps"] + + if not is_review: + output = {} + fps = stream_fps or instance_fps + if fps: + output["frameRate"] = fps + + if stream_width and stream_height: + output["width"] = int(stream_width) + output["height"] = int(stream_height) + return output + + frame_start = repre.get("frameStartFtrack") + frame_end = repre.get("frameEndFtrack") + if frame_start is None or frame_end is None: + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + + fps = None + repre_fps = repre.get("fps") + if repre_fps is not None: + repre_fps = float(repre_fps) + + fps = stream_fps or repre_fps or instance_fps + + # Frame end of uploaded video file should be duration in frames + # - frame start is always 0 + # - frame end is duration in frames + if not frame_out: + frame_out = frame_end - frame_start + 1 + + # Ftrack documentation says that it is required to have + # 'width' and 'height' in review component. But with those values + # review video does not play. + component_meta = { + "frameIn": 0, + "frameOut": frame_out, + "frameRate": float(fps) + } + + return component_meta From 99469a14438665595fafc21bdc517c083d76bd2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 16:24:50 +0200 Subject: [PATCH 315/432] OP-3684 - revert - added default to Integrate Setting to skip render.farm" This reverts commit ef60744d Not necessary, better to use `instance.data["farm"]` --- openpype/plugins/publish/integrate.py | 11 +++++------ .../settings/defaults/project_settings/global.json | 11 +---------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 70ab9f611e..d817595888 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -167,7 +167,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): skip_host_families = [] def process(self, instance): - if self.skip_instance_by_settings(instance): + if self._temp_skip_instance_by_settings(instance): return # Mark instance as processed for legacy integrator @@ -203,12 +203,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def skip_instance_by_settings(self, instance): - """Decide if instance will be processed with new integrator. + def _temp_skip_instance_by_settings(self, instance): + """Decide if instance will be processed with new or legacy integrator. - This might be temporary solution for broken publishing for any families - (therefore it should fallback into legacy publish plugin) OR this - could replace 'exclude_families' in legacy plugin (host is required). + This is temporary solution until we test all usecases with new (this) + integrator plugin. """ host_name = instance.context.data["hostName"] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index d349066924..e509db2791 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -225,16 +225,7 @@ ] }, "IntegrateAsset": { - "skip_host_families": [ - { - "host": [ - "aftereffects" - ], - "families": [ - "render.farm" - ] - } - ] + "skip_host_families": [] }, "IntegrateHeroVersion": { "enabled": true, From bab5629e35736d94c01c012b0e5aee79fe95ba71 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 16:26:46 +0200 Subject: [PATCH 316/432] OP-3684 - use instance.data["farm"] to skip local integrate No Settings necessary, instance itself should hold if it is targetted for farm (eg. not locally integrated.) --- .../hosts/aftereffects/plugins/publish/collect_render.py | 5 +++-- openpype/pipeline/publish/abstract_collect_render.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index bb199a61f7..d444ead6dc 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -102,7 +102,6 @@ class CollectAERender(publish.AbstractCollectRender): attachTo=False, setMembers='', publish=True, - renderer='aerender', name=subset_name, resolutionWidth=render_q.width, resolutionHeight=render_q.height, @@ -113,7 +112,6 @@ class CollectAERender(publish.AbstractCollectRender): frameStart=frame_start, frameEnd=frame_end, frameStep=1, - toBeRenderedOn='deadline', fps=fps, app_version=app_version, publish_attributes=inst.data.get("publish_attributes", {}), @@ -138,6 +136,9 @@ class CollectAERender(publish.AbstractCollectRender): fam = "render.farm" if fam not in instance.families: instance.families.append(fam) + instance.toBeRenderedOn = "deadline" + instance.renderer = "aerender" + instance.farm = True # to skip integrate instances.append(instance) instances_to_remove.append(inst) diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 2e537227c3..ccb2415346 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -63,6 +63,8 @@ class RenderInstance(object): family = attr.ib(default="renderlayer") families = attr.ib(default=["renderlayer"]) # list of families + # True if should be rendered on farm, eg not integrate + farm = attr.ib(default=False) # format settings multipartExr = attr.ib(default=False) # flag for multipart exrs From e4c1c204d19ab5c97751c33428927d85771eacc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 16:33:17 +0200 Subject: [PATCH 317/432] add metada also for src components --- .../publish/integrate_ftrack_instances.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 4c0e5127fa..a1e5922730 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -302,6 +302,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): component_data = copy_src_item["component_data"] component_name = component_data["name"] component_data["name"] = component_name + "_src" + component_meta = self._prepare_component_metadata( + instance, repre, copy_src_item["component_path"], False + ) + if component_meta: + component_data["metadata"] = { + "ftr_meta": json.dumps(component_meta) + } component_list.append(copy_src_item) # Add others representations as component @@ -442,14 +449,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_fps = None frame_out = None for video_stream in video_streams: - input_framerate = video_stream.get("r_frame_rate") - duration = video_stream.get("duration") tmp_width = video_stream.get("width") tmp_height = video_stream.get("height") + if tmp_width and tmp_height: + stream_width = tmp_width + stream_height = tmp_height + + input_framerate = video_stream.get("r_frame_rate") + duration = video_stream.get("duration") if input_framerate is None or duration is None: - if tmp_width and tmp_height: - stream_width = int(tmp_width) - stream_height = int(tmp_height) continue try: stream_fps = convert_ffprobe_fps_to_float( From 59463a345784eda01a5ce9f158dd3d1ffb9a821d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 16:54:53 +0200 Subject: [PATCH 318/432] add new function to determine fps value --- openpype/lib/__init__.py | 2 ++ openpype/lib/transcoding.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 31cd5e7510..3d3e425a86 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -115,6 +115,7 @@ from .transcoding import ( get_ffmpeg_codec_args, get_ffmpeg_format_args, convert_ffprobe_fps_value, + convert_ffprobe_fps_to_float, ) from .avalon_context import ( CURRENT_DOC_SCHEMAS, @@ -287,6 +288,7 @@ __all__ = [ "get_ffmpeg_codec_args", "get_ffmpeg_format_args", "convert_ffprobe_fps_value", + "convert_ffprobe_fps_to_float", "CURRENT_DOC_SCHEMAS", "PROJECT_NAME_ALLOWED_SYMBOLS", diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index ee9a0f08de..60d5d3ed4a 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -938,3 +938,40 @@ def convert_ffprobe_fps_value(str_value): fps = int(fps) return str(fps) + + +def convert_ffprobe_fps_to_float(value): + """Convert string value of frame rate to float. + + Copy of 'convert_ffprobe_fps_value' which raises exceptions on invalid + value, does not convert value to string and does not return "Unknown" + string. + + Args: + value (str): Value to be converted. + + Returns: + Float: Converted frame rate in float. If divisor in value is '0' then + '0.0' is returned. + + Raises: + ValueError: Passed value is invalid for conversion. + """ + + if not value: + raise ValueError("Got empty value.") + + items = value.split("/") + if len(items) == 1: + return float(items[0]) + + if len(items) > 2: + raise ValueError(( + "FPS expression contains multiple dividers \"{}\"." + ).format(value)) + + dividend = float(items.pop(0)) + divisor = float(items.pop(0)) + if divisor == 0.0: + return 0.0 + return dividend / divisor From cc048e4b6f053e9fdc9041fbc79ed6a82a0ecbd5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 16:57:56 +0200 Subject: [PATCH 319/432] OP-3704 - translated validate_containers.py into New publisher style AE could be already using NP. --- .../publish/help/validate_containers.xml | 24 +++++++++++++++++++ .../plugins/publish/validate_containers.py | 14 +++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 openpype/plugins/publish/help/validate_containers.xml diff --git a/openpype/plugins/publish/help/validate_containers.xml b/openpype/plugins/publish/help/validate_containers.xml new file mode 100644 index 0000000000..e540c3c7a9 --- /dev/null +++ b/openpype/plugins/publish/help/validate_containers.xml @@ -0,0 +1,24 @@ + + + +Not up-to-date assets + +## Obsolete containers found + +Scene contains one or more obsolete loaded containers, eg. items loaded into scene by Loader. + +### How to repair? + +Use 'Scene Inventory' and update all highlighted old container to latest OR + refresh Publish and switch 'Validate Containers' toggle on 'Options' tab. + + WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets. + + +### __Detailed Info__ (optional) + +This validator protects you from rendering obsolete content, someone modified some referenced asset in this scene, eg. + by skipping this you would ignore changes to that asset. + + + \ No newline at end of file diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index b2a3ed9b79..79759450e1 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -1,5 +1,9 @@ import pyblish.api from openpype.pipeline.load import any_outdated_containers +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin +) class ShowInventory(pyblish.api.Action): @@ -14,7 +18,9 @@ class ShowInventory(pyblish.api.Action): host_tools.show_scene_inventory() -class ValidateContainers(pyblish.api.ContextPlugin): +class ValidateContainers(OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin): + """Containers are must be updated to latest version on publish.""" label = "Validate Containers" @@ -24,5 +30,9 @@ class ValidateContainers(pyblish.api.ContextPlugin): actions = [ShowInventory] def process(self, context): + if not self.is_active(context.data): + return + if any_outdated_containers(): - raise ValueError("There are outdated containers in the scene.") + msg = "There are outdated containers in the scene." + raise PublishXmlValidationError(self, msg) From e43748bb3d0091f3d92f8f1ce64764dba54cf09d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 17:28:27 +0200 Subject: [PATCH 320/432] validate representation files sequence --- openpype/plugins/publish/integrate.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index f7f5ca2aeb..f65ef80db7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -541,8 +541,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if any(os.path.isabs(fname) for fname in files): raise KnownPublishError("Given file names contain full paths") - src_collection = clique.assemble(files) + src_collections, remainders = clique.assemble(files) + if len(files) < 2 or len(src_collections) != 1 or remainders: + raise KnownPublishError(( + "Files of representation does not contain proper" + " sequence files.\nCollected collections: {}" + "\nCollected remainders: {}" + ).format( + ", ".join([str(col) for col in src_collections]), + ", ".join([str(rem) for rem in remainders]) + )) + src_collection = src_collections[0] destination_indexes = list(src_collection.indexes) # Use last frame for minimum padding # - that should cover both 'udim' and 'frame' minimum padding From e0fc9d5d12974563b90c0714e8f3605672690afb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 17:31:31 +0200 Subject: [PATCH 321/432] fix typo --- openpype/plugins/publish/integrate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index f65ef80db7..070ebc290c 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -601,9 +601,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_filled = anatomy_filled[template_name]["path"] dst_filepaths.append(template_filled) if repre_context is None: - repre_context = template_filled.used_value + self.log.debug( + "Template filled: {}".format(str(template_filled)) + ) + repre_context = template_filled.used_values - self.log.debug("Template filled: {}".format(str(template_filled))) # Make sure context contains frame # NOTE: Frame would not be available only if template does not # contain '{frame}' in template -> Do we want support it? From 9d8a05d8a7b627b77cc434662ef39868847f31ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 17:33:26 +0200 Subject: [PATCH 322/432] fix dst collection access --- openpype/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 070ebc290c..688e252f1b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -613,7 +613,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): repre_context["frame"] = first_index_padded # Update the destination indexes and padding - dst_collection = clique.assemble(dst_filepaths) + dst_collection = clique.assemble(dst_filepaths)[0][0] dst_collection.padding = destination_padding if len(src_collection.indexes) != len(dst_collection.indexes): raise KnownPublishError(( From 7cfd9624a31b16f8bfff52684bacc7b9366cb925 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 17:39:35 +0200 Subject: [PATCH 323/432] "OP-3684 - revert - fix for new publisher" This reverts commit 80b6ef98 Made obsolete by https://github.com/pypeclub/OpenPype/pull/3611 --- .../plugins/publish/extract_local_render.py | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 67a89ba9df..7323a0b125 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -1,8 +1,7 @@ import os import sys import six -import re -import shutil + import openpype.api from openpype.hosts.aftereffects.api import get_stub @@ -23,26 +22,15 @@ class ExtractLocalRender(openpype.api.Extractor): # pull file name from Render Queue Output module render_q = stub.get_render_info() stub.render(staging_dir) - render_q_file_name = render_q.file_name if not render_q: raise ValueError("No file extension set in Render Queue") - _, ext = os.path.splitext(os.path.basename(render_q_file_name)) + _, ext = os.path.splitext(os.path.basename(render_q.file_name)) ext = ext[1:] - replace_frames_format = self._get_replace_format(render_q_file_name) - first_file_path = None files = [] + self.log.info("files::{}".format(os.listdir(staging_dir))) for file_name in os.listdir(staging_dir): - _, found_ext = os.path.splitext(file_name) - if found_ext[1:] != ext: - continue - - if replace_frames_format: - file_name = self._translate_frames(file_name, - replace_frames_format, - staging_dir) - files.append(file_name) if first_file_path is None: first_file_path = os.path.join(staging_dir, @@ -90,23 +78,3 @@ class ExtractLocalRender(openpype.api.Extractor): "stagingDir": staging_dir, "tags": ["thumbnail"] }) - - def _translate_frames(self, file_name, replace_frames_format, staging_dir): - orig_file_name = file_name - - found_frames = re.search(replace_frames_format, file_name) - if found_frames: - new_frames = found_frames.group(0).replace('_', '.') - file_name = file_name.replace(found_frames.group(0), new_frames) - shutil.move(os.path.join(staging_dir, orig_file_name), - os.path.join(staging_dir, file_name)) - - return file_name - - def _get_replace_format(self, file_name): - # replace delimiter for frames to one integrate is expecting (.0000.) - # returns frame format to be replaced - hashes_found = re.search(r"(_%5B[#]*%5D.)", file_name) - if hashes_found: - hashes = re.sub("[^#]", '', hashes_found.group(0)) - return "_[0-9]{{{0}}}.".format(len(hashes)) From c4fce5fea9ad37e0706c1b76500ed21585e66141 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 17:40:47 +0200 Subject: [PATCH 324/432] integrate description can use optional keys --- .../publish/integrate_ftrack_description.py | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py index c6a3d47f66..e7c265988e 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py @@ -6,9 +6,11 @@ Requires: """ import sys +import json import six import pyblish.api +from openpype.lib import StringTemplate class IntegrateFtrackDescription(pyblish.api.InstancePlugin): @@ -25,6 +27,10 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin): description_template = "{comment}" def process(self, instance): + if not self.description_template: + self.log.info("Skipping. Description template is not set.") + return + # Check if there are any integrated AssetVersion entities asset_versions_key = "ftrackIntegratedAssetVersionsData" asset_versions_data_by_id = instance.data.get(asset_versions_key) @@ -38,39 +44,62 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin): else: self.log.debug("Comment is set to `{}`".format(comment)) - session = instance.context.data["ftrackSession"] - intent = instance.context.data.get("intent") - intent_label = None - if intent and isinstance(intent, dict): - intent_val = intent.get("value") - intent_label = intent.get("label") - else: - intent_val = intent + if intent and "{intent}" in self.description_template: + value = intent.get("value") + if value: + intent = intent.get("label") or value - if not intent_label: - intent_label = intent_val or "" + if not intent and not comment: + self.log.info("Skipping. Intent and comment are empty.") + return # if intent label is set then format comment # - it is possible that intent_label is equal to "" (empty string) - if intent_label: - self.log.debug( - "Intent label is set to `{}`.".format(intent_label) - ) - + if intent: + self.log.debug("Intent is set to `{}`.".format(intent)) else: self.log.debug("Intent is not set.") + # If we would like to use more "optional" possibilities we would have + # come up with some expressions in templates or speicifc templates + # for all 3 possible combinations when comment and intent are + # set or not (when both are not set then description does not + # make sense). + fill_data = {} + if comment: + fill_data["comment"] = comment + if intent: + fill_data["intent"] = intent + + description = StringTemplate.format_template( + self.description_template, fill_data + ) + if not description.solved: + self.log.warning(( + "Couldn't solve template \"{}\" with data {}" + ).format( + self.description_template, json.dumps(fill_data, indent=4) + )) + return + + if not description: + self.log.debug(( + "Skipping. Result of template is empty string." + " Template \"{}\" Fill data: {}" + ).format( + self.description_template, json.dumps(fill_data, indent=4) + )) + return + + session = instance.context.data["ftrackSession"] for asset_version_data in asset_versions_data_by_id.values(): asset_version = asset_version_data["asset_version"] # Backwards compatibility for older settings using # attribute 'note_with_intent_template' - comment = self.description_template.format(**{ - "intent": intent_label, - "comment": comment - }) - asset_version["comment"] = comment + + asset_version["comment"] = description try: session.commit() From d7d8d45ee5589741092a66187f42f2332296420a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Aug 2022 18:27:08 +0200 Subject: [PATCH 325/432] OP-3405 - representation is not a list Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/sync_server/tray/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index f05a5bd8ea..629c4cbbf1 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -923,7 +923,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): representation = get_representation_by_id(self.project, repre_id) if representation: self.sync_server.update_db(self.project, None, None, - representation.pop(), + representation, get_local_site_id(), priority=value) self.is_editing = False From 8f5360d9d55efefc7bdfa9e182b279bb046ce733 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 18:28:40 +0200 Subject: [PATCH 326/432] added ability to keep '<>' without formatting content unchanged --- openpype/lib/path_templates.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/lib/path_templates.py b/openpype/lib/path_templates.py index c1282016ef..e4b18ec258 100644 --- a/openpype/lib/path_templates.py +++ b/openpype/lib/path_templates.py @@ -211,15 +211,28 @@ class StringTemplate(object): if counted_symb > -1: parts = tmp_parts.pop(counted_symb) counted_symb -= 1 + # If part contains only single string keep value + # unchanged if parts: # Remove optional start char parts.pop(0) - if counted_symb < 0: - out_parts = new_parts - else: - out_parts = tmp_parts[counted_symb] - # Store temp parts - out_parts.append(OptionalPart(parts)) + + if not parts: + value = "<>" + elif ( + len(parts) == 1 + and isinstance(parts[0], six.string_types) + ): + value = "<{}>".format(parts[0]) + else: + value = OptionalPart(parts) + + if counted_symb < 0: + out_parts = new_parts + else: + out_parts = tmp_parts[counted_symb] + # Store value + out_parts.append(value) continue if counted_symb < 0: @@ -793,6 +806,7 @@ class OptionalPart: parts(list): Parts of template. Can contain 'str', 'OptionalPart' or 'FormattingPart'. """ + def __init__(self, parts): self._parts = parts From 09e68b5a257916e07fcd8824fb0695b6e032a856 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 3 Aug 2022 18:30:25 +0200 Subject: [PATCH 327/432] use StringTemplate in integrate ftrack note --- .../plugins/publish/integrate_ftrack_note.py | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 77a7ebdfcf..ac3fa874e0 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -9,9 +9,11 @@ Requires: """ import sys +import copy import six import pyblish.api +from openpype.lib import StringTemplate class IntegrateFtrackNote(pyblish.api.InstancePlugin): @@ -53,14 +55,10 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): intent = instance.context.data.get("intent") intent_label = None - if intent and isinstance(intent, dict): - intent_val = intent.get("value") - intent_label = intent.get("label") - else: - intent_val = intent - - if not intent_label: - intent_label = intent_val or "" + if intent: + value = intent["value"] + if value: + intent_label = intent["label"] or value # if intent label is set then format comment # - it is possible that intent_label is equal to "" (empty string) @@ -96,6 +94,14 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): labels.append(label) + base_format_data = { + "host_name": host_name, + "app_name": app_name, + "app_label": app_label, + "source": instance.data.get("source", '') + } + if comment: + base_format_data["comment"] = comment for asset_version_data in asset_versions_data_by_id.values(): asset_version = asset_version_data["asset_version"] component_items = asset_version_data["component_items"] @@ -109,23 +115,31 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): template = self.note_template if template is None: template = self.note_with_intent_template - format_data = { - "intent": intent_label, - "comment": comment, - "host_name": host_name, - "app_name": app_name, - "app_label": app_label, - "published_paths": "
    ".join(sorted(published_paths)), - "source": instance.data.get("source", '') - } - comment = template.format(**format_data) - if not comment: + format_data = copy.deepcopy(base_format_data) + format_data["published_paths"] = "
    ".join( + sorted(published_paths) + ) + if intent: + if "{intent}" in template: + format_data["intent"] = intent_label + else: + format_data["intent"] = intent + + note_text = StringTemplate.format_template(template, format_data) + if not note_text.solved: + self.log.warning(( + "Note template require more keys then can be provided." + "\nTemplate: {}\nData: {}" + ).format(template, format_data)) + continue + + if not note_text: self.log.info(( "Note for AssetVersion {} would be empty. Skipping." "\nTemplate: {}\nData: {}" ).format(asset_version["id"], template, format_data)) continue - asset_version.create_note(comment, author=user, labels=labels) + asset_version.create_note(note_text, author=user, labels=labels) try: session.commit() From 3137644299e4ade30ff8e9fe1184cf0430e3a925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 11:07:29 +0200 Subject: [PATCH 328/432] :recycle: change macos installer --- setup.py | 2 +- tools/build.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 8b5a545c16..eab0187983 100644 --- a/setup.py +++ b/setup.py @@ -152,7 +152,7 @@ build_exe_options = dict( ) bdist_mac_options = dict( - bundle_name="OpenPype", + bundle_name=f"OpenPype {__version__}", iconfile=mac_icon_path ) diff --git a/tools/build.sh b/tools/build.sh index 79fb748cd5..fa2c580648 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -193,15 +193,15 @@ if [ "$disable_submodule_update" == 1 ]; then if [[ "$OSTYPE" == "darwin"* ]]; then # fix code signing issue - codesign --remove-signature "$openpype_root/build/OpenPype.app/Contents/MacOS/lib/Python" + codesign --remove-signature "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/lib/Python" if command -v create-dmg > /dev/null 2>&1; then create-dmg \ - --volname "OpenPype Installer" \ + --volname "OpenPype $openpype_version Installer" \ --window-pos 200 120 \ --window-size 600 300 \ --app-drop-link 100 50 \ - "$openpype_root/build/OpenPype-Installer.dmg" \ - "$openpype_root/build/OpenPype.app" + "$openpype_root/build/OpenPype-Installer-$openpype_version.dmg" \ + "$openpype_root/build/OpenPype $openpype_version.app" else echo -e "${BIYellow}!!!${RST} ${BIWhite}create-dmg${RST} command is not available." fi From 633c7a5cde89a27c69ad24108ef802c66da02c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 11:26:33 +0200 Subject: [PATCH 329/432] :hammer: add more verbose info to Deadline --- .../repository/custom/plugins/GlobalJobPreLoad.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 5e923eb09a..793ee782f4 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -63,7 +63,7 @@ def inject_openpype_environment(deadlinePlugin): print(("Scanning for compatible requested " f"version {requested_version}")) install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if dir: + if install_dir: sub_dirs = [ f.path for f in os.scandir(install_dir) if f.is_dir() @@ -72,6 +72,7 @@ def inject_openpype_environment(deadlinePlugin): version = get_openpype_version_from_path(subdir) if not version: continue + print(f" - found: {version} - {subdir}") openpype_versions.append((version, subdir)) exe = FileUtils.SearchFileList(exe_list) @@ -81,12 +82,15 @@ def inject_openpype_environment(deadlinePlugin): version = get_openpype_version_from_path( os.path.dirname(exe)) if version: + print(f" - found: {version} - {os.path.dirname(exe)}") openpype_versions.append((version, os.path.dirname(exe))) if requested_version: # sort detected versions if openpype_versions: openpype_versions.sort(key=lambda ver: ver[0]) + print(("Latest available version found is " + f"{openpype_versions[-1][0]}")) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] for version in openpype_versions: @@ -102,6 +106,8 @@ def inject_openpype_environment(deadlinePlugin): "directory.").format(requested_version)) # sort compatible versions nad pick the last one compatible_versions.sort(key=lambda ver: ver[0]) + print(("Latest compatible version found is " + f"{compatible_versions[-1][0]}")) # create list of executables for different platform and let # Deadline decide. exe_list = [ From b9703f3fda15a9999edba3ce4be1bae43f74913a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 11:43:48 +0200 Subject: [PATCH 330/432] :bug: fix inverted condition --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 +- .../deadline/repository/custom/plugins/OpenPype/OpenPype.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 793ee782f4..e0fd22e218 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -23,7 +23,7 @@ def get_openpype_version_from_path(path, build=True): if not os.path.isfile(version_file): return None # skip if the version is not build - if not build and \ + if build and \ (not os.path.isfile(os.path.join(path, "openpype_console")) or not os.path.isfile(os.path.join(path, "openpype_console.exe"))): return None diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 79101bb90c..3eba347770 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -74,7 +74,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): if not os.path.isfile(version_file): return None # skip if the version is not build - if not build and \ + if build and \ (not os.path.isfile(os.path.join(path, "openpype_console")) or not os.path.isfile(os.path.join(path, "openpype_console.exe"))): # noqa: E501 return None From b65a360ca6415269fcd90a0ab1385be87ad8bb0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 12:25:08 +0200 Subject: [PATCH 331/432] fix types in default settings --- openpype/settings/defaults/project_settings/maya.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b98506f6a8..d52dd407f2 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -42,14 +42,14 @@ "multilayer_exr": true, "tiled": true, "aov_list": [], - "additional_options": {} + "additional_options": [] }, "vray_renderer": { "image_prefix": "maya///", "engine": "1", "image_format": "png", "aov_list": [], - "additional_options": {} + "additional_options": [] }, "redshift_renderer": { "image_prefix": "maya///", @@ -59,7 +59,7 @@ "multilayer_exr": true, "force_combine": true, "aov_list": [], - "additional_options": {} + "additional_options": [] } }, "create": { From a32ca255f6edd3c1c3f0b47c212a035e6b169792 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 12:25:30 +0200 Subject: [PATCH 332/432] resave settings to match formattings --- .../defaults/project_settings/maya.json | 31 +++++++++---------- .../project_settings/traypublisher.json | 8 +++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index d52dd407f2..ac0f161cf2 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -99,6 +99,20 @@ "enabled": true, "publish_mip_map": true }, + "CreateAnimation": { + "enabled": true, + "write_color_sets": false, + "defaults": [ + "Main" + ] + }, + "CreatePointCache": { + "enabled": true, + "write_color_sets": false, + "defaults": [ + "Main" + ] + }, "CreateMultiverseUsd": { "enabled": true, "defaults": [ @@ -117,14 +131,6 @@ "Main" ] }, - "CreateAnimation": { - "enabled": true, - "write_color_sets": false, - "defaults": [ - "Main" - ] - - }, "CreateAss": { "enabled": true, "defaults": [ @@ -163,13 +169,6 @@ "Sculpt" ] }, - "CreatePointCache": { - "enabled": true, - "write_color_sets": false, - "defaults": [ - "Main" - ] - }, "CreateRenderSetup": { "enabled": true, "defaults": [ @@ -977,4 +976,4 @@ "ValidateNoAnimation": false } } -} +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 2cb7d358ed..5db2a79772 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -294,8 +294,12 @@ } }, "BatchMovieCreator": { - "default_variants": ["Main"], - "default_tasks": ["Compositing"], + "default_variants": [ + "Main" + ], + "default_tasks": [ + "Compositing" + ], "extensions": [ ".mov" ] From 03c648c8fd897ab374752eea1175f6c67b281afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 13:08:51 +0200 Subject: [PATCH 333/432] :bug: fix executable detection on platforms --- .../custom/plugins/GlobalJobPreLoad.py | 24 ++++++++++++++----- .../custom/plugins/OpenPype/OpenPype.py | 17 ++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index e0fd22e218..2972eeec40 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -19,14 +19,24 @@ def get_openpype_version_from_path(path, build=True): str or None: version of OpenPype if found. """ + # fix path for application bundle on macos + if platform.system().lower() == "darwin": + path = os.path.join(path, "Contents", "MacOS", "lib", "Python") + version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): return None + # skip if the version is not build - if build and \ - (not os.path.isfile(os.path.join(path, "openpype_console")) or - not os.path.isfile(os.path.join(path, "openpype_console.exe"))): + exe = os.path.join(path, "openpype_console.exe") + if platform.system().lower() in ["linux", "darwin"]: + exe = os.path.join(path, "openpype_console") + + # if only builds are requested + if build and not os.path.isfile(exe): # noqa: E501 + print(f" ! path is not a build: {path}") return None + version = {} with open(version_file, "r") as vf: exec(vf.read(), version) @@ -64,6 +74,7 @@ def inject_openpype_environment(deadlinePlugin): f"version {requested_version}")) install_dir = DirectoryUtils.SearchDirectoryList(dir_list) if install_dir: + print(f"Looking for OpenPype at: {install_dir}") sub_dirs = [ f.path for f in os.scandir(install_dir) if f.is_dir() @@ -79,6 +90,7 @@ def inject_openpype_environment(deadlinePlugin): if openpype_versions: # if looking for requested compatible version, # add the implicitly specified to the list too. + print(f"Looking for OpenPype at: {os.path.dirname(exe)}") version = get_openpype_version_from_path( os.path.dirname(exe)) if version: @@ -89,8 +101,8 @@ def inject_openpype_environment(deadlinePlugin): # sort detected versions if openpype_versions: openpype_versions.sort(key=lambda ver: ver[0]) - print(("Latest available version found is " - f"{openpype_versions[-1][0]}")) + print(("Latest available version found is " + f"{openpype_versions[-1][0]}")) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] for version in openpype_versions: @@ -166,7 +178,7 @@ def inject_openpype_environment(deadlinePlugin): env["OPENPYPE_HEADLESS_MODE"] = "1" env["AVALON_TIMEOUT"] = "5000" - print(">>> Executing: {}".format(args)) + print(">>> Executing: {}".format(" ".join(args))) std_output = subprocess.check_output(args, cwd=os.path.dirname(exe), env=env) diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 3eba347770..aa3ddc7088 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -13,6 +13,7 @@ from Deadline.Scripting import ( import re import os +import platform ###################################################################### @@ -70,14 +71,24 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): str or None: version of OpenPype if found. """ + # fix path for application bundle on macos + if platform.system().lower() == "darwin": + path = os.path.join(path, "Contents", "MacOS", "lib", "Python") + version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): return None + # skip if the version is not build - if build and \ - (not os.path.isfile(os.path.join(path, "openpype_console")) or - not os.path.isfile(os.path.join(path, "openpype_console.exe"))): # noqa: E501 + exe = os.path.join(path, "openpype_console.exe") + if platform.system().lower() in ["linux", "darwin"]: + exe = os.path.join(path, "openpype_console") + + # if only builds are requested + if build and not os.path.isfile(exe): # noqa: E501 + print(f" ! path is not a build: {path}") return None + version = {} with open(version_file, "r") as vf: exec(vf.read(), version) From 53877ebe96114f3a38e428c502d05ce72ec4dc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 13:25:56 +0200 Subject: [PATCH 334/432] :rotating_light: unify output messages --- .../repository/custom/plugins/GlobalJobPreLoad.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 2972eeec40..b8a31e01ff 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -70,11 +70,11 @@ def inject_openpype_environment(deadlinePlugin): # lets go over all available and find compatible build. requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") if requested_version: - print(("Scanning for compatible requested " + print((">>> Scanning for compatible requested " f"version {requested_version}")) install_dir = DirectoryUtils.SearchDirectoryList(dir_list) if install_dir: - print(f"Looking for OpenPype at: {install_dir}") + print(f"--- Looking for OpenPype at: {install_dir}") sub_dirs = [ f.path for f in os.scandir(install_dir) if f.is_dir() @@ -83,7 +83,7 @@ def inject_openpype_environment(deadlinePlugin): version = get_openpype_version_from_path(subdir) if not version: continue - print(f" - found: {version} - {subdir}") + print(f" - found: {version} - {subdir}") openpype_versions.append((version, subdir)) exe = FileUtils.SearchFileList(exe_list) @@ -94,14 +94,14 @@ def inject_openpype_environment(deadlinePlugin): version = get_openpype_version_from_path( os.path.dirname(exe)) if version: - print(f" - found: {version} - {os.path.dirname(exe)}") + print(f" - found: {version} - {os.path.dirname(exe)}") openpype_versions.append((version, os.path.dirname(exe))) if requested_version: # sort detected versions if openpype_versions: openpype_versions.sort(key=lambda ver: ver[0]) - print(("Latest available version found is " + print(("*** Latest available version found is " f"{openpype_versions[-1][0]}")) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] @@ -118,7 +118,7 @@ def inject_openpype_environment(deadlinePlugin): "directory.").format(requested_version)) # sort compatible versions nad pick the last one compatible_versions.sort(key=lambda ver: ver[0]) - print(("Latest compatible version found is " + print(("*** Latest compatible version found is " f"{compatible_versions[-1][0]}")) # create list of executables for different platform and let # Deadline decide. From 097638c9e54c6fd6cd02d88d456e820c72c6a9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 14:35:01 +0200 Subject: [PATCH 335/432] :recycle: natural sort versions --- .../repository/custom/plugins/GlobalJobPreLoad.py | 13 +++++++++++-- .../repository/custom/plugins/OpenPype/OpenPype.py | 12 ++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index b8a31e01ff..17f911a686 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -100,7 +100,12 @@ def inject_openpype_environment(deadlinePlugin): if requested_version: # sort detected versions if openpype_versions: - openpype_versions.sort(key=lambda ver: ver[0]) + # use natural sorting + openpype_versions.sort( + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split('(\d+)', ver[0]) + ]) print(("*** Latest available version found is " f"{openpype_versions[-1][0]}")) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 @@ -117,7 +122,11 @@ def inject_openpype_environment(deadlinePlugin): "in Deadline or install it to configured " "directory.").format(requested_version)) # sort compatible versions nad pick the last one - compatible_versions.sort(key=lambda ver: ver[0]) + compatible_versions.sort( + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split('(\d+)', ver[0]) + ]) print(("*** Latest compatible version found is " f"{compatible_versions[-1][0]}")) # create list of executables for different platform and let diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index aa3ddc7088..d270a1b87e 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -132,7 +132,11 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): if requested_version: # sort detected versions if openpype_versions: - openpype_versions.sort(key=lambda ver: ver[0]) + openpype_versions.sort( + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split('(\d+)', ver[0]) + ]) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] for version in openpype_versions: @@ -146,7 +150,11 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): "in Deadline or install it to configured " "directory.").format(requested_version)) # sort compatible versions nad pick the last one - compatible_versions.sort(key=lambda ver: ver[0]) + compatible_versions.sort( + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split('(\d+)', ver[0]) + ]) # create list of executables for different platform and let # Deadline decide. exe_list = [ From 7de8c3394a0aa3ed5dadb6fb78e4b217956509bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 14:38:57 +0200 Subject: [PATCH 336/432] :rotating_light: fix invalid sequence warning --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 ++-- .../deadline/repository/custom/plugins/OpenPype/OpenPype.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 17f911a686..ae5f2e5914 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -104,7 +104,7 @@ def inject_openpype_environment(deadlinePlugin): openpype_versions.sort( key=lambda ver: [ int(t) if t.isdigit() else t.lower() - for t in re.split('(\d+)', ver[0]) + for t in re.split(r"(\d+)", ver[0]) ]) print(("*** Latest available version found is " f"{openpype_versions[-1][0]}")) @@ -125,7 +125,7 @@ def inject_openpype_environment(deadlinePlugin): compatible_versions.sort( key=lambda ver: [ int(t) if t.isdigit() else t.lower() - for t in re.split('(\d+)', ver[0]) + for t in re.split(r"(\d+)", ver[0]) ]) print(("*** Latest compatible version found is " f"{compatible_versions[-1][0]}")) diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index d270a1b87e..00292ed5a9 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -135,7 +135,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): openpype_versions.sort( key=lambda ver: [ int(t) if t.isdigit() else t.lower() - for t in re.split('(\d+)', ver[0]) + for t in re.split(r"(\d+)", ver[0]) ]) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] @@ -153,7 +153,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): compatible_versions.sort( key=lambda ver: [ int(t) if t.isdigit() else t.lower() - for t in re.split('(\d+)', ver[0]) + for t in re.split(r"(\d+)", ver[0]) ]) # create list of executables for different platform and let # Deadline decide. From 52eba357d6c1eeae3b2b73d13ec99140a8801a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 14:46:24 +0200 Subject: [PATCH 337/432] :rotating_light: fix hound :dog: --- .../repository/custom/plugins/GlobalJobPreLoad.py | 8 ++++---- .../repository/custom/plugins/OpenPype/OpenPype.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index ae5f2e5914..172649c951 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -123,10 +123,10 @@ def inject_openpype_environment(deadlinePlugin): "directory.").format(requested_version)) # sort compatible versions nad pick the last one compatible_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split(r"(\d+)", ver[0]) + ]) print(("*** Latest compatible version found is " f"{compatible_versions[-1][0]}")) # create list of executables for different platform and let diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 00292ed5a9..6b0f69d98f 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -151,10 +151,10 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): "directory.").format(requested_version)) # sort compatible versions nad pick the last one compatible_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) + key=lambda ver: [ + int(t) if t.isdigit() else t.lower() + for t in re.split(r"(\d+)", ver[0]) + ]) # create list of executables for different platform and let # Deadline decide. exe_list = [ From bfa906eb62043decb0c55549fbc678575384c052 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Aug 2022 15:35:09 +0200 Subject: [PATCH 338/432] OP-3698 - added profile to Webpublisher settings for timeouts Currently applicable only to PS --- .../project_settings/webpublisher.json | 9 ++++++ .../schema_project_webpublisher.json | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index 77168c25e6..cba472514e 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -1,4 +1,13 @@ { + "timeout_profiles": [ + { + "hosts": [ + "photoshop" + ], + "task_types": [], + "timeout": 600 + } + ], "publish": { "CollectPublishedFiles": { "task_type_to_family": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index b76a0fa844..2ef7a05b21 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -5,6 +5,38 @@ "label": "Web Publisher", "is_file": true, "children": [ + { + "type": "list", + "collapsible": true, + "use_label_wrap": true, + "key": "timeout_profiles", + "label": "Timeout profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum", + "multiselection": true + }, + { + "type": "separator" + }, + { + "type": "number", + "key": "timeout", + "label": "Timeout (sec)" + } + ] + } + }, { "type": "dict", "collapsible": true, From c05f893333aed9a3a1638a097b15d682b886bb3d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Aug 2022 15:36:16 +0200 Subject: [PATCH 339/432] OP-3698 - implemented timout or Webpublisher's PS processing --- openpype/lib/remote_publish.py | 29 +++++++++++++++++++++-------- openpype/pype_commands.py | 21 +++++++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 38c6b07c5b..9409b72e39 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -1,4 +1,5 @@ import os +import sys from datetime import datetime import collections @@ -9,6 +10,8 @@ import pyblish.api from openpype.client.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json +from openpype.lib.profiles_filtering import filter_profiles +from openpype.api import get_project_settings ERROR_STATUS = "error" IN_PROGRESS_STATUS = "in_progress" @@ -175,14 +178,8 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): ) -def fail_batch(_id, batches_in_progress, dbcon): - """Set current batch as failed as there are some stuck batches.""" - running_batches = [str(batch["_id"]) - for batch in batches_in_progress - if batch["_id"] != _id] - msg = "There are still running batches {}\n". \ - format("\n".join(running_batches)) - msg += "Ask admin to check them and reprocess current batch" +def fail_batch(_id, dbcon, msg): + """Set current batch as failed as there is some problem.""" dbcon.update_one( {"_id": _id}, {"$set": @@ -259,3 +256,19 @@ def get_task_data(batch_dir): "Cannot parse batch meta in {} folder".format(task_data)) return task_data + + +def get_timeout(project_name, host_name, task_type): + """Returns timeout(seconds) from Setting profile.""" + filter_data = { + "task_types": task_type, + "hosts": host_name + } + timeout_profiles = (get_project_settings(project_name)["webpublisher"] + ["timeout_profiles"]) + matching_item = filter_profiles(timeout_profiles, filter_data) + timeout = sys.maxsize + if matching_item: + timeout = matching_item["timeout"] + + return timeout diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 124eacbe39..0e217ad8a1 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -15,6 +15,7 @@ from openpype.lib.remote_publish import ( fail_batch, find_variant_key, get_task_data, + get_timeout, IN_PROGRESS_STATUS ) @@ -222,10 +223,17 @@ class PypeCommands: batches_in_progress = list(dbcon.find({"status": IN_PROGRESS_STATUS})) if len(batches_in_progress) > 1: - fail_batch(_id, batches_in_progress, dbcon) + running_batches = [str(batch["_id"]) + for batch in batches_in_progress + if batch["_id"] != _id] + msg = "There are still running batches {}\n". \ + format("\n".join(running_batches)) + msg += "Ask admin to check them and reprocess current batch" + fail_batch(_id, dbcon, msg) print("Another batch running, probably stuck, ask admin for help") - asset, task_name, _ = get_batch_asset_task_info(task_data["context"]) + asset, task_name, task_type = get_batch_asset_task_info( + task_data["context"]) application_manager = ApplicationManager() found_variant_key = find_variant_key(application_manager, host_name) @@ -269,8 +277,17 @@ class PypeCommands: launched_app = application_manager.launch(app_name, **data) + timeout = get_timeout(project, host_name, task_type) + + time_start = time.time() while launched_app.poll() is None: time.sleep(0.5) + if time.time() - time_start > timeout: + launched_app.terminate() + msg = "Timeout reached" + fail_batch(_id, dbcon, msg) + raise ValueError("Timeout reached") + @staticmethod def remotepublish(project, batch_path, user_email, targets=None): From e48eea04e6785a5ca96627bd32d60d5b2f3dbf90 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Aug 2022 15:38:10 +0200 Subject: [PATCH 340/432] OP-3698 - refactor - renamed variables --- openpype/pype_commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 0e217ad8a1..c18ca218c6 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -171,7 +171,7 @@ class PypeCommands: log.info("Publish finished.") @staticmethod - def remotepublishfromapp(project, batch_path, host_name, + def remotepublishfromapp(project_name, batch_path, host_name, user_email, targets=None): """Opens installed variant of 'host' and run remote publish there. @@ -190,8 +190,8 @@ class PypeCommands: Runs publish process as user would, in automatic fashion. Args: - project (str): project to publish (only single context is expected - per call of remotepublish + project_name (str): project to publish (only single context is + expected per call of remotepublish batch_path (str): Path batch folder. Contains subfolders with resources (workfile, another subfolder 'renders' etc.) host_name (str): 'photoshop' @@ -232,7 +232,7 @@ class PypeCommands: fail_batch(_id, dbcon, msg) print("Another batch running, probably stuck, ask admin for help") - asset, task_name, task_type = get_batch_asset_task_info( + asset_name, task_name, task_type = get_batch_asset_task_info( task_data["context"]) application_manager = ApplicationManager() @@ -241,8 +241,8 @@ class PypeCommands: # must have for proper launch of app env = get_app_environments_for_context( - project, - asset, + project_name, + asset_name, task_name, app_name ) @@ -270,14 +270,14 @@ class PypeCommands: data = { "last_workfile_path": workfile_path, "start_last_workfile": True, - "project_name": project, - "asset_name": asset, + "project_name": project_name, + "asset_name": asset_name, "task_name": task_name } launched_app = application_manager.launch(app_name, **data) - timeout = get_timeout(project, host_name, task_type) + timeout = get_timeout(project_name, host_name, task_type) time_start = time.time() while launched_app.poll() is None: From f6899fad62aa430eb1d36e18f2e170d8aba9e25e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Aug 2022 15:40:47 +0200 Subject: [PATCH 341/432] OP-3698 - updated docstring Removed raise, already in function Added default to 1 hour --- openpype/lib/remote_publish.py | 9 ++++++--- openpype/pype_commands.py | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 9409b72e39..b4b05c053b 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -1,5 +1,4 @@ import os -import sys from datetime import datetime import collections @@ -179,7 +178,11 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): def fail_batch(_id, dbcon, msg): - """Set current batch as failed as there is some problem.""" + """Set current batch as failed as there is some problem. + + Raises: + ValueError + """ dbcon.update_one( {"_id": _id}, {"$set": @@ -267,7 +270,7 @@ def get_timeout(project_name, host_name, task_type): timeout_profiles = (get_project_settings(project_name)["webpublisher"] ["timeout_profiles"]) matching_item = filter_profiles(timeout_profiles, filter_data) - timeout = sys.maxsize + timeout = 3600 if matching_item: timeout = matching_item["timeout"] diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c18ca218c6..a447aa916b 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -286,8 +286,6 @@ class PypeCommands: launched_app.terminate() msg = "Timeout reached" fail_batch(_id, dbcon, msg) - raise ValueError("Timeout reached") - @staticmethod def remotepublish(project, batch_path, user_email, targets=None): From 7f6e6649cd217997bea383bdbf1a351362717bec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 17:04:53 +0200 Subject: [PATCH 342/432] let ffmpeg handle scales by forcing original aspect ratio --- openpype/plugins/publish/extract_review.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 533a87acb4..fe5d34b1a1 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1390,9 +1390,11 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("height_half_pad: `{}`".format(height_half_pad)) filters.extend([ - "scale={}x{}:flags=lanczos".format( - width_scale, height_scale - ), + ( + "scale={}x{}" + ":flags=lanczos" + ":force_original_aspect_ratio=decrease" + ).format(output_width, output_height), "pad={}:{}:{}:{}:{}".format( output_width, output_height, width_half_pad, height_half_pad, From a0fed43787fab4b945ea850235dde2270d0203b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 17:07:54 +0200 Subject: [PATCH 343/432] don't even calculate the padded part --- openpype/plugins/publish/extract_review.py | 23 +--------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index fe5d34b1a1..7442d3aacb 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1369,35 +1369,14 @@ class ExtractReview(pyblish.api.InstancePlugin): or input_width != output_width or pixel_aspect != 1 ): - if input_res_ratio < output_res_ratio: - self.log.debug( - "Input's resolution ratio is lower then output's" - ) - width_scale = int(input_width * scale_factor_by_height) - width_half_pad = int((output_width - width_scale) / 2) - height_scale = output_height - height_half_pad = 0 - else: - self.log.debug("Input is heigher then output") - width_scale = output_width - width_half_pad = 0 - height_scale = int(input_height * scale_factor_by_width) - height_half_pad = int((output_height - height_scale) / 2) - - self.log.debug("width_scale: `{}`".format(width_scale)) - self.log.debug("width_half_pad: `{}`".format(width_half_pad)) - self.log.debug("height_scale: `{}`".format(height_scale)) - self.log.debug("height_half_pad: `{}`".format(height_half_pad)) - filters.extend([ ( "scale={}x{}" ":flags=lanczos" ":force_original_aspect_ratio=decrease" ).format(output_width, output_height), - "pad={}:{}:{}:{}:{}".format( + "pad={}:{}:(ow-iw)/2:(oh-ih)/2:{}".format( output_width, output_height, - width_half_pad, height_half_pad, overscan_color_value ), "setsar=1" From b7c377e42288f0c7cdab55dd5d0ce6ac6e46499d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 18:07:01 +0200 Subject: [PATCH 344/432] handle create, update and delete operations properly --- .../event_push_frame_values_to_task.py | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 0914933de4..0895967fb1 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -380,33 +380,49 @@ class PushFrameValuesToTaskEvent(BaseEvent): uncommited_changes = False for idx, item in enumerate(changes): new_value = item["new_value"] + old_value = item["old_value"] attr_id = item["attr_id"] entity_id = item["entity_id"] attr_key = item["attr_key"] - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = attr_id - entity_key["entity_id"] = entity_id + entity_key = collections.OrderedDict(( + ("configuration_id", attr_id), + ("entity_id", entity_id) + )) self._cached_changes.append({ "attr_key": attr_key, "entity_id": entity_id, "value": new_value, "time": datetime.datetime.now() }) + old_value_is_set = ( + old_value is not ftrack_api.symbol.NOT_SET + and old_value is not None + ) if new_value is None: + if not old_value_is_set: + continue op = ftrack_api.operation.DeleteEntityOperation( "CustomAttributeValue", entity_key ) - else: + + elif old_value_is_set: op = ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", + "CustomAttributeValue", entity_key, "value", - ftrack_api.symbol.NOT_SET, + old_value, new_value ) + else: + op = ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + entity_key, + {"value": new_value} + ) + session.recorded_operations.push(op) self.log.info(( "Changing Custom Attribute \"{}\" to value" @@ -550,7 +566,11 @@ class PushFrameValuesToTaskEvent(BaseEvent): attr_ids = set(attr_id_to_key.keys()) current_values_by_id = self.get_current_values( - session, attr_ids, entity_ids, task_entity_ids, hier_attrs + session, + attr_ids, + entity_ids, + task_entity_ids, + hier_attrs ) changes = [] @@ -567,7 +587,12 @@ class PushFrameValuesToTaskEvent(BaseEvent): # Convert new value from string new_value = values.get(attr_key) - if new_value is not None and old_value is not None: + new_value_is_valid = ( + old_value is not ftrack_api.symbol.NOT_SET + and new_value is not None + ) + + if new_value is not None and new_value_is_valid: try: new_value = type(old_value)(new_value) except Exception: @@ -581,6 +606,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): changes.append({ "new_value": new_value, "attr_id": attr_id, + "old_value": old_value, "entity_id": entity_id, "attr_key": attr_key }) @@ -645,15 +671,28 @@ class PushFrameValuesToTaskEvent(BaseEvent): return interesting_data, changed_keys_by_object_id def get_current_values( - self, session, attr_ids, entity_ids, task_entity_ids, hier_attrs + self, + session, + attr_ids, + entity_ids, + task_entity_ids, + hier_attrs ): current_values_by_id = {} if not attr_ids or not entity_ids: return current_values_by_id + for entity_id in entity_ids: + current_values_by_id[entity_id] = {} + for attr_id in attr_ids: + current_values_by_id[entity_id][attr_id] = ( + ftrack_api.symbol.NOT_SET + ) + values = query_custom_attributes( session, attr_ids, entity_ids, True ) + for item in values: entity_id = item["entity_id"] attr_id = item["configuration_id"] From 7e2f7efa64b7b7869f97a86f065532748582770e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 18:07:07 +0200 Subject: [PATCH 345/432] handle new added entities --- .../event_push_frame_values_to_task.py | 181 ++++++++++++++++-- 1 file changed, 166 insertions(+), 15 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 0895967fb1..dc76920a57 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -1,10 +1,11 @@ import collections import datetime +import copy import ftrack_api from openpype_modules.ftrack.lib import ( BaseEvent, - query_custom_attributes + query_custom_attributes, ) @@ -124,10 +125,15 @@ class PushFrameValuesToTaskEvent(BaseEvent): # Separate value changes and task parent changes _entities_info = [] + added_entities = [] + added_entity_ids = set() task_parent_changes = [] for entity_info in entities_info: if entity_info["entity_type"].lower() == "task": task_parent_changes.append(entity_info) + elif entity_info.get("action") == "add": + added_entities.append(entity_info) + added_entity_ids.add(entity_info["entityId"]) else: _entities_info.append(entity_info) entities_info = _entities_info @@ -136,6 +142,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): interesting_data, changed_keys_by_object_id = self.filter_changes( session, event, entities_info, interest_attributes ) + self.interesting_data_for_added( + session, + added_entities, + interest_attributes, + interesting_data, + changed_keys_by_object_id + ) if not interesting_data and not task_parent_changes: return @@ -151,9 +164,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): # - it is a complex way how to find out if interesting_data: self.process_attribute_changes( - session, object_types_by_name, - interesting_data, changed_keys_by_object_id, - interest_entity_types, interest_attributes + session, + object_types_by_name, + interesting_data, + changed_keys_by_object_id, + interest_entity_types, + interest_attributes, + added_entity_ids ) if task_parent_changes: @@ -163,8 +180,12 @@ class PushFrameValuesToTaskEvent(BaseEvent): ) def process_task_parent_change( - self, session, object_types_by_name, task_parent_changes, - interest_entity_types, interest_attributes + self, + session, + object_types_by_name, + task_parent_changes, + interest_entity_types, + interest_attributes ): """Push custom attribute values if task parent has changed. @@ -176,6 +197,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): real hierarchical value and non hierarchical custom attribute value should be set to hierarchical value. """ + # Store task ids which were created or moved under parent with entity # type defined in settings (interest_entity_types). task_ids = set() @@ -448,9 +470,14 @@ class PushFrameValuesToTaskEvent(BaseEvent): self.log.warning("Changing of values failed.", exc_info=True) def process_attribute_changes( - self, session, object_types_by_name, - interesting_data, changed_keys_by_object_id, - interest_entity_types, interest_attributes + self, + session, + object_types_by_name, + interesting_data, + changed_keys_by_object_id, + interest_entity_types, + interest_attributes, + added_entity_ids ): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] @@ -538,15 +565,26 @@ class PushFrameValuesToTaskEvent(BaseEvent): parent_id_by_task_id[task_id] = task_entity["parent_id"] self.finalize_attribute_changes( - session, interesting_data, - changed_keys, attrs_by_obj_id, hier_attrs, - task_entity_ids, parent_id_by_task_id + session, + interesting_data, + changed_keys, + attrs_by_obj_id, + hier_attrs, + task_entity_ids, + parent_id_by_task_id, + added_entity_ids ) def finalize_attribute_changes( - self, session, interesting_data, - changed_keys, attrs_by_obj_id, hier_attrs, - task_entity_ids, parent_id_by_task_id + self, + session, + interesting_data, + changed_keys, + attrs_by_obj_id, + hier_attrs, + task_entity_ids, + parent_id_by_task_id, + added_entity_ids ): attr_id_to_key = {} for attr_confs in attrs_by_obj_id.values(): @@ -580,7 +618,11 @@ class PushFrameValuesToTaskEvent(BaseEvent): parent_id = entity_id values = interesting_data[parent_id] + added_entity = entity_id in added_entity_ids for attr_id, old_value in current_values.items(): + if added_entity and attr_id in hier_attrs: + continue + attr_key = attr_id_to_key.get(attr_id) if not attr_key: continue @@ -591,6 +633,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): old_value is not ftrack_api.symbol.NOT_SET and new_value is not None ) + if added_entity and not new_value_is_valid: + continue if new_value is not None and new_value_is_valid: try: @@ -625,6 +669,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): interesting_data = {} changed_keys_by_object_id = {} + for entity_info in entities_info: # Care only about changes if specific keys entity_changes = {} @@ -670,6 +715,100 @@ class PushFrameValuesToTaskEvent(BaseEvent): return interesting_data, changed_keys_by_object_id + def interesting_data_for_added( + self, + session, + added_entities, + interest_attributes, + interesting_data, + changed_keys_by_object_id + ): + if not added_entities or not interest_attributes: + return + + object_type_ids = set() + entity_ids = set() + all_entity_ids = set() + object_id_by_entity_id = {} + project_id = None + entity_ids_by_parent_id = collections.defaultdict(set) + for entity_info in added_entities: + object_id = entity_info["objectTypeId"] + entity_id = entity_info["entityId"] + object_type_ids.add(object_id) + entity_ids.add(entity_id) + object_id_by_entity_id[entity_id] = object_id + + for item in entity_info["parents"]: + entity_id = item["entityId"] + all_entity_ids.add(entity_id) + parent_id = item["parentId"] + if not parent_id: + project_id = entity_id + else: + entity_ids_by_parent_id[parent_id].add(entity_id) + + hier_attrs = self.get_hierarchical_configurations( + session, interest_attributes + ) + if not hier_attrs: + return + + hier_attrs_key_by_id = { + attr_conf["id"]: attr_conf["key"] + for attr_conf in hier_attrs + } + default_values_by_key = { + attr_conf["key"]: attr_conf["default"] + for attr_conf in hier_attrs + } + + values = query_custom_attributes( + session, list(hier_attrs_key_by_id.keys()), all_entity_ids, True + ) + values_per_entity_id = {} + for entity_id in all_entity_ids: + values_per_entity_id[entity_id] = {} + for attr_name in interest_attributes: + values_per_entity_id[entity_id][attr_name] = None + + for item in values: + entity_id = item["entity_id"] + key = hier_attrs_key_by_id[item["configuration_id"]] + values_per_entity_id[entity_id][key] = item["value"] + + fill_queue = collections.deque() + fill_queue.append((project_id, default_values_by_key)) + while fill_queue: + item = fill_queue.popleft() + entity_id, values_by_key = item + entity_values = values_per_entity_id[entity_id] + new_values_by_key = copy.deepcopy(values_by_key) + for key, value in values_by_key.items(): + current_value = entity_values[key] + if current_value is None: + entity_values[key] = value + else: + new_values_by_key[key] = current_value + + for child_id in entity_ids_by_parent_id[entity_id]: + fill_queue.append((child_id, new_values_by_key)) + + for entity_id in entity_ids: + entity_changes = {} + for key, value in values_per_entity_id[entity_id].items(): + if value is not None: + entity_changes[key] = value + + if not entity_changes: + continue + + interesting_data[entity_id] = entity_changes + object_id = object_id_by_entity_id[entity_id] + if object_id not in changed_keys_by_object_id: + changed_keys_by_object_id[object_id] = set() + changed_keys_by_object_id[object_id] |= set(entity_changes.keys()) + def get_current_values( self, session, @@ -738,6 +877,18 @@ class PushFrameValuesToTaskEvent(BaseEvent): output[obj_id][attr["key"]] = attr["id"] return output, hiearchical + def get_hierarchical_configurations(self, session, interest_attributes): + hier_attr_query = ( + "select id, key, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where key in ({}) and is_hierarchical is true" + ) + if not interest_attributes: + return [] + return list(session.query(hier_attr_query.format( + self.join_query_keys(interest_attributes), + )).all()) + def register(session): PushFrameValuesToTaskEvent(session).register() From 34dff12fb35b898f2c06c08b97f59a95c33063b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 4 Aug 2022 19:13:48 +0200 Subject: [PATCH 346/432] :bug: fix build directory on darwin --- tools/build_dependencies.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index d3566dd289..d186ead881 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -29,6 +29,7 @@ import shutil import blessed import enlighten import time +import re term = blessed.Terminal() @@ -52,7 +53,7 @@ def _print(msg: str, type: int = 0) -> None: else: header = term.darkolivegreen3("--- ") - print("{}{}".format(header, msg)) + print(f"{header}{msg}") def count_folders(path: Path) -> int: @@ -95,16 +96,22 @@ assert site_pkg, "No venv site-packages are found." _print(f"Working with: {site_pkg}", 2) openpype_root = Path(os.path.dirname(__file__)).parent +version = {} +with open(openpype_root / "openpype" / "version.py") as fp: + exec(fp.read(), version) + +version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) +openpype_version = version_match[1] # create full path if platform.system().lower() == "darwin": build_dir = openpype_root.joinpath( "build", - "OpenPype.app", + f"OpenPype {openpype_version}.app", "Contents", "MacOS") else: - build_subdir = "exe.{}-{}".format(get_platform(), sys.version[0:3]) + build_subdir = f"exe.{get_platform()}-{sys.version[:3]}" build_dir = openpype_root / "build" / build_subdir _print(f"Using build at {build_dir}", 2) From 08a9cb207385a0906cc56d063c19de3aa88eb51d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 10:08:07 +0200 Subject: [PATCH 347/432] fix typo --- openpype/lib/plugin_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index c94d1251fc..060db94ae0 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -57,7 +57,7 @@ def deprecated(new_destination): stacklevel=4 ) return decorated_func(*args, **kwargs) - return wrapper- + return wrapper if func is None: return _decorator From 6d2a869b2ebdb9a46545a1e650fe8c009f93fed3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 10:08:20 +0200 Subject: [PATCH 348/432] discover loader plugins can expect project name --- openpype/pipeline/load/plugins.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index 233aace035..7438b3230f 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -2,6 +2,7 @@ import os import logging from openpype.settings import get_system_settings, get_project_settings +from openpype.pipeline import legacy_io from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -151,9 +152,10 @@ class SubsetLoaderPlugin(LoaderPlugin): pass -def discover_loader_plugins(): +def discover_loader_plugins(project_name=None): plugins = discover(LoaderPlugin) - project_name = os.environ.get("AVALON_PROJECT") + if not project_name: + project_name = legacy_io.active_project() system_settings = get_system_settings() project_settings = get_project_settings(project_name) for plugin in plugins: From 0b24237bfe178270e062e3828e804edecfe6eb23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 10:08:54 +0200 Subject: [PATCH 349/432] loader pass project name to discover loader plugins --- openpype/tools/loader/widgets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 13e18b3757..48c038418a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -434,7 +434,8 @@ class SubsetWidget(QtWidgets.QWidget): # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. - available_loaders = discover_loader_plugins() + project_name = self.dbcon.active_project() + available_loaders = discover_loader_plugins(project_name) if self.tool_name: available_loaders = lib.remove_tool_name_from_loaders( available_loaders, self.tool_name @@ -1330,7 +1331,8 @@ class RepresentationWidget(QtWidgets.QWidget): selected_side = self._get_selected_side(point_index, rows) # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. - available_loaders = discover_loader_plugins() + project_name = self.dbcon.active_project() + available_loaders = discover_loader_plugins(project_name) filtered_loaders = [] for loader in available_loaders: From cbfa9015b1f7a5d134a6ea436db587d8251fc324 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 10:45:35 +0200 Subject: [PATCH 350/432] catch failed applied settings --- openpype/pipeline/create/creator_plugins.py | 14 +++++++++++++- openpype/pipeline/load/plugins.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 4a1630d8ef..9a5d559774 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -437,12 +437,24 @@ def discover_creator_plugins(): def discover_legacy_creator_plugins(): + from openpype.lib import Logger + + log = Logger.get_logger("CreatorDiscover") + plugins = discover(LegacyCreator) project_name = os.environ.get("AVALON_PROJECT") system_settings = get_system_settings() project_settings = get_project_settings(project_name) for plugin in plugins: - plugin.apply_settings(project_settings, system_settings) + try: + plugin.apply_settings(project_settings, system_settings) + except Exception: + log.warning( + "Failed to apply settings to loader {}".format( + plugin.__name__ + ), + exc_info=True + ) return plugins diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index 7438b3230f..8cba8d8217 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -153,13 +153,24 @@ class SubsetLoaderPlugin(LoaderPlugin): def discover_loader_plugins(project_name=None): + from openpype.lib import Logger + + log = Logger.get_logger("LoaderDiscover") plugins = discover(LoaderPlugin) if not project_name: project_name = legacy_io.active_project() system_settings = get_system_settings() project_settings = get_project_settings(project_name) for plugin in plugins: - plugin.apply_settings(project_settings, system_settings) + try: + plugin.apply_settings(project_settings, system_settings) + except Exception: + log.warning( + "Failed to apply settings to loader {}".format( + plugin.__name__ + ), + exc_info=True + ) return plugins From e014deb411ebc4daaf031df28927b136fedaed56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 12:20:22 +0200 Subject: [PATCH 351/432] small variable name changes --- openpype/client/operations.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index dfb1d8c4dd..69d1eb2bb6 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -55,7 +55,7 @@ def new_project_document( "_id": _create_or_convert_to_mongo_id(entity_id), "name": project_name, "type": CURRENT_PROJECT_SCHEMA, - "data": data, + "entity_data": data, "config": config } @@ -290,6 +290,10 @@ class AbstractOperation(object): def to_data(self): """Convert opration to data that can be converted to json or others. + Warning: + Current state returns ObjectId objects which cannot be parsed by + json. + Returns: Dict[str, Any]: Description of operation. """ @@ -412,16 +416,16 @@ class UpdateOperation(AbstractOperation): ) def to_data(self): - fields = {} + changes = {} for key, value in self._update_data.items(): if value is REMOVED_VALUE: value = None - fields[key] = value + changes[key] = value output = super(UpdateOperation, self).to_data() output.update({ - "entity_id": str(self.entity_id), - "fields": fields + "entity_id": self.entity_id, + "changes": changes }) return output From fa7b7d67f94b7f8dca87088034204f3dc6f1a03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 5 Aug 2022 16:29:13 +0200 Subject: [PATCH 352/432] :bug: fix aov separator in redshift --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index c145f92f91..295791576d 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -963,7 +963,7 @@ class RenderProductsRedshift(ARenderProducts): """ prefix = super(RenderProductsRedshift, self).get_renderer_prefix() - prefix = "{}{}".format(prefix, self.aov_separator) + prefix = "{}{}".format(prefix, self.layer_data["aov_separator"]) return prefix def get_render_products(self): From 10ff3562739d260cf0ad13817c5ee2fd4a3a7636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 5 Aug 2022 16:44:30 +0200 Subject: [PATCH 353/432] :recycle: refactor the fix --- openpype/hosts/maya/api/lib_renderproducts.py | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 295791576d..1e883ea43f 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -309,6 +309,42 @@ class ARenderProducts: return lib.get_attr_in_layer(plug, layer=self.layer) + @staticmethod + def extract_separator(file_prefix): + """Extract AOV separator character from the prefix. + + Default behavior extracts the part between + last occurrences of and + + Todo: + This code also triggers for V-Ray which overrides it explicitly + so this code will invalidly debug log it couldn't extract the + AOV separator even though it does set it in RenderProductsVray. + + Args: + file_prefix (str): File prefix with tokens. + + Returns: + str or None: prefix character if it can be extracted. + """ + layer_tokens = ["", ""] + aov_tokens = ["", ""] + + def match_last(tokens, text): + """regex match the last occurence from a list of tokens""" + pattern = "(?:.*)({})".format("|".join(tokens)) + return re.search(pattern, text, re.IGNORECASE) + + layer_match = match_last(layer_tokens, file_prefix) + aov_match = match_last(aov_tokens, file_prefix) + separator = None + if layer_match and aov_match: + matches = sorted((layer_match, aov_match), + key=lambda match: match.end(1)) + separator = file_prefix[matches[0].end(1):matches[1].start(1)] + return separator + + def _get_layer_data(self): # type: () -> LayerMetadata # ______________________________________________ @@ -317,7 +353,7 @@ class ARenderProducts: # ____________________/ _, scene_basename = os.path.split(cmds.file(q=True, loc=True)) scene_name, _ = os.path.splitext(scene_basename) - + kwargs = {} file_prefix = self.get_renderer_prefix() # If the Render Layer belongs to a Render Setup layer then the @@ -332,26 +368,8 @@ class ARenderProducts: # defaultRenderLayer renders as masterLayer layer_name = "masterLayer" - # AOV separator - default behavior extracts the part between - # last occurences of and - # todo: This code also triggers for V-Ray which overrides it explicitly - # so this code will invalidly debug log it couldn't extract the - # aov separator even though it does set it in RenderProductsVray - layer_tokens = ["", ""] - aov_tokens = ["", ""] - - def match_last(tokens, text): - """regex match the last occurence from a list of tokens""" - pattern = "(?:.*)({})".format("|".join(tokens)) - return re.search(pattern, text, re.IGNORECASE) - - layer_match = match_last(layer_tokens, file_prefix) - aov_match = match_last(aov_tokens, file_prefix) - kwargs = {} - if layer_match and aov_match: - matches = sorted((layer_match, aov_match), - key=lambda match: match.end(1)) - separator = file_prefix[matches[0].end(1):matches[1].start(1)] + separator = self.extract_separator(file_prefix) + if separator: kwargs["aov_separator"] = separator else: log.debug("Couldn't extract aov separator from " @@ -962,8 +980,9 @@ class RenderProductsRedshift(ARenderProducts): :func:`ARenderProducts.get_renderer_prefix()` """ - prefix = super(RenderProductsRedshift, self).get_renderer_prefix() - prefix = "{}{}".format(prefix, self.layer_data["aov_separator"]) + file_prefix = super(RenderProductsRedshift, self).get_renderer_prefix() + separator = self.extract_separator(file_prefix) + prefix = "{}{}".format(file_prefix, separator or "_") return prefix def get_render_products(self): From 401a04c767eff76a8981a1371c36f2ec36fc9d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 5 Aug 2022 17:14:10 +0200 Subject: [PATCH 354/432] :bug: fix missing variable and handle unset Settings value --- openpype/hosts/maya/plugins/publish/collect_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index d1e87c95bb..e6fc8a01e5 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -205,7 +205,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): .get('maya')\ .get('create')\ .get('CreateRender')\ - .get('default_render_image_folder') + .get('default_render_image_folder') or "" # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None @@ -318,7 +318,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "useReferencedAovs": render_instance.data.get( "useReferencedAovs") or render_instance.data.get( "vrayUseReferencedAovs") or False, - "aovSeparator": aov_separator + "aovSeparator": layer_render_products.layer_data.aov_separator # noqa: E501 } # Collect Deadline url if Deadline module is enabled From 5bd2d1d3c865510e7c4c8528f579ba6ca0d90f18 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 6 Aug 2022 03:45:37 +0000 Subject: [PATCH 355/432] [Automated] Bump version --- CHANGELOG.md | 36 +++++++++++++++--------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9671c8b8..15a120ec2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,45 @@ # Changelog -## [3.12.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.3-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...HEAD) -**🆕 New features** - -- Traypublisher: simple editorial publishing [\#3492](https://github.com/pypeclub/OpenPype/pull/3492) - **🚀 Enhancements** +- Ftrack: Comment template can contain optional keys [\#3615](https://github.com/pypeclub/OpenPype/pull/3615) +- Ftrack: Add more metadata to ftrack components [\#3612](https://github.com/pypeclub/OpenPype/pull/3612) +- General: Add context to pyblish context [\#3594](https://github.com/pypeclub/OpenPype/pull/3594) - Kitsu: Shot&Sequence name with prefix over appends [\#3593](https://github.com/pypeclub/OpenPype/pull/3593) - Photoshop: implemented {layer} placeholder in subset template [\#3591](https://github.com/pypeclub/OpenPype/pull/3591) - General: New Integrator small fixes [\#3583](https://github.com/pypeclub/OpenPype/pull/3583) **🐛 Bug fixes** +- Ftrack: Sync hierarchical attributes can handle new created entities [\#3621](https://github.com/pypeclub/OpenPype/pull/3621) +- General: Extract review aspect ratio scale is calculated by ffmpeg [\#3620](https://github.com/pypeclub/OpenPype/pull/3620) +- Maya: Fix types of default settings [\#3617](https://github.com/pypeclub/OpenPype/pull/3617) +- Integrator: Don't force to have dot before frame [\#3611](https://github.com/pypeclub/OpenPype/pull/3611) +- AfterEffects: refactored integrate doesnt work formulti frame publishes [\#3610](https://github.com/pypeclub/OpenPype/pull/3610) +- Maya look data contents fails with custom attribute on group [\#3607](https://github.com/pypeclub/OpenPype/pull/3607) - TrayPublisher: Fix wrong conflict merge [\#3600](https://github.com/pypeclub/OpenPype/pull/3600) - Bugfix: Add OCIO as submodule to prepare for handling `maketx` color space conversion. [\#3590](https://github.com/pypeclub/OpenPype/pull/3590) - Editorial publishing workflow improvements [\#3580](https://github.com/pypeclub/OpenPype/pull/3580) +- General: Update imports in start script [\#3579](https://github.com/pypeclub/OpenPype/pull/3579) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) - Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) +- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** +- General: Plugin settings handled by plugins [\#3623](https://github.com/pypeclub/OpenPype/pull/3623) - General: Use query functions in general code [\#3596](https://github.com/pypeclub/OpenPype/pull/3596) - General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Lib cleanup [\#3571](https://github.com/pypeclub/OpenPype/pull/3571) **Merged pull requests:** +- Webpublisher: timeout for PS studio processing [\#3619](https://github.com/pypeclub/OpenPype/pull/3619) +- Core: translated validate\_containers.py into New publisher style [\#3614](https://github.com/pypeclub/OpenPype/pull/3614) - Enable write color sets on animation publish automatically [\#3582](https://github.com/pypeclub/OpenPype/pull/3582) ## [3.12.2](https://github.com/pypeclub/OpenPype/tree/3.12.2) (2022-07-27) @@ -51,7 +61,6 @@ - Add pack and unpack convenience scripts [\#3502](https://github.com/pypeclub/OpenPype/pull/3502) - NewPublisher: Keep plugins with mismatch target in report [\#3498](https://github.com/pypeclub/OpenPype/pull/3498) - Nuke: load clip with options from settings [\#3497](https://github.com/pypeclub/OpenPype/pull/3497) -- TrayPublisher: implemented render\_mov\_batch [\#3486](https://github.com/pypeclub/OpenPype/pull/3486) **🐛 Bug fixes** @@ -60,7 +69,6 @@ - NewPublisher: Python 2 compatible html escape [\#3559](https://github.com/pypeclub/OpenPype/pull/3559) - Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) - General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) -- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) - Settings: Clean default values of environments [\#3550](https://github.com/pypeclub/OpenPype/pull/3550) - Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) - Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) @@ -95,20 +103,6 @@ **🚀 Enhancements** - TrayPublisher: Added more options for grouping of instances [\#3494](https://github.com/pypeclub/OpenPype/pull/3494) -- NewPublisher: Align creator attributes from top to bottom [\#3487](https://github.com/pypeclub/OpenPype/pull/3487) -- NewPublisher: Added ability to use label of instance [\#3484](https://github.com/pypeclub/OpenPype/pull/3484) -- General: Creator Plugins have access to project [\#3476](https://github.com/pypeclub/OpenPype/pull/3476) -- General: Better arguments order in creator init [\#3475](https://github.com/pypeclub/OpenPype/pull/3475) - -**🐛 Bug fixes** - -- TrayPublisher: Keep use instance label in list view [\#3493](https://github.com/pypeclub/OpenPype/pull/3493) -- General: Extract review use first frame of input sequence [\#3491](https://github.com/pypeclub/OpenPype/pull/3491) -- General: Fix Plist loading for application launch [\#3485](https://github.com/pypeclub/OpenPype/pull/3485) -- Nuke: Workfile tools open on start [\#3479](https://github.com/pypeclub/OpenPype/pull/3479) -- New Publisher: Disabled context change allows creation [\#3478](https://github.com/pypeclub/OpenPype/pull/3478) -- General: thumbnail extractor fix [\#3474](https://github.com/pypeclub/OpenPype/pull/3474) -- Kitsu: bugfix with sync-service ans publish plugins [\#3473](https://github.com/pypeclub/OpenPype/pull/3473) ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) diff --git a/openpype/version.py b/openpype/version.py index 636dff5930..3f1056249a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.3-nightly.2" +__version__ = "3.12.3-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 9ab2fd4513..66aca5e5e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.3-nightly.2" # OpenPype +version = "3.12.3-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From ed13f96a1222dbede0b8ea62268e2a8350d84ee6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 Aug 2022 19:44:43 +0800 Subject: [PATCH 356/432] fix the bug of failing to extract look when UDIMs format used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 0b26e922d5..bbd21cfa42 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,9 +429,14 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - if files_metadata[source]["color_space"] == "Raw": + try: + if files_metadata[source]["color_space"] == "Raw": # set color space to raw if we linearized it - color_space = "Raw" + color_space = "Raw" + except KeyError: + #set color space to Raw if the attribute of the color space is raw. + if cmds.getAttr(color_space_attr) == "Raw": + color_space = "Raw" # Remap file node filename to destination remap[color_space_attr] = color_space attr = resource["attribute"] From 13bc6cab8efca3d9038e76a7a6d7fb5e11663f57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 Aug 2022 20:10:04 +0800 Subject: [PATCH 357/432] fix the bug of failing to extract the look with the UDIMs format in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index bbd21cfa42..32724c64c1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -430,11 +430,11 @@ class ExtractLook(openpype.api.Extractor): color_space = "Raw" else: try: - if files_metadata[source]["color_space"] == "Raw": + if files_metadata[source]["color_space"] == "Raw": # set color space to raw if we linearized it color_space = "Raw" except KeyError: - #set color space to Raw if the attribute of the color space is raw. + # set color space to Raw if the attribute of the color space is raw. if cmds.getAttr(color_space_attr) == "Raw": color_space = "Raw" # Remap file node filename to destination From 1a7164fa90be5e394ce994a07c0355937a4987c7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 Aug 2022 20:11:07 +0800 Subject: [PATCH 358/432] fix the bug of failing to extract the look with the UDIMs format in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 32724c64c1..c6737c7215 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -434,7 +434,7 @@ class ExtractLook(openpype.api.Extractor): # set color space to raw if we linearized it color_space = "Raw" except KeyError: - # set color space to Raw if the attribute of the color space is raw. + # set color space to Raw if its attribute is raw. if cmds.getAttr(color_space_attr) == "Raw": color_space = "Raw" # Remap file node filename to destination From 38c35a87dea322e8fb81179cb40abd0549a905b7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 Aug 2022 22:15:50 +0800 Subject: [PATCH 359/432] fix AiImage colorspace and UDIMs errored out while extracting the look --- openpype/hosts/maya/plugins/publish/extract_look.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c6737c7215..9974f97f1b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,14 +429,7 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - try: - if files_metadata[source]["color_space"] == "Raw": - # set color space to raw if we linearized it - color_space = "Raw" - except KeyError: - # set color space to Raw if its attribute is raw. - if cmds.getAttr(color_space_attr) == "Raw": - color_space = "Raw" + color_space = "Raw" # Remap file node filename to destination remap[color_space_attr] = color_space attr = resource["attribute"] From 13302ca23e804ab476e1822657b91c8369bd9cb9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:29:02 +0200 Subject: [PATCH 360/432] mix audio using side file for filters --- .../publish/extract_otio_audio_tracks.py | 98 ++++++++++++------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 00c1748cdc..ed30a2f0f5 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -57,15 +57,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): audio_inputs.insert(0, empty) # create cmd - cmd = path_to_subprocess_arg(self.ffmpeg_path) + " " - cmd += self.create_cmd(audio_inputs) - cmd += path_to_subprocess_arg(audio_temp_fpath) - - # run subprocess - self.log.debug("Executing: {}".format(cmd)) - openpype.api.run_subprocess( - cmd, shell=True, logger=self.log - ) + self.mix_audio(audio_inputs, audio_temp_fpath) # remove empty os.remove(empty["mediaPath"]) @@ -245,46 +237,80 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): "durationSec": max_duration_sec } - def create_cmd(self, inputs): + def mix_audio(self, audio_inputs, audio_temp_fpath): """Creating multiple input cmd string Args: - inputs (list): list of input dicts. Order mater. + audio_inputs (list): list of input dicts. Order mater. Returns: str: the command body - """ + + longest_input = 0 + for audio_input in audio_inputs: + audio_len = audio_input["durationSec"] + if audio_len > longest_input: + longest_input = audio_len + # create cmd segments - _inputs = "" - _filters = "-filter_complex \"" - _channels = "" - for index, input in enumerate(inputs): - input_format = input.copy() - input_format.update({"i": index}) - input_format["mediaPath"] = path_to_subprocess_arg( - input_format["mediaPath"] + input_args = [] + filters = [] + tag_names = [] + for index, audio_input in enumerate(audio_inputs): + input_args.extend([ + "-ss", str(audio_input["startSec"]), + "-t", str(audio_input["durationSec"]), + "-i", audio_input["mediaPath"] + ]) + + # Output tag of a filtered audio input + tag_name = "[r{}]".format(index) + tag_names.append(tag_name) + # Delay in audio by delay in item + filters.append("[{}]adelay={}:all=1{}".format( + index, audio_input["delayMilSec"], tag_name + )) + + # Mixing filter + # - dropout transition (when audio will get loader) is set to be + # higher then any input audio item + # - volume is set to number of inputs - each mix adds 1/n volume + # where n is input inder (to get more info read ffmpeg docs and + # send a giftcard to contributor) + filters.append( + ( + "{}amix=inputs={}:duration=first:" + "dropout_transition={},volume={}[a]" + ).format( + "".join(tag_names), + len(audio_inputs), + (longest_input * 1000) + 1000, + len(audio_inputs), ) + ) - _inputs += ( - "-ss {startSec} " - "-t {durationSec} " - "-i {mediaPath} " - ).format(**input_format) + # Store filters to a file (separated by ',') + # - this is to avoid "too long" command issue in ffmpeg + with tempfile.NamedTemporaryFile( + delete=False, mode="w", suffix=".txt" + ) as tmp_file: + filters_tmp_filepath = tmp_file.name + tmp_file.write(",".join(filters)) - _filters += "[{i}]adelay={delayMilSec}:all=1[r{i}]; ".format( - **input_format) - _channels += "[r{}]".format(index) + args = [self.ffmpeg_path] + args.extend(input_args) + args.extend([ + "-filter_complex_script", filters_tmp_filepath, + "-map", "[a]" + ]) + args.append(audio_temp_fpath) - # merge all cmd segments together - cmd = _inputs + _filters + _channels - cmd += str( - "amix=inputs={inputs}:duration=first:" - "dropout_transition=1000,volume={inputs}[a]\" " - ).format(inputs=len(inputs)) - cmd += "-map \"[a]\" " + # run subprocess + self.log.debug("Executing: {}".format(args)) + openpype.api.run_subprocess(args, logger=self.log) - return cmd + os.remove(filters_tmp_filepath) def create_temp_file(self, name): """Create temp wav file From 26572719c9eb82dc6f818665c2544ef376d6769a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 8 Aug 2022 17:01:40 +0100 Subject: [PATCH 361/432] Added FBX support for update in reference loader --- openpype/hosts/maya/api/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 9280805945..2b0c6131b4 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -208,7 +208,8 @@ class ReferenceLoader(Loader): file_type = { "ma": "mayaAscii", "mb": "mayaBinary", - "abc": "Alembic" + "abc": "Alembic", + "fbx": "fbx" }.get(representation["name"]) assert file_type, "Unsupported representation: %s" % representation From ab810691c5d4d9dc3bc314a0b6ce482260d1a4ee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 8 Aug 2022 22:34:57 +0200 Subject: [PATCH 362/432] nuke: wrong key name in settings for write node type --- openpype/hosts/nuke/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 501ab4ba93..c1f49cbf8c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -912,7 +912,7 @@ def get_render_path(node): avalon_knob_data = read_avalon_data(node) nuke_imageio_writes = get_imageio_node_setting( - node_class=avalon_knob_data["family"], + node_class=avalon_knob_data["families"], plugin_name=avalon_knob_data["creator"], subset=avalon_knob_data["subset"] ) @@ -1920,7 +1920,7 @@ class WorkfileSettings(object): families.append(avalon_knob_data.get("families")) nuke_imageio_writes = get_imageio_node_setting( - node_class=avalon_knob_data["family"], + node_class=avalon_knob_data["families"], plugin_name=avalon_knob_data["creator"], subset=avalon_knob_data["subset"] ) @@ -2219,7 +2219,7 @@ def get_write_node_template_attr(node): avalon_knob_data = read_avalon_data(node) # get template data nuke_imageio_writes = get_imageio_node_setting( - node_class=avalon_knob_data["family"], + node_class=avalon_knob_data["families"], plugin_name=avalon_knob_data["creator"], subset=avalon_knob_data["subset"] ) From 61457bffde96102079c3ccfb83b9a201a3ea4b8d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 Aug 2022 15:19:12 +0800 Subject: [PATCH 363/432] fix the bug of failing to extract look with UDIMs format in aiIMage --- openpype/hosts/maya/plugins/publish/extract_look.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 9974f97f1b..ed8ada3c62 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,17 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - color_space = "Raw" + #get all the resolved files + src = files_metadata.get(source) + if src: + if files_metadata[source]["color_space"] == "Raw": + # set color space to raw if we linearized it + color_space = "Raw" + else: + # if the files are unresolved + if files_metadata[filepath]["color_space"] == "Raw": + # set color space to raw if we linearized it + color_space = "Raw" # Remap file node filename to destination remap[color_space_attr] = color_space attr = resource["attribute"] From de84296711bf8420850af5b065c328c55a2c7a27 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 Aug 2022 15:20:25 +0800 Subject: [PATCH 364/432] fix the bug of failing to extract look with UDIMs format in aiIMage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index ed8ada3c62..d69eaffe59 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,7 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - #get all the resolved files + # get all the resolved files src = files_metadata.get(source) if src: if files_metadata[source]["color_space"] == "Raw": From cb5dd41fba13c7f8e6a7fd62067d4bdddee46f66 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 Aug 2022 15:43:01 +0800 Subject: [PATCH 365/432] fix the bug of failing to extract look with UDIMs format in aiIMage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index d69eaffe59..80d82a4f58 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,7 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - # get all the resolved files + # get all the resolved files in Maya File Path Editor src = files_metadata.get(source) if src: if files_metadata[source]["color_space"] == "Raw": From b570374264f0a7cda4f5b4dc15f3c048a675548e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 9 Aug 2022 08:28:26 +0000 Subject: [PATCH 366/432] [Automated] Bump version --- CHANGELOG.md | 21 +++++++++++++-------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a120ec2a..788c915b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,29 @@ # Changelog -## [3.12.3-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.13.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...HEAD) +**🆕 New features** + +- Support for mutliple installed versions - 3.13 [\#3605](https://github.com/pypeclub/OpenPype/pull/3605) + **🚀 Enhancements** +- Editorial: Mix audio use side file for ffmpeg filters [\#3630](https://github.com/pypeclub/OpenPype/pull/3630) - Ftrack: Comment template can contain optional keys [\#3615](https://github.com/pypeclub/OpenPype/pull/3615) - Ftrack: Add more metadata to ftrack components [\#3612](https://github.com/pypeclub/OpenPype/pull/3612) - General: Add context to pyblish context [\#3594](https://github.com/pypeclub/OpenPype/pull/3594) - Kitsu: Shot&Sequence name with prefix over appends [\#3593](https://github.com/pypeclub/OpenPype/pull/3593) - Photoshop: implemented {layer} placeholder in subset template [\#3591](https://github.com/pypeclub/OpenPype/pull/3591) +- General: Python module appdirs from git [\#3589](https://github.com/pypeclub/OpenPype/pull/3589) +- Ftrack: Update ftrack api to 2.3.3 [\#3588](https://github.com/pypeclub/OpenPype/pull/3588) - General: New Integrator small fixes [\#3583](https://github.com/pypeclub/OpenPype/pull/3583) **🐛 Bug fixes** +- Maya: fix aov separator in Redshift [\#3625](https://github.com/pypeclub/OpenPype/pull/3625) +- Fix for multi-version build on Mac [\#3622](https://github.com/pypeclub/OpenPype/pull/3622) - Ftrack: Sync hierarchical attributes can handle new created entities [\#3621](https://github.com/pypeclub/OpenPype/pull/3621) - General: Extract review aspect ratio scale is calculated by ffmpeg [\#3620](https://github.com/pypeclub/OpenPype/pull/3620) - Maya: Fix types of default settings [\#3617](https://github.com/pypeclub/OpenPype/pull/3617) @@ -23,6 +32,7 @@ - Maya look data contents fails with custom attribute on group [\#3607](https://github.com/pypeclub/OpenPype/pull/3607) - TrayPublisher: Fix wrong conflict merge [\#3600](https://github.com/pypeclub/OpenPype/pull/3600) - Bugfix: Add OCIO as submodule to prepare for handling `maketx` color space conversion. [\#3590](https://github.com/pypeclub/OpenPype/pull/3590) +- Fix general settings environment variables resolution [\#3587](https://github.com/pypeclub/OpenPype/pull/3587) - Editorial publishing workflow improvements [\#3580](https://github.com/pypeclub/OpenPype/pull/3580) - General: Update imports in start script [\#3579](https://github.com/pypeclub/OpenPype/pull/3579) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) @@ -32,8 +42,8 @@ **🔀 Refactored code** - General: Plugin settings handled by plugins [\#3623](https://github.com/pypeclub/OpenPype/pull/3623) +- General: Naive implementation of document create, update, delete [\#3601](https://github.com/pypeclub/OpenPype/pull/3601) - General: Use query functions in general code [\#3596](https://github.com/pypeclub/OpenPype/pull/3596) -- General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Lib cleanup [\#3571](https://github.com/pypeclub/OpenPype/pull/3571) **Merged pull requests:** @@ -60,7 +70,6 @@ - Ftrack: add source into Note [\#3509](https://github.com/pypeclub/OpenPype/pull/3509) - Add pack and unpack convenience scripts [\#3502](https://github.com/pypeclub/OpenPype/pull/3502) - NewPublisher: Keep plugins with mismatch target in report [\#3498](https://github.com/pypeclub/OpenPype/pull/3498) -- Nuke: load clip with options from settings [\#3497](https://github.com/pypeclub/OpenPype/pull/3497) **🐛 Bug fixes** @@ -84,13 +93,13 @@ **🔀 Refactored code** +- General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Use query functions in integrator [\#3563](https://github.com/pypeclub/OpenPype/pull/3563) - General: Mongo core connection moved to client [\#3531](https://github.com/pypeclub/OpenPype/pull/3531) - Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) - General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) - General: Move load related functions into pipeline [\#3527](https://github.com/pypeclub/OpenPype/pull/3527) - General: Get current context document functions [\#3522](https://github.com/pypeclub/OpenPype/pull/3522) -- Kitsu: Use query function from client [\#3496](https://github.com/pypeclub/OpenPype/pull/3496) **Merged pull requests:** @@ -100,10 +109,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.1-nightly.6...3.12.1) -**🚀 Enhancements** - -- TrayPublisher: Added more options for grouping of instances [\#3494](https://github.com/pypeclub/OpenPype/pull/3494) - ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) diff --git a/openpype/version.py b/openpype/version.py index 3f1056249a..5dc4c0be8a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.3-nightly.3" +__version__ = "3.13.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 31a6505280..13a7609920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.3-nightly.3" # OpenPype +version = "3.13.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e595dbba85733664544c4073f92fde1a1063b68f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 9 Aug 2022 08:39:56 +0000 Subject: [PATCH 367/432] [Automated] Release --- CHANGELOG.md | 7 ++++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 788c915b9d..3124201758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.13.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...3.13.0) **🆕 New features** @@ -44,6 +44,7 @@ - General: Plugin settings handled by plugins [\#3623](https://github.com/pypeclub/OpenPype/pull/3623) - General: Naive implementation of document create, update, delete [\#3601](https://github.com/pypeclub/OpenPype/pull/3601) - General: Use query functions in general code [\#3596](https://github.com/pypeclub/OpenPype/pull/3596) +- General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Lib cleanup [\#3571](https://github.com/pypeclub/OpenPype/pull/3571) **Merged pull requests:** @@ -88,12 +89,12 @@ - General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) - Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) +- NewPublisher: Publish attributes are properly collected [\#3510](https://github.com/pypeclub/OpenPype/pull/3510) - TrayPublisher: Make sure host name is filled [\#3504](https://github.com/pypeclub/OpenPype/pull/3504) - NewPublisher: Groups work and enum multivalue [\#3501](https://github.com/pypeclub/OpenPype/pull/3501) **🔀 Refactored code** -- General: Separate extraction of template data into more functions [\#3574](https://github.com/pypeclub/OpenPype/pull/3574) - General: Use query functions in integrator [\#3563](https://github.com/pypeclub/OpenPype/pull/3563) - General: Mongo core connection moved to client [\#3531](https://github.com/pypeclub/OpenPype/pull/3531) - Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) diff --git a/openpype/version.py b/openpype/version.py index 5dc4c0be8a..d2eb3a8ab6 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.0-nightly.1" +__version__ = "3.13.0" diff --git a/pyproject.toml b/pyproject.toml index 13a7609920..03922a8e67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.0-nightly.1" # OpenPype +version = "3.13.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9427d791ea3536dda99e591280cc415969f1e3c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 11:19:08 +0200 Subject: [PATCH 368/432] moved workfile path resolving into openpype/pipeline/workfile --- openpype/pipeline/workfile/__init__.py | 14 ++ openpype/pipeline/workfile/path_resolving.py | 184 +++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 openpype/pipeline/workfile/__init__.py create mode 100644 openpype/pipeline/workfile/path_resolving.py diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py new file mode 100644 index 0000000000..3a51491cdd --- /dev/null +++ b/openpype/pipeline/workfile/__init__.py @@ -0,0 +1,14 @@ +from .path_resolving import ( + get_workfile_template_key_from_context, + get_workfile_template_key, + get_workdir_with_workdir_data, + get_workdir, +) + + +__all__ = ( + "get_workfile_template_key_from_context", + "get_workfile_template_key", + "get_workdir_with_workdir_data", + "get_workdir", +) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py new file mode 100644 index 0000000000..9525dd59dc --- /dev/null +++ b/openpype/pipeline/workfile/path_resolving.py @@ -0,0 +1,184 @@ +from openpype.client import get_asset_by_name +from openpype.settings import get_project_settings +from openpype.lib import filter_profiles +from openpype.pipeline import Anatomy +from openpype.pipeline.template_data import get_template_data + + +def get_workfile_template_key_from_context( + asset_name, task_name, host_name, project_name, project_settings=None +): + """Helper function to get template key for workfile template. + + Do the same as `get_workfile_template_key` but returns value for "session + context". + + It is required to pass one of 'dbcon' with already set project name or + 'project_name' arguments. + + Args: + asset_name(str): Name of asset document. + task_name(str): Task name for which is template key retrieved. + Must be available on asset document under `data.tasks`. + host_name(str): Name of host implementation for which is workfile + used. + project_name(str): Project name where asset and task is. Not required + when 'dbcon' is passed. + project_settings(Dict[str, Any]): Project settings for passed + 'project_name'. Not required at all but makes function faster. + """ + + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.tasks"] + ) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + return get_workfile_template_key( + task_type, host_name, project_name, project_settings + ) + + +def get_workfile_template_key( + task_type, host_name, project_name, project_settings=None +): + """Workfile template key which should be used to get workfile template. + + Function is using profiles from project settings to return right template + for passet task type and host name. + + Args: + task_type(str): Name of task type. + host_name(str): Name of host implementation (e.g. "maya", "nuke", ...) + project_name(str): Name of project in which context should look for + settings. + project_settings(Dict[str, Any]): Prepared project settings for + project name. Optional to make processing faster. + """ + + default = "work" + if not task_type or not host_name: + return default + + if not project_settings: + project_settings = get_project_settings(project_name) + + try: + profiles = ( + project_settings + ["global"] + ["tools"] + ["Workfiles"] + ["workfile_template_profiles"] + ) + except Exception: + profiles = [] + + if not profiles: + return default + + profile_filter = { + "task_types": task_type, + "hosts": host_name + } + profile = filter_profiles(profiles, profile_filter) + if profile: + return profile["workfile_template"] or default + return default + + +def get_workdir_with_workdir_data( + workdir_data, + project_name, + anatomy=None, + template_key=None, + project_settings=None +): + """Fill workdir path from entered data and project's anatomy. + + It is possible to pass only project's name instead of project's anatomy but + one of them **must** be entered. It is preferred to enter anatomy if is + available as initialization of a new Anatomy object may be time consuming. + + Args: + workdir_data (Dict[str, Any]): Data to fill workdir template. + project_name (str): Project's name. + otherwise Anatomy object is created with using the project name. + anatomy (Anatomy): Anatomy object for specific project. Faster + processing if is passed. + template_key (str): Key of work templates in anatomy templates. If not + passed `get_workfile_template_key_from_context` is used to get it. + project_settings(Dict[str, Any]): Prepared project settings for + project name. Optional to make processing faster. Ans id used only + if 'template_key' is not passed. + + Returns: + TemplateResult: Workdir path. + """ + + if not anatomy: + anatomy = Anatomy(project_name) + + if not template_key: + template_key = get_workfile_template_key( + workdir_data["task"]["type"], + workdir_data["app"], + workdir_data["project"]["name"], + project_settings + ) + + anatomy_filled = anatomy.format(workdir_data) + # Output is TemplateResult object which contain useful data + output = anatomy_filled[template_key]["folder"] + if output: + return output.normalized() + return output + + +def get_workdir( + project_doc, + asset_doc, + task_name, + host_name, + anatomy=None, + template_key=None, + project_settings=None +): + """Fill workdir path from entered data and project's anatomy. + + Args: + project_doc (Dict[str, Any]): Mongo document of project from MongoDB. + asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB. + task_name (str): Task name for which are workdir data preapred. + host_name (str): Host which is used to workdir. This is required + because workdir template may contain `{app}` key. In `Session` + is stored under `AVALON_APP` key. + anatomy (Anatomy): Optional argument. Anatomy object is created using + project name from `project_doc`. It is preferred to pass this + argument as initialization of a new Anatomy object may be time + consuming. + template_key (str): Key of work templates in anatomy templates. Default + value is defined in `get_workdir_with_workdir_data`. + project_settings(Dict[str, Any]): Prepared project settings for + project name. Optional to make processing faster. Ans id used only + if 'template_key' is not passed. + + Returns: + TemplateResult: Workdir path. + """ + + if not anatomy: + anatomy = Anatomy(project_doc["name"]) + + workdir_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + # Output is TemplateResult object which contain useful data + return get_workdir_with_workdir_data( + workdir_data, + anatomy.project_name, + anatomy, + template_key, + project_settings + ) From fabec0819beeab79cf1695d164420896254d750c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 11:19:29 +0200 Subject: [PATCH 369/432] maked moved functions as deprecated --- openpype/lib/avalon_context.py | 100 +++++++++++---------------------- 1 file changed, 32 insertions(+), 68 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 42854f39d6..636806d1f4 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -321,6 +321,8 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): ) +@deprecated( + "openpype.pipeline.workfile.get_workfile_template_key_from_context") def get_workfile_template_key_from_context( asset_name, task_name, host_name, project_name=None, dbcon=None, project_settings=None @@ -349,27 +351,26 @@ def get_workfile_template_key_from_context( ValueError: When both 'dbcon' and 'project_name' were not passed. """ + + from openpype.pipeline.workfile import ( + get_workfile_template_key_from_context + ) + if not project_name: if not dbcon: raise ValueError(( "`get_workfile_template_key_from_context` requires to pass" " one of 'dbcon' or 'project_name' arguments." )) - project_name = dbcon.active_project() - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["data.tasks"] - ) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - return get_workfile_template_key( - task_type, host_name, project_name, project_settings + return get_workfile_template_key_from_context( + asset_name, task_name, host_name, project_name, project_settings ) +@deprecated( + "openpype.pipeline.workfile.get_workfile_template_key") def get_workfile_template_key( task_type, host_name, project_name=None, project_settings=None ): @@ -393,40 +394,12 @@ def get_workfile_template_key( ValueError: When both 'project_name' and 'project_settings' were not passed. """ - default = "work" - if not task_type or not host_name: - return default - if not project_settings: - if not project_name: - raise ValueError(( - "`get_workfile_template_key` requires to pass" - " one of 'project_name' or 'project_settings' arguments." - )) - project_settings = get_project_settings(project_name) + from openpype.pipeline.workfile import get_workfile_template_key - try: - profiles = ( - project_settings - ["global"] - ["tools"] - ["Workfiles"] - ["workfile_template_profiles"] - ) - except Exception: - profiles = [] - - if not profiles: - return default - - profile_filter = { - "task_types": task_type, - "hosts": host_name - } - profile = filter_profiles(profiles, profile_filter) - if profile: - return profile["workfile_template"] or default - return default + return get_workfile_template_key( + task_type, host_name, project_name, project_settings + ) @deprecated("openpype.pipeline.template_data.get_template_data") @@ -454,6 +427,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): ) +@deprecated("openpype.pipeline.workfile.get_workdir_with_workdir_data") def get_workdir_with_workdir_data( workdir_data, anatomy=None, project_name=None, template_key=None ): @@ -480,31 +454,24 @@ def get_workdir_with_workdir_data( Raises: ValueError: When both `anatomy` and `project_name` are set to None. """ + if not anatomy and not project_name: raise ValueError(( "Missing required arguments one of `project_name` or `anatomy`" " must be entered." )) - if not anatomy: - from openpype.pipeline import Anatomy - anatomy = Anatomy(project_name) + if not project_name: + project_name = anatomy.project_name - if not template_key: - template_key = get_workfile_template_key( - workdir_data["task"]["type"], - workdir_data["app"], - project_name=workdir_data["project"]["name"] - ) + from openpype.pipeline.workfile import get_workdir_with_workdir_data - anatomy_filled = anatomy.format(workdir_data) - # Output is TemplateResult object which contain useful data - output = anatomy_filled[template_key]["folder"] - if output: - return output.normalized() - return output + return get_workdir_with_workdir_data( + workdir_data, project_name, anatomy, template_key + ) +@deprecated("openpype.pipeline.workfile.get_workdir_with_workdir_data") def get_workdir( project_doc, asset_doc, @@ -533,18 +500,15 @@ def get_workdir( TemplateResult: Workdir path. """ - from openpype.pipeline import Anatomy - from openpype.pipeline.template_data import get_template_data - - if not anatomy: - anatomy = Anatomy(project_doc["name"]) - - workdir_data = get_template_data( - project_doc, asset_doc, task_name, host_name - ) + from openpype.pipeline.workfile import get_workdir # Output is TemplateResult object which contain useful data - return get_workdir_with_workdir_data( - workdir_data, anatomy, template_key=template_key + return get_workdir( + project_doc, + asset_doc, + task_name, + host_name, + anatomy, + template_key ) From 97d55eb335e417102c519d10f280a28afb3275c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 11:39:17 +0200 Subject: [PATCH 370/432] modified docstrings --- openpype/pipeline/workfile/path_resolving.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 9525dd59dc..07a814f616 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -13,17 +13,13 @@ def get_workfile_template_key_from_context( Do the same as `get_workfile_template_key` but returns value for "session context". - It is required to pass one of 'dbcon' with already set project name or - 'project_name' arguments. - Args: asset_name(str): Name of asset document. task_name(str): Task name for which is template key retrieved. Must be available on asset document under `data.tasks`. host_name(str): Name of host implementation for which is workfile used. - project_name(str): Project name where asset and task is. Not required - when 'dbcon' is passed. + project_name(str): Project name where asset and task is. project_settings(Dict[str, Any]): Project settings for passed 'project_name'. Not required at all but makes function faster. """ @@ -104,7 +100,6 @@ def get_workdir_with_workdir_data( Args: workdir_data (Dict[str, Any]): Data to fill workdir template. project_name (str): Project's name. - otherwise Anatomy object is created with using the project name. anatomy (Anatomy): Anatomy object for specific project. Faster processing if is passed. template_key (str): Key of work templates in anatomy templates. If not From c4a932d3e2cf989b7f98e7d309b6368049619679 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Aug 2022 12:17:42 +0200 Subject: [PATCH 371/432] Refactor `get_output_link_versions` to query `data.inputLinks.id` instead of `data.inputLinks.input` --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index dd5d831ecf..326c8a58a9 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -819,7 +819,7 @@ def get_output_link_versions(project_name, version_id, fields=None): # Does make sense to look for hero versions? query_filter = { "type": "version", - "data.inputLinks.input": version_id + "data.inputLinks.id": version_id } return conn.find(query_filter, _prepare_fields(fields)) From 48c94ea22b0f53108d3023f48bd3c681b108b60d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 12:29:30 +0200 Subject: [PATCH 372/432] added operations for workfile info --- openpype/client/operations.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index 69d1eb2bb6..c4b95bf696 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -17,6 +17,7 @@ CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" CURRENT_VERSION_SCHEMA = "openpype:version-3.0" CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" +CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0" def _create_or_convert_to_mongo_id(mongo_id): @@ -188,6 +189,38 @@ def new_representation_doc( } +def new_workfile_info_doc( + filename, asset_id, task_name, files, data=None, entity_id=None +): + """Create skeleton data of workfile info document. + + Workfile document is at this moment used primarily for artist notes. + + Args: + filename (str): Filename of workfile. + asset_id (Union[str, ObjectId]): Id of asset under which workfile live. + task_name (str): Task under which was workfile created. + files (List[str]): List of rootless filepaths related to workfile. + data (Dict[str, Any]): Additional metadata. + + Returns: + Dict[str, Any]: Skeleton of workfile info document. + """ + + if not data: + data = {} + + return { + "_id": _create_or_convert_to_mongo_id(entity_id), + "type": "workfile", + "parent": ObjectId(asset_id), + "task_name": task_name, + "filename": filename, + "data": data, + "files": files + } + + def _prepare_update_data(old_doc, new_doc, replace): changes = {} for key, value in new_doc.items(): @@ -243,6 +276,20 @@ def prepare_representation_update_data(old_doc, new_doc, replace=True): return _prepare_update_data(old_doc, new_doc, replace) +def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): + """Compare two workfile info documents and prepare update data. + + Based on compared values will create update data for 'UpdateOperation'. + + Empty output means that documents are identical. + + Returns: + Dict[str, Any]: Changes between old and new document. + """ + + return _prepare_update_data(old_doc, new_doc, replace) + + @six.add_metaclass(ABCMeta) class AbstractOperation(object): """Base operation class. From adcc7010c2f84e2cd6edc2fe01065082cb63f8ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 12:31:08 +0200 Subject: [PATCH 373/432] workfiles tool use operations session to create workfile info documents --- openpype/tools/workfiles/window.py | 69 +++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 0b0d67e589..de42b80d64 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -1,18 +1,20 @@ import os import datetime +import copy from Qt import QtCore, QtWidgets, QtGui from openpype.client import ( - get_asset_by_id, get_asset_by_name, get_workfile_info, ) +from openpype.client.operations import ( + OperationsSession, + new_workfile_info_doc, + prepare_workfile_info_update_data, +) from openpype import style from openpype import resources -from openpype.lib import ( - create_workfile_doc, - save_workfile_data_to_doc, -) +from openpype.pipeline import Anatomy from openpype.pipeline import legacy_io from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.tasks_widget import TasksWidget @@ -324,10 +326,23 @@ class Window(QtWidgets.QWidget): workfile_doc, data = self.side_panel.get_workfile_data() if not workfile_doc: filepath = self.files_widget._get_selected_filepath() - self._create_workfile_doc(filepath, force=True) - workfile_doc = self._get_current_workfile_doc() + workfile_doc = self._create_workfile_doc(filepath) - save_workfile_data_to_doc(workfile_doc, data, legacy_io) + new_workfile_doc = copy.deepcopy(workfile_doc) + new_workfile_doc["data"] = data + update_data = prepare_workfile_info_update_data( + workfile_doc, new_workfile_doc + ) + if not update_data: + return + + project_name = legacy_io.active_project() + + session = OperationsSession() + session.update_entity( + project_name, "workfile", workfile_doc["_id"], update_data + ) + session.commit() def _get_current_workfile_doc(self, filepath=None): if filepath is None: @@ -343,20 +358,32 @@ class Window(QtWidgets.QWidget): project_name, asset_id, task_name, filename ) - def _create_workfile_doc(self, filepath, force=False): - workfile_doc = None - if not force: - workfile_doc = self._get_current_workfile_doc(filepath) + def _create_workfile_doc(self, filepath): + workfile_doc = self._get_current_workfile_doc(filepath) + if workfile_doc: + return workfile_doc - if not workfile_doc: - workdir, filename = os.path.split(filepath) - asset_id = self.assets_widget.get_selected_asset_id() - project_name = legacy_io.active_project() - asset_doc = get_asset_by_id(project_name, asset_id) - task_name = self.tasks_widget.get_selected_task_name() - create_workfile_doc( - asset_doc, task_name, filename, workdir, legacy_io - ) + workdir, filename = os.path.split(filepath) + + project_name = legacy_io.active_project() + asset_id = self.assets_widget.get_selected_asset_id() + task_name = self.tasks_widget.get_selected_task_name() + + anatomy = Anatomy(project_name) + success, rootless_dir = anatomy.find_root_template_from_path(workdir) + filepath = "/".join([ + os.path.normpath(rootless_dir).replace("\\", "/"), + filename + ]) + + workfile_doc = new_workfile_info_doc( + filename, asset_id, task_name, [filepath] + ) + + session = OperationsSession() + session.create_entity(project_name, "workfile", workfile_doc) + session.commit() + return workfile_doc def refresh(self): # Refresh asset widget From c64578684d4d280121c30d402815934c54af6683 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 12:31:26 +0200 Subject: [PATCH 374/432] marked create and update workfile doc functions as deprecated --- openpype/lib/avalon_context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 636806d1f4..c341b35b71 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -670,7 +670,6 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): return changes -@with_pipeline_io @deprecated("openpype.client.get_workfile_info") def get_workfile_doc(asset_id, task_name, filename, dbcon=None): """Return workfile document for entered context. @@ -691,13 +690,14 @@ def get_workfile_doc(asset_id, task_name, filename, dbcon=None): # Use legacy_io if dbcon is not entered if not dbcon: + from openpype.pipeline import legacy_io dbcon = legacy_io project_name = dbcon.active_project() return get_workfile_info(project_name, asset_id, task_name, filename) -@with_pipeline_io +@deprecated def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): """Creates or replace workfile document in mongo. @@ -718,6 +718,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): # Use legacy_io if dbcon is not entered if not dbcon: + from openpype.pipeline import legacy_io dbcon = legacy_io # Filter of workfile document @@ -764,7 +765,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): ) -@with_pipeline_io +@deprecated def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): if not workfile_doc: # TODO add log message @@ -775,6 +776,7 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): # Use legacy_io if dbcon is not entered if not dbcon: + from openpype.pipeline import legacy_io dbcon = legacy_io # Convert data to mongo modification keys/values From b89e99e8905a91deda2211138570978023c3e26e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 12:47:29 +0200 Subject: [PATCH 375/432] change imports of 'get_workfile_template_key', 'get_workfile_template_key_from_context' and 'get_workdir_with_workdir_data' and 'get_workdir' in code --- .../plugins/publish/integrate_batch_group.py | 10 +++++++-- .../tvpaint/plugins/load/load_workfile.py | 7 +++--- openpype/lib/applications.py | 22 +++++++++++++------ .../action_fill_workfile_attr.py | 11 +++++----- openpype/tools/workfiles/files_widget.py | 3 ++- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index b59107f155..4d45f67ded 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -3,9 +3,9 @@ import copy from collections import OrderedDict from pprint import pformat import pyblish -from openpype.lib import get_workdir import openpype.hosts.flame.api as opfapi import openpype.pipeline as op_pipeline +from openpype.pipeline.workfile import get_workdir class IntegrateBatchGroup(pyblish.api.InstancePlugin): @@ -324,7 +324,13 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): project_doc = instance.data["projectEntity"] asset_entity = instance.data["assetEntity"] anatomy = instance.context.data["anatomy"] + project_settings = instance.context.data["project_settings"] return get_workdir( - project_doc, asset_entity, task_data["name"], "flame", anatomy + project_doc, + asset_entity, + task_data["name"], + "flame", + anatomy, + project_settings=project_settings ) diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index 8b09d20755..40ce972a09 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -2,7 +2,6 @@ import os from openpype.lib import ( StringTemplate, - get_workfile_template_key_from_context, get_last_workfile_with_version, ) from openpype.pipeline import ( @@ -10,6 +9,9 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) +from openpype.pipeline.workfile import ( + get_workfile_template_key_from_context, +) from openpype.pipeline.template_data import get_template_data_with_names from openpype.hosts.tvpaint.api import lib, pipeline, plugin @@ -57,8 +59,7 @@ class LoadWorkfile(plugin.Loader): asset_name, task_name, host_name, - project_name=project_name, - dbcon=legacy_io + project_name=project_name ) anatomy = Anatomy(project_name) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index da8623ea13..f1ddae6063 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -27,11 +27,7 @@ from openpype.settings.constants import ( from . import PypeLogger from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username -from .avalon_context import ( - get_workdir_with_workdir_data, - get_workfile_template_key, - get_last_workfile -) +from .avalon_context import get_last_workfile from .python_module_tools import ( modules_from_path, @@ -1635,7 +1631,14 @@ def prepare_context_environments(data, env_group=None): data["task_type"] = task_type try: - workdir = get_workdir_with_workdir_data(workdir_data, anatomy) + from openpype.pipeline.workfile import get_workdir_with_workdir_data + + workdir = get_workdir_with_workdir_data( + workdir_data, + anatomy.project_name, + anatomy, + project_settings=project_settings + ) except Exception as exc: raise ApplicationLaunchFailed( @@ -1725,11 +1728,16 @@ def _prepare_last_workfile(data, workdir): if not last_workfile_path: extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) if extensions: + from openpype.pipeline import get_workfile_template_key + anatomy = data["anatomy"] project_settings = data["project_settings"] task_type = workdir_data["task"]["type"] template_key = get_workfile_template_key( - task_type, app.host_name, project_settings=project_settings + task_type, + app.host_name, + project_name, + project_settings=project_settings ) # Find last workfile file_template = str(anatomy.templates[template_key]["file"]) diff --git a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py index c7fa2dce5e..fb1cdf340e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py +++ b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py @@ -12,12 +12,10 @@ from openpype.client import ( get_assets, ) from openpype.settings import get_project_settings, get_system_settings -from openpype.lib import ( - get_workfile_template_key, - StringTemplate, -) +from openpype.lib import StringTemplate from openpype.pipeline import Anatomy from openpype.pipeline.template_data import get_template_data +from openpype.pipeline.workfile import get_workfile_template_key from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype_modules.ftrack.lib.avalon_sync import create_chunks @@ -299,7 +297,10 @@ class FillWorkfileAttributeAction(BaseAction): task_type = workfile_data["task"]["type"] template_key = get_workfile_template_key( - task_type, host_name, project_settings=project_settings + task_type, + host_name, + project_name, + project_settings=project_settings ) if template_key in templates_by_key: template = templates_by_key[template_key] diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 34692b7102..a4109c511e 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -12,7 +12,6 @@ from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.delegates import PrettyTimeDelegate from openpype.lib import ( emit_event, - get_workfile_template_key, create_workdir_extra_folders, ) from openpype.lib.avalon_context import ( @@ -24,6 +23,8 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) +from openpype.pipeline.workfile import get_workfile_template_key + from .model import ( WorkAreaFilesModel, PublishFilesModel, From 02007784faa52417e2e8bd9381dd4d7b523f1e1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 12:56:50 +0200 Subject: [PATCH 376/432] moved 'get_last_workfile_with_version' and 'get_last_workfile' to 'openpype.pipeline.workfile' --- .../tvpaint/plugins/load/load_workfile.py | 6 +- openpype/lib/applications.py | 6 +- openpype/lib/avalon_context.py | 92 ++---------- openpype/pipeline/workfile/__init__.py | 6 + openpype/pipeline/workfile/path_resolving.py | 131 +++++++++++++++++- openpype/tools/workfiles/save_as_dialog.py | 2 +- 6 files changed, 153 insertions(+), 90 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index 40ce972a09..a99b300730 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -1,9 +1,6 @@ import os -from openpype.lib import ( - StringTemplate, - get_last_workfile_with_version, -) +from openpype.lib import StringTemplate from openpype.pipeline import ( registered_host, legacy_io, @@ -11,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.workfile import ( get_workfile_template_key_from_context, + get_last_workfile_with_version, ) from openpype.pipeline.template_data import get_template_data_with_names from openpype.hosts.tvpaint.api import lib, pipeline, plugin diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index f1ddae6063..8c92665366 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -27,7 +27,6 @@ from openpype.settings.constants import ( from . import PypeLogger from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username -from .avalon_context import get_last_workfile from .python_module_tools import ( modules_from_path, @@ -1728,7 +1727,10 @@ def _prepare_last_workfile(data, workdir): if not last_workfile_path: extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) if extensions: - from openpype.pipeline import get_workfile_template_key + from openpype.pipeline.workfile import ( + get_workfile_template_key, + get_last_workfile + ) anatomy = data["anatomy"] project_settings = data["project_settings"] diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index c341b35b71..a2a1839218 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1696,6 +1696,7 @@ def get_custom_workfile_template(template_profiles): ) +@deprecated("openpype.pipeline.workfile.get_last_workfile_with_version") def get_last_workfile_with_version( workdir, file_template, fill_data, extensions ): @@ -1711,78 +1712,15 @@ def get_last_workfile_with_version( tuple: Last workfile with version if there is any otherwise returns (None, None). """ - if not os.path.exists(workdir): - return None, None - # Fast match on extension - filenames = [ - filename - for filename in os.listdir(workdir) - if os.path.splitext(filename)[1] in extensions - ] + from openpype.pipeline.workfile import get_last_workfile_with_version - # Build template without optionals, version to digits only regex - # and comment to any definable value. - _ext = [] - for ext in extensions: - if not ext.startswith("."): - ext = "." + ext - # Escape dot for regex - ext = "\\" + ext - _ext.append(ext) - ext_expression = "(?:" + "|".join(_ext) + ")" - - # Replace `.{ext}` with `{ext}` so we are sure there is not dot at the end - file_template = re.sub(r"\.?{ext}", ext_expression, file_template) - # Replace optional keys with optional content regex - file_template = re.sub(r"<.*?>", r".*?", file_template) - # Replace `{version}` with group regex - file_template = re.sub(r"{version.*?}", r"([0-9]+)", file_template) - file_template = re.sub(r"{comment.*?}", r".+?", file_template) - file_template = StringTemplate.format_strict_template( - file_template, fill_data + return get_last_workfile_with_version( + workdir, file_template, fill_data, extensions ) - # Match with ignore case on Windows due to the Windows - # OS not being case-sensitive. This avoids later running - # into the error that the file did exist if it existed - # with a different upper/lower-case. - kwargs = {} - if platform.system().lower() == "windows": - kwargs["flags"] = re.IGNORECASE - - # Get highest version among existing matching files - version = None - output_filenames = [] - for filename in sorted(filenames): - match = re.match(file_template, filename, **kwargs) - if not match: - continue - - file_version = int(match.group(1)) - if version is None or file_version > version: - output_filenames[:] = [] - version = file_version - - if file_version == version: - output_filenames.append(filename) - - output_filename = None - if output_filenames: - if len(output_filenames) == 1: - output_filename = output_filenames[0] - else: - last_time = None - for _output_filename in output_filenames: - full_path = os.path.join(workdir, _output_filename) - mod_time = os.path.getmtime(full_path) - if last_time is None or last_time < mod_time: - output_filename = _output_filename - last_time = mod_time - - return output_filename, version - +@deprecated("openpype.pipeline.workfile.get_last_workfile") def get_last_workfile( workdir, file_template, fill_data, extensions, full_path=False ): @@ -1800,22 +1738,12 @@ def get_last_workfile( Returns: str: Last or first workfile as filename of full path to filename. """ - filename, version = get_last_workfile_with_version( - workdir, file_template, fill_data, extensions + + from openpype.pipeline.workfile import get_last_workfile + + return get_last_workfile( + workdir, file_template, fill_data, extensions, full_path ) - if filename is None: - data = copy.deepcopy(fill_data) - data["version"] = 1 - data.pop("comment", None) - if not data.get("ext"): - data["ext"] = extensions[0] - data["ext"] = data["ext"].replace('.', '') - filename = StringTemplate.format_strict_template(file_template, data) - - if full_path: - return os.path.normpath(os.path.join(workdir, filename)) - - return filename @with_pipeline_io diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py index 3a51491cdd..dc4955f7af 100644 --- a/openpype/pipeline/workfile/__init__.py +++ b/openpype/pipeline/workfile/__init__.py @@ -3,6 +3,9 @@ from .path_resolving import ( get_workfile_template_key, get_workdir_with_workdir_data, get_workdir, + + get_last_workfile_with_version, + get_last_workfile, ) @@ -11,4 +14,7 @@ __all__ = ( "get_workfile_template_key", "get_workdir_with_workdir_data", "get_workdir", + + "get_last_workfile_with_version", + "get_last_workfile", ) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 07a814f616..7362902bcd 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -1,6 +1,11 @@ +import os +import re +import copy +import platform + from openpype.client import get_asset_by_name from openpype.settings import get_project_settings -from openpype.lib import filter_profiles +from openpype.lib import filter_profiles, StringTemplate from openpype.pipeline import Anatomy from openpype.pipeline.template_data import get_template_data @@ -177,3 +182,127 @@ def get_workdir( template_key, project_settings ) + + +def get_last_workfile_with_version( + workdir, file_template, fill_data, extensions +): + """Return last workfile version. + + Args: + workdir(str): Path to dir where workfiles are stored. + file_template(str): Template of file name. + fill_data(Dict[str, Any]): Data for filling template. + extensions(Iterable[str]): All allowed file extensions of workfile. + + Returns: + Tuple[Union[str, None], Union[int, None]]: Last workfile with version + if there is any workfile otherwise None for both. + """ + + if not os.path.exists(workdir): + return None, None + + # Fast match on extension + filenames = [ + filename + for filename in os.listdir(workdir) + if os.path.splitext(filename)[1] in extensions + ] + + # Build template without optionals, version to digits only regex + # and comment to any definable value. + _ext = [] + for ext in extensions: + if not ext.startswith("."): + ext = "." + ext + # Escape dot for regex + ext = "\\" + ext + _ext.append(ext) + ext_expression = "(?:" + "|".join(_ext) + ")" + + # Replace `.{ext}` with `{ext}` so we are sure there is not dot at the end + file_template = re.sub(r"\.?{ext}", ext_expression, file_template) + # Replace optional keys with optional content regex + file_template = re.sub(r"<.*?>", r".*?", file_template) + # Replace `{version}` with group regex + file_template = re.sub(r"{version.*?}", r"([0-9]+)", file_template) + file_template = re.sub(r"{comment.*?}", r".+?", file_template) + file_template = StringTemplate.format_strict_template( + file_template, fill_data + ) + + # Match with ignore case on Windows due to the Windows + # OS not being case-sensitive. This avoids later running + # into the error that the file did exist if it existed + # with a different upper/lower-case. + kwargs = {} + if platform.system().lower() == "windows": + kwargs["flags"] = re.IGNORECASE + + # Get highest version among existing matching files + version = None + output_filenames = [] + for filename in sorted(filenames): + match = re.match(file_template, filename, **kwargs) + if not match: + continue + + file_version = int(match.group(1)) + if version is None or file_version > version: + output_filenames[:] = [] + version = file_version + + if file_version == version: + output_filenames.append(filename) + + output_filename = None + if output_filenames: + if len(output_filenames) == 1: + output_filename = output_filenames[0] + else: + last_time = None + for _output_filename in output_filenames: + full_path = os.path.join(workdir, _output_filename) + mod_time = os.path.getmtime(full_path) + if last_time is None or last_time < mod_time: + output_filename = _output_filename + last_time = mod_time + + return output_filename, version + + +def get_last_workfile( + workdir, file_template, fill_data, extensions, full_path=False +): + """Return last workfile filename. + + Returns file with version 1 if there is not workfile yet. + + Args: + workdir(str): Path to dir where workfiles are stored. + file_template(str): Template of file name. + fill_data(Dict[str, Any]): Data for filling template. + extensions(Iterable[str]): All allowed file extensions of workfile. + full_path(bool): Full path to file is returned if set to True. + + Returns: + str: Last or first workfile as filename of full path to filename. + """ + + filename, version = get_last_workfile_with_version( + workdir, file_template, fill_data, extensions + ) + if filename is None: + data = copy.deepcopy(fill_data) + data["version"] = 1 + data.pop("comment", None) + if not data.get("ext"): + data["ext"] = extensions[0] + data["ext"] = data["ext"].replace('.', '') + filename = StringTemplate.format_strict_template(file_template, data) + + if full_path: + return os.path.normpath(os.path.join(workdir, filename)) + + return filename diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index ea602846e7..cded4eb1a5 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -5,11 +5,11 @@ import logging from Qt import QtWidgets, QtCore -from openpype.lib import get_last_workfile_with_version from openpype.pipeline import ( registered_host, legacy_io, ) +from openpype.pipeline.workfile import get_last_workfile_with_version from openpype.pipeline.template_data import get_template_data_with_names from openpype.tools.utils import PlaceholderLineEdit From bf463afc41abcb4afd25006422b17d940aee1300 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 13:50:16 +0200 Subject: [PATCH 377/432] moved 'get_workdir_from_session' to context tools --- .../fusion/scripts/fusion_switch_shot.py | 2 +- .../hosts/fusion/utility_scripts/switch_ui.py | 2 +- openpype/lib/avalon_context.py | 27 +++----------- openpype/pipeline/context_tools.py | 35 +++++++++++++++++++ openpype/scripts/fusion_switch_shot.py | 2 +- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index 87ff8e2ffe..49ef340679 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -15,7 +15,7 @@ from openpype.pipeline import ( from openpype.lib import version_up from openpype.hosts.fusion import api from openpype.hosts.fusion.api import lib -from openpype.lib.avalon_context import get_workdir_from_session +from openpype.pipeline.context_tools import get_workdir_from_session log = logging.getLogger("Update Slap Comp") diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index 01d55db647..93f775b24b 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( legacy_io, ) from openpype.hosts.fusion import api -from openpype.lib.avalon_context import get_workdir_from_session +from openpype.pipeline.context_tools import get_workdir_from_session log = logging.getLogger("Fusion Switch Shot") diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index a2a1839218..1b2ac459a1 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -554,6 +554,8 @@ def compute_session_changes( dict: The required changes in the Session dictionary. """ + from openpype.pipeline.context_tools import get_workdir_from_session + changes = dict() # If no changes, return directly @@ -600,30 +602,11 @@ def compute_session_changes( return changes -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.get_workdir_from_session") def get_workdir_from_session(session=None, template_key=None): - from openpype.pipeline import Anatomy - from openpype.pipeline.context_tools import get_template_data_from_session + from openpype.pipeline.context_tools import get_workdir_from_session - if session is None: - session = legacy_io.Session - project_name = session["AVALON_PROJECT"] - host_name = session["AVALON_APP"] - anatomy = Anatomy(project_name) - template_data = get_template_data_from_session(session) - anatomy_filled = anatomy.format(template_data) - - if not template_key: - task_type = template_data["task"]["type"] - template_key = get_workfile_template_key( - task_type, - host_name, - project_name=project_name - ) - path = anatomy_filled[template_key]["folder"] - if path: - path = os.path.normpath(path) - return path + return get_workdir_from_session(session, template_key) @with_pipeline_io diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index c8c70e5ea8..13185c72b2 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -22,6 +22,7 @@ from openpype.settings import get_project_settings from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy from .template_data import get_template_data_with_names +from .workfile import get_workfile_template_key from . import ( legacy_io, register_loader_plugin_path, @@ -377,3 +378,37 @@ def get_template_data_from_session(session=None, system_settings=None): return get_template_data_with_names( project_name, asset_name, task_name, host_name, system_settings ) + + +def get_workdir_from_session(session=None, template_key=None): + """Template data for template fill from session keys. + + Args: + session (Union[Dict[str, str], None]): The Session to use. If not + provided use the currently active global Session. + template_key (str): Prepared template key from which workdir is + calculated. + + Returns: + str: Workdir path. + """ + + if session is None: + session = legacy_io.Session + project_name = session["AVALON_PROJECT"] + host_name = session["AVALON_APP"] + anatomy = Anatomy(project_name) + template_data = get_template_data_from_session(session) + anatomy_filled = anatomy.format(template_data) + + if not template_key: + task_type = template_data["task"]["type"] + template_key = get_workfile_template_key( + task_type, + host_name, + project_name=project_name + ) + path = anatomy_filled[template_key]["folder"] + if path: + path = os.path.normpath(path) + return path diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index 15f189e7cb..fc22f060a2 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -17,7 +17,7 @@ from openpype.pipeline import ( legacy_io, ) -from openpype.lib.avalon_context import get_workdir_from_session +from openpype.pipeline.context_tools import get_workdir_from_session log = logging.getLogger("Update Slap Comp") From 01d87ba032dc5930526f7740bdcbd4840b9fb508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 15:10:45 +0200 Subject: [PATCH 378/432] moved build workfile to 'openpype.pipeline.workfile' --- openpype/lib/avalon_context.py | 658 +----------------- openpype/pipeline/workfile/__init__.py | 4 + openpype/pipeline/workfile/build_workfile.py | 693 +++++++++++++++++++ 3 files changed, 701 insertions(+), 654 deletions(-) create mode 100644 openpype/pipeline/workfile/build_workfile.py diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 1b2ac459a1..b32c9bce6d 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -777,661 +777,11 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) -class BuildWorkfile: - """Wrapper for build workfile process. +@deprecated("openpype.pipeline.workfile.BuildWorkfile") +def BuildWorkfile(): + from openpype.pipeline.workfile import BuildWorkfile - Load representations for current context by build presets. Build presets - are host related, since each host has it's loaders. - """ - - log = logging.getLogger("BuildWorkfile") - - @staticmethod - def map_subsets_by_family(subsets): - subsets_by_family = collections.defaultdict(list) - for subset in subsets: - family = subset["data"].get("family") - if not family: - families = subset["data"].get("families") - if not families: - continue - family = families[0] - - subsets_by_family[family].append(subset) - return subsets_by_family - - def process(self): - """Main method of this wrapper. - - Building of workfile is triggered and is possible to implement - post processing of loaded containers if necessary. - """ - containers = self.build_workfile() - - return containers - - @with_pipeline_io - def build_workfile(self): - """Prepares and load containers into workfile. - - Loads latest versions of current and linked assets to workfile by logic - stored in Workfile profiles from presets. Profiles are set by host, - filtered by current task name and used by families. - - Each family can specify representation names and loaders for - representations and first available and successful loaded - representation is returned as container. - - At the end you'll get list of loaded containers per each asset. - - loaded_containers [{ - "asset_entity": , - "containers": [, , ...] - }, { - "asset_entity": , - "containers": [, ...] - }, { - ... - }] - """ - from openpype.pipeline import discover_loader_plugins - - # Get current asset name and entity - project_name = legacy_io.active_project() - current_asset_name = legacy_io.Session["AVALON_ASSET"] - current_asset_entity = get_asset_by_name( - project_name, current_asset_name - ) - # Skip if asset was not found - if not current_asset_entity: - print("Asset entity with name `{}` was not found".format( - current_asset_name - )) - return - - # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader - - # Skip if there are any loaders - if not loaders_by_name: - self.log.warning("There are no registered loaders.") - return - - # Get current task name - current_task_name = legacy_io.Session["AVALON_TASK"] - - # Load workfile presets for task - self.build_presets = self.get_build_presets( - current_task_name, current_asset_entity - ) - - # Skip if there are any presets for task - if not self.build_presets: - self.log.warning( - "Current task `{}` does not have any loading preset.".format( - current_task_name - ) - ) - return - - # Get presets for loading current asset - current_context_profiles = self.build_presets.get("current_context") - # Get presets for loading linked assets - link_context_profiles = self.build_presets.get("linked_assets") - # Skip if both are missing - if not current_context_profiles and not link_context_profiles: - self.log.warning( - "Current task `{}` has empty loading preset.".format( - current_task_name - ) - ) - return - - elif not current_context_profiles: - self.log.warning(( - "Current task `{}` doesn't have any loading" - " preset for it's context." - ).format(current_task_name)) - - elif not link_context_profiles: - self.log.warning(( - "Current task `{}` doesn't have any" - "loading preset for it's linked assets." - ).format(current_task_name)) - - # Prepare assets to process by workfile presets - assets = [] - current_asset_id = None - if current_context_profiles: - # Add current asset entity if preset has current context set - assets.append(current_asset_entity) - current_asset_id = current_asset_entity["_id"] - - if link_context_profiles: - # Find and append linked assets if preset has set linked mapping - link_assets = get_linked_assets(current_asset_entity) - if link_assets: - assets.extend(link_assets) - - # Skip if there are no assets. This can happen if only linked mapping - # is set and there are no links for his asset. - if not assets: - self.log.warning( - "Asset does not have linked assets. Nothing to process." - ) - return - - # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) - - # Load containers by prepared entities and presets - loaded_containers = [] - # - Current asset containers - if current_asset_id and current_asset_id in prepared_entities: - current_context_data = prepared_entities.pop(current_asset_id) - loaded_data = self.load_containers_by_asset_data( - current_context_data, current_context_profiles, loaders_by_name - ) - if loaded_data: - loaded_containers.append(loaded_data) - - # - Linked assets container - for linked_asset_data in prepared_entities.values(): - loaded_data = self.load_containers_by_asset_data( - linked_asset_data, link_context_profiles, loaders_by_name - ) - if loaded_data: - loaded_containers.append(loaded_data) - - # Return list of loaded containers - return loaded_containers - - @with_pipeline_io - def get_build_presets(self, task_name, asset_doc): - """ Returns presets to build workfile for task name. - - Presets are loaded for current project set in - io.Session["AVALON_PROJECT"], filtered by registered host - and entered task name. - - Args: - task_name (str): Task name used for filtering build presets. - - Returns: - (dict): preset per entered task name - """ - host_name = os.environ["AVALON_APP"] - project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - - host_settings = project_settings.get(host_name) or {} - # Get presets for host - wb_settings = host_settings.get("workfile_builder") - if not wb_settings: - # backward compatibility - wb_settings = host_settings.get("workfile_build") or {} - - builder_profiles = wb_settings.get("profiles") - if not builder_profiles: - return None - - task_type = ( - asset_doc - .get("data", {}) - .get("tasks", {}) - .get(task_name, {}) - .get("type") - ) - filter_data = { - "task_types": task_type, - "tasks": task_name - } - return filter_profiles(builder_profiles, filter_data) - - def _filter_build_profiles(self, build_profiles, loaders_by_name): - """ Filter build profiles by loaders and prepare process data. - - Valid profile must have "loaders", "families" and "repre_names" keys - with valid values. - - "loaders" expects list of strings representing possible loaders. - - "families" expects list of strings for filtering - by main subset family. - - "repre_names" expects list of strings for filtering by - representation name. - - Lowered "families" and "repre_names" are prepared for each profile with - all required keys. - - Args: - build_profiles (dict): Profiles for building workfile. - loaders_by_name (dict): Available loaders per name. - - Returns: - (list): Filtered and prepared profiles. - """ - valid_profiles = [] - for profile in build_profiles: - # Check loaders - profile_loaders = profile.get("loaders") - if not profile_loaders: - self.log.warning(( - "Build profile has missing loaders configuration: {0}" - ).format(json.dumps(profile, indent=4))) - continue - - # Check if any loader is available - loaders_match = False - for loader_name in profile_loaders: - if loader_name in loaders_by_name: - loaders_match = True - break - - if not loaders_match: - self.log.warning(( - "All loaders from Build profile are not available: {0}" - ).format(json.dumps(profile, indent=4))) - continue - - # Check families - profile_families = profile.get("families") - if not profile_families: - self.log.warning(( - "Build profile is missing families configuration: {0}" - ).format(json.dumps(profile, indent=4))) - continue - - # Check representation names - profile_repre_names = profile.get("repre_names") - if not profile_repre_names: - self.log.warning(( - "Build profile is missing" - " representation names filtering: {0}" - ).format(json.dumps(profile, indent=4))) - continue - - # Prepare lowered families and representation names - profile["families_lowered"] = [ - fam.lower() for fam in profile_families - ] - profile["repre_names_lowered"] = [ - name.lower() for name in profile_repre_names - ] - - valid_profiles.append(profile) - - return valid_profiles - - def _prepare_profile_for_subsets(self, subsets, profiles): - """Select profile for each subset by it's data. - - Profiles are filtered for each subset individually. - Profile is filtered by subset's family, optionally by name regex and - representation names set in profile. - It is possible to not find matching profile for subset, in that case - subset is skipped and it is possible that none of subsets have - matching profile. - - Args: - subsets (list): Subset documents. - profiles (dict): Build profiles. - - Returns: - (dict) Profile by subset's id. - """ - # Prepare subsets - subsets_by_family = self.map_subsets_by_family(subsets) - - profiles_per_subset_id = {} - for family, subsets in subsets_by_family.items(): - family_low = family.lower() - for profile in profiles: - # Skip profile if does not contain family - if family_low not in profile["families_lowered"]: - continue - - # Precompile name filters as regexes - profile_regexes = profile.get("subset_name_filters") - if profile_regexes: - _profile_regexes = [] - for regex in profile_regexes: - _profile_regexes.append(re.compile(regex)) - profile_regexes = _profile_regexes - - # TODO prepare regex compilation - for subset in subsets: - # Verify regex filtering (optional) - if profile_regexes: - valid = False - for pattern in profile_regexes: - if re.match(pattern, subset["name"]): - valid = True - break - - if not valid: - continue - - profiles_per_subset_id[subset["_id"]] = profile - - # break profiles loop on finding the first matching profile - break - return profiles_per_subset_id - - def load_containers_by_asset_data( - self, asset_entity_data, build_profiles, loaders_by_name - ): - """Load containers for entered asset entity by Build profiles. - - Args: - asset_entity_data (dict): Prepared data with subsets, last version - and representations for specific asset. - build_profiles (dict): Build profiles. - loaders_by_name (dict): Available loaders per name. - - Returns: - (dict) Output contains asset document and loaded containers. - """ - - # Make sure all data are not empty - if not asset_entity_data or not build_profiles or not loaders_by_name: - return - - asset_entity = asset_entity_data["asset_entity"] - - valid_profiles = self._filter_build_profiles( - build_profiles, loaders_by_name - ) - if not valid_profiles: - self.log.warning( - "There are not valid Workfile profiles. Skipping process." - ) - return - - self.log.debug("Valid Workfile profiles: {}".format(valid_profiles)) - - subsets_by_id = {} - version_by_subset_id = {} - repres_by_version_id = {} - for subset_id, in_data in asset_entity_data["subsets"].items(): - subset_entity = in_data["subset_entity"] - subsets_by_id[subset_entity["_id"]] = subset_entity - - version_data = in_data["version"] - version_entity = version_data["version_entity"] - version_by_subset_id[subset_id] = version_entity - repres_by_version_id[version_entity["_id"]] = ( - version_data["repres"] - ) - - if not subsets_by_id: - self.log.warning("There are not subsets for asset {0}".format( - asset_entity["name"] - )) - return - - profiles_per_subset_id = self._prepare_profile_for_subsets( - subsets_by_id.values(), valid_profiles - ) - if not profiles_per_subset_id: - self.log.warning("There are not valid subsets.") - return - - valid_repres_by_subset_id = collections.defaultdict(list) - for subset_id, profile in profiles_per_subset_id.items(): - profile_repre_names = profile["repre_names_lowered"] - - version_entity = version_by_subset_id[subset_id] - version_id = version_entity["_id"] - repres = repres_by_version_id[version_id] - for repre in repres: - repre_name_low = repre["name"].lower() - if repre_name_low in profile_repre_names: - valid_repres_by_subset_id[subset_id].append(repre) - - # DEBUG message - msg = "Valid representations for Asset: `{}`".format( - asset_entity["name"] - ) - for subset_id, repres in valid_repres_by_subset_id.items(): - subset = subsets_by_id[subset_id] - msg += "\n# Subset Name/ID: `{}`/{}".format( - subset["name"], subset_id - ) - for repre in repres: - msg += "\n## Repre name: `{}`".format(repre["name"]) - - self.log.debug(msg) - - containers = self._load_containers( - valid_repres_by_subset_id, subsets_by_id, - profiles_per_subset_id, loaders_by_name - ) - - return { - "asset_entity": asset_entity, - "containers": containers - } - - @with_pipeline_io - def _load_containers( - self, repres_by_subset_id, subsets_by_id, - profiles_per_subset_id, loaders_by_name - ): - """Real load by collected data happens here. - - Loading of representations per subset happens here. Each subset can - loads one representation. Loading is tried in specific order. - Representations are tried to load by names defined in configuration. - If subset has representation matching representation name each loader - is tried to load it until any is successful. If none of them was - successful then next representation name is tried. - Subset process loop ends when any representation is loaded or - all matching representations were already tried. - - Args: - repres_by_subset_id (dict): Available representations mapped - by their parent (subset) id. - subsets_by_id (dict): Subset documents mapped by their id. - profiles_per_subset_id (dict): Build profiles mapped by subset id. - loaders_by_name (dict): Available loaders per name. - - Returns: - (list) Objects of loaded containers. - """ - from openpype.pipeline import ( - IncompatibleLoaderError, - load_container, - ) - - loaded_containers = [] - - # Get subset id order from build presets. - build_presets = self.build_presets.get("current_context", []) - build_presets += self.build_presets.get("linked_assets", []) - subset_ids_ordered = [] - for preset in build_presets: - for preset_family in preset["families"]: - for id, subset in subsets_by_id.items(): - if preset_family not in subset["data"].get("families", []): - continue - - subset_ids_ordered.append(id) - - # Order representations from subsets. - print("repres_by_subset_id", repres_by_subset_id) - representations_ordered = [] - representations = [] - for id in subset_ids_ordered: - for subset_id, repres in repres_by_subset_id.items(): - if repres in representations: - continue - - if id == subset_id: - representations_ordered.append((subset_id, repres)) - representations.append(repres) - - print("representations", representations) - - # Load ordered representations. - for subset_id, repres in representations_ordered: - subset_name = subsets_by_id[subset_id]["name"] - - profile = profiles_per_subset_id[subset_id] - loaders_last_idx = len(profile["loaders"]) - 1 - repre_names_last_idx = len(profile["repre_names_lowered"]) - 1 - - repre_by_low_name = { - repre["name"].lower(): repre for repre in repres - } - - is_loaded = False - for repre_name_idx, profile_repre_name in enumerate( - profile["repre_names_lowered"] - ): - # Break iteration if representation was already loaded - if is_loaded: - break - - repre = repre_by_low_name.get(profile_repre_name) - if not repre: - continue - - for loader_idx, loader_name in enumerate(profile["loaders"]): - if is_loaded: - break - - loader = loaders_by_name.get(loader_name) - if not loader: - continue - try: - container = load_container( - loader, - repre["_id"], - name=subset_name - ) - loaded_containers.append(container) - is_loaded = True - - except Exception as exc: - if exc == IncompatibleLoaderError: - self.log.info(( - "Loader `{}` is not compatible with" - " representation `{}`" - ).format(loader_name, repre["name"])) - - else: - self.log.error( - "Unexpected error happened during loading", - exc_info=True - ) - - msg = "Loading failed." - if loader_idx < loaders_last_idx: - msg += " Trying next loader." - elif repre_name_idx < repre_names_last_idx: - msg += ( - " Loading of subset `{}` was not successful." - ).format(subset_name) - else: - msg += " Trying next representation." - self.log.info(msg) - - return loaded_containers - - @with_pipeline_io - def _collect_last_version_repres(self, asset_docs): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - output = {} - if not asset_docs: - return output - - asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} - - project_name = legacy_io.active_project() - subsets = list(get_subsets( - project_name, asset_ids=asset_docs_by_ids.keys() - )) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - last_version_by_subset_id = get_last_versions( - project_name, subset_entity_by_ids.keys() - ) - last_version_docs_by_id = { - version["_id"]: version - for version in last_version_by_subset_id.values() - } - repre_docs = get_representations( - project_name, version_ids=last_version_docs_by_id.keys() - ) - - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_doc = last_version_docs_by_id[version_id] - - subset_id = version_doc["parent"] - subset_doc = subset_entity_by_ids[subset_id] - - asset_id = subset_doc["parent"] - asset_doc = asset_docs_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset_doc, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset_doc, - "version": { - "version_entity": version_doc, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre_doc - ) - - return output + return BuildWorkfile() @with_pipeline_io diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py index dc4955f7af..3bc125cfc4 100644 --- a/openpype/pipeline/workfile/__init__.py +++ b/openpype/pipeline/workfile/__init__.py @@ -8,6 +8,8 @@ from .path_resolving import ( get_last_workfile, ) +from .build_workfile import BuildWorkfile + __all__ = ( "get_workfile_template_key_from_context", @@ -17,4 +19,6 @@ __all__ = ( "get_last_workfile_with_version", "get_last_workfile", + + "BuildWorkfile", ) diff --git a/openpype/pipeline/workfile/build_workfile.py b/openpype/pipeline/workfile/build_workfile.py new file mode 100644 index 0000000000..bb6fcb4189 --- /dev/null +++ b/openpype/pipeline/workfile/build_workfile.py @@ -0,0 +1,693 @@ +import os +import re +import collections +import json + +from openpype.client import ( + get_asset_by_name, + get_subsets, + get_last_versions, + get_representations, +) +from openpype.settings import get_project_settings +from openpype.lib import ( + get_linked_assets, + filter_profiles, + Logger, +) +from openpype.pipeline import legacy_io +from openpype.pipeline.load import ( + discover_loader_plugins, + IncompatibleLoaderError, + load_container, +) + + +class BuildWorkfile: + """Wrapper for build workfile process. + + Load representations for current context by build presets. Build presets + are host related, since each host has it's loaders. + """ + + _log = None + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + @staticmethod + def map_subsets_by_family(subsets): + subsets_by_family = collections.defaultdict(list) + for subset in subsets: + family = subset["data"].get("family") + if not family: + families = subset["data"].get("families") + if not families: + continue + family = families[0] + + subsets_by_family[family].append(subset) + return subsets_by_family + + def process(self): + """Main method of this wrapper. + + Building of workfile is triggered and is possible to implement + post processing of loaded containers if necessary. + + Returns: + List[Dict[str, Any]]: Loaded containers during build. + """ + + return self.build_workfile() + + def build_workfile(self): + """Prepares and load containers into workfile. + + Loads latest versions of current and linked assets to workfile by logic + stored in Workfile profiles from presets. Profiles are set by host, + filtered by current task name and used by families. + + Each family can specify representation names and loaders for + representations and first available and successful loaded + representation is returned as container. + + At the end you'll get list of loaded containers per each asset. + + loaded_containers [{ + "asset_entity": , + "containers": [, , ...] + }, { + "asset_entity": , + "containers": [, ...] + }, { + ... + }] + + Returns: + List[Dict[str, Any]]: Loaded containers during build. + """ + + loaded_containers = [] + + # Get current asset name and entity + project_name = legacy_io.active_project() + current_asset_name = legacy_io.Session["AVALON_ASSET"] + current_asset_entity = get_asset_by_name( + project_name, current_asset_name + ) + # Skip if asset was not found + if not current_asset_entity: + print("Asset entity with name `{}` was not found".format( + current_asset_name + )) + return loaded_containers + + # Prepare available loaders + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {0}!".format(loader_name) + ) + loaders_by_name[loader_name] = loader + + # Skip if there are any loaders + if not loaders_by_name: + self.log.warning("There are no registered loaders.") + return loaded_containers + + # Get current task name + current_task_name = legacy_io.Session["AVALON_TASK"] + + # Load workfile presets for task + self.build_presets = self.get_build_presets( + current_task_name, current_asset_entity + ) + + # Skip if there are any presets for task + if not self.build_presets: + self.log.warning( + "Current task `{}` does not have any loading preset.".format( + current_task_name + ) + ) + return loaded_containers + + # Get presets for loading current asset + current_context_profiles = self.build_presets.get("current_context") + # Get presets for loading linked assets + link_context_profiles = self.build_presets.get("linked_assets") + # Skip if both are missing + if not current_context_profiles and not link_context_profiles: + self.log.warning( + "Current task `{}` has empty loading preset.".format( + current_task_name + ) + ) + return loaded_containers + + elif not current_context_profiles: + self.log.warning(( + "Current task `{}` doesn't have any loading" + " preset for it's context." + ).format(current_task_name)) + + elif not link_context_profiles: + self.log.warning(( + "Current task `{}` doesn't have any" + "loading preset for it's linked assets." + ).format(current_task_name)) + + # Prepare assets to process by workfile presets + assets = [] + current_asset_id = None + if current_context_profiles: + # Add current asset entity if preset has current context set + assets.append(current_asset_entity) + current_asset_id = current_asset_entity["_id"] + + if link_context_profiles: + # Find and append linked assets if preset has set linked mapping + link_assets = get_linked_assets(current_asset_entity) + if link_assets: + assets.extend(link_assets) + + # Skip if there are no assets. This can happen if only linked mapping + # is set and there are no links for his asset. + if not assets: + self.log.warning( + "Asset does not have linked assets. Nothing to process." + ) + return loaded_containers + + # Prepare entities from database for assets + prepared_entities = self._collect_last_version_repres(assets) + + # Load containers by prepared entities and presets + # - Current asset containers + if current_asset_id and current_asset_id in prepared_entities: + current_context_data = prepared_entities.pop(current_asset_id) + loaded_data = self.load_containers_by_asset_data( + current_context_data, current_context_profiles, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # - Linked assets container + for linked_asset_data in prepared_entities.values(): + loaded_data = self.load_containers_by_asset_data( + linked_asset_data, link_context_profiles, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # Return list of loaded containers + return loaded_containers + + def get_build_presets(self, task_name, asset_doc): + """ Returns presets to build workfile for task name. + + Presets are loaded for current project set in + io.Session["AVALON_PROJECT"], filtered by registered host + and entered task name. + + Args: + task_name (str): Task name used for filtering build presets. + + Returns: + Dict[str, Any]: preset per entered task name + """ + + host_name = os.environ["AVALON_APP"] + project_settings = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + + host_settings = project_settings.get(host_name) or {} + # Get presets for host + wb_settings = host_settings.get("workfile_builder") + if not wb_settings: + # backward compatibility + wb_settings = host_settings.get("workfile_build") or {} + + builder_profiles = wb_settings.get("profiles") + if not builder_profiles: + return None + + task_type = ( + asset_doc + .get("data", {}) + .get("tasks", {}) + .get(task_name, {}) + .get("type") + ) + filter_data = { + "task_types": task_type, + "tasks": task_name + } + return filter_profiles(builder_profiles, filter_data) + + def _filter_build_profiles(self, build_profiles, loaders_by_name): + """ Filter build profiles by loaders and prepare process data. + + Valid profile must have "loaders", "families" and "repre_names" keys + with valid values. + - "loaders" expects list of strings representing possible loaders. + - "families" expects list of strings for filtering + by main subset family. + - "repre_names" expects list of strings for filtering by + representation name. + + Lowered "families" and "repre_names" are prepared for each profile with + all required keys. + + Args: + build_profiles (Dict[str, Any]): Profiles for building workfile. + loaders_by_name (Dict[str, LoaderPlugin]): Available loaders + per name. + + Returns: + List[Dict[str, Any]]: Filtered and prepared profiles. + """ + + valid_profiles = [] + for profile in build_profiles: + # Check loaders + profile_loaders = profile.get("loaders") + if not profile_loaders: + self.log.warning(( + "Build profile has missing loaders configuration: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Check if any loader is available + loaders_match = False + for loader_name in profile_loaders: + if loader_name in loaders_by_name: + loaders_match = True + break + + if not loaders_match: + self.log.warning(( + "All loaders from Build profile are not available: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Check families + profile_families = profile.get("families") + if not profile_families: + self.log.warning(( + "Build profile is missing families configuration: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Check representation names + profile_repre_names = profile.get("repre_names") + if not profile_repre_names: + self.log.warning(( + "Build profile is missing" + " representation names filtering: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Prepare lowered families and representation names + profile["families_lowered"] = [ + fam.lower() for fam in profile_families + ] + profile["repre_names_lowered"] = [ + name.lower() for name in profile_repre_names + ] + + valid_profiles.append(profile) + + return valid_profiles + + def _prepare_profile_for_subsets(self, subsets, profiles): + """Select profile for each subset by it's data. + + Profiles are filtered for each subset individually. + Profile is filtered by subset's family, optionally by name regex and + representation names set in profile. + It is possible to not find matching profile for subset, in that case + subset is skipped and it is possible that none of subsets have + matching profile. + + Args: + subsets (List[Dict[str, Any]]): Subset documents. + profiles (List[Dict[str, Any]]): Build profiles. + + Returns: + Dict[str, Any]: Profile by subset's id. + """ + + # Prepare subsets + subsets_by_family = self.map_subsets_by_family(subsets) + + profiles_per_subset_id = {} + for family, subsets in subsets_by_family.items(): + family_low = family.lower() + for profile in profiles: + # Skip profile if does not contain family + if family_low not in profile["families_lowered"]: + continue + + # Precompile name filters as regexes + profile_regexes = profile.get("subset_name_filters") + if profile_regexes: + _profile_regexes = [] + for regex in profile_regexes: + _profile_regexes.append(re.compile(regex)) + profile_regexes = _profile_regexes + + # TODO prepare regex compilation + for subset in subsets: + # Verify regex filtering (optional) + if profile_regexes: + valid = False + for pattern in profile_regexes: + if re.match(pattern, subset["name"]): + valid = True + break + + if not valid: + continue + + profiles_per_subset_id[subset["_id"]] = profile + + # break profiles loop on finding the first matching profile + break + return profiles_per_subset_id + + def load_containers_by_asset_data( + self, asset_entity_data, build_profiles, loaders_by_name + ): + """Load containers for entered asset entity by Build profiles. + + Args: + asset_entity_data (Dict[str, Any]): Prepared data with subsets, + last versions and representations for specific asset. + build_profiles (Dict[str, Any]): Build profiles. + loaders_by_name (Dict[str, LoaderPlugin]): Available loaders + per name. + + Returns: + Dict[str, Any]: Output contains asset document + and loaded containers. + """ + + # Make sure all data are not empty + if not asset_entity_data or not build_profiles or not loaders_by_name: + return + + asset_entity = asset_entity_data["asset_entity"] + + valid_profiles = self._filter_build_profiles( + build_profiles, loaders_by_name + ) + if not valid_profiles: + self.log.warning( + "There are not valid Workfile profiles. Skipping process." + ) + return + + self.log.debug("Valid Workfile profiles: {}".format(valid_profiles)) + + subsets_by_id = {} + version_by_subset_id = {} + repres_by_version_id = {} + for subset_id, in_data in asset_entity_data["subsets"].items(): + subset_entity = in_data["subset_entity"] + subsets_by_id[subset_entity["_id"]] = subset_entity + + version_data = in_data["version"] + version_entity = version_data["version_entity"] + version_by_subset_id[subset_id] = version_entity + repres_by_version_id[version_entity["_id"]] = ( + version_data["repres"] + ) + + if not subsets_by_id: + self.log.warning("There are not subsets for asset {0}".format( + asset_entity["name"] + )) + return + + profiles_per_subset_id = self._prepare_profile_for_subsets( + subsets_by_id.values(), valid_profiles + ) + if not profiles_per_subset_id: + self.log.warning("There are not valid subsets.") + return + + valid_repres_by_subset_id = collections.defaultdict(list) + for subset_id, profile in profiles_per_subset_id.items(): + profile_repre_names = profile["repre_names_lowered"] + + version_entity = version_by_subset_id[subset_id] + version_id = version_entity["_id"] + repres = repres_by_version_id[version_id] + for repre in repres: + repre_name_low = repre["name"].lower() + if repre_name_low in profile_repre_names: + valid_repres_by_subset_id[subset_id].append(repre) + + # DEBUG message + msg = "Valid representations for Asset: `{}`".format( + asset_entity["name"] + ) + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = subsets_by_id[subset_id] + msg += "\n# Subset Name/ID: `{}`/{}".format( + subset["name"], subset_id + ) + for repre in repres: + msg += "\n## Repre name: `{}`".format(repre["name"]) + + self.log.debug(msg) + + containers = self._load_containers( + valid_repres_by_subset_id, subsets_by_id, + profiles_per_subset_id, loaders_by_name + ) + + return { + "asset_entity": asset_entity, + "containers": containers + } + + def _load_containers( + self, repres_by_subset_id, subsets_by_id, + profiles_per_subset_id, loaders_by_name + ): + """Real load by collected data happens here. + + Loading of representations per subset happens here. Each subset can + loads one representation. Loading is tried in specific order. + Representations are tried to load by names defined in configuration. + If subset has representation matching representation name each loader + is tried to load it until any is successful. If none of them was + successful then next representation name is tried. + Subset process loop ends when any representation is loaded or + all matching representations were already tried. + + Args: + repres_by_subset_id (Dict[str, Dict[str, Any]]): Available + representations mapped by their parent (subset) id. + subsets_by_id (Dict[str, Dict[str, Any]]): Subset documents + mapped by their id. + profiles_per_subset_id (Dict[str, Dict[str, Any]]): Build profiles + mapped by subset id. + loaders_by_name (Dict[str, LoaderPlugin]): Available loaders + per name. + + Returns: + List[Dict[str, Any]]: Objects of loaded containers. + """ + + loaded_containers = [] + + # Get subset id order from build presets. + build_presets = self.build_presets.get("current_context", []) + build_presets += self.build_presets.get("linked_assets", []) + subset_ids_ordered = [] + for preset in build_presets: + for preset_family in preset["families"]: + for id, subset in subsets_by_id.items(): + if preset_family not in subset["data"].get("families", []): + continue + + subset_ids_ordered.append(id) + + # Order representations from subsets. + print("repres_by_subset_id", repres_by_subset_id) + representations_ordered = [] + representations = [] + for id in subset_ids_ordered: + for subset_id, repres in repres_by_subset_id.items(): + if repres in representations: + continue + + if id == subset_id: + representations_ordered.append((subset_id, repres)) + representations.append(repres) + + print("representations", representations) + + # Load ordered representations. + for subset_id, repres in representations_ordered: + subset_name = subsets_by_id[subset_id]["name"] + + profile = profiles_per_subset_id[subset_id] + loaders_last_idx = len(profile["loaders"]) - 1 + repre_names_last_idx = len(profile["repre_names_lowered"]) - 1 + + repre_by_low_name = { + repre["name"].lower(): repre for repre in repres + } + + is_loaded = False + for repre_name_idx, profile_repre_name in enumerate( + profile["repre_names_lowered"] + ): + # Break iteration if representation was already loaded + if is_loaded: + break + + repre = repre_by_low_name.get(profile_repre_name) + if not repre: + continue + + for loader_idx, loader_name in enumerate(profile["loaders"]): + if is_loaded: + break + + loader = loaders_by_name.get(loader_name) + if not loader: + continue + try: + container = load_container( + loader, + repre["_id"], + name=subset_name + ) + loaded_containers.append(container) + is_loaded = True + + except Exception as exc: + if exc == IncompatibleLoaderError: + self.log.info(( + "Loader `{}` is not compatible with" + " representation `{}`" + ).format(loader_name, repre["name"])) + + else: + self.log.error( + "Unexpected error happened during loading", + exc_info=True + ) + + msg = "Loading failed." + if loader_idx < loaders_last_idx: + msg += " Trying next loader." + elif repre_name_idx < repre_names_last_idx: + msg += ( + " Loading of subset `{}` was not successful." + ).format(subset_name) + else: + msg += " Trying next representation." + self.log.info(msg) + + return loaded_containers + + def _collect_last_version_repres(self, asset_docs): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_docs (List[Dict[str, Any]]): Asset entities for which + want to find data. + + Returns: + Dict[str, Any]: collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + output = {} + if not asset_docs: + return output + + asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} + + project_name = legacy_io.active_project() + subsets = list(get_subsets( + project_name, asset_ids=asset_docs_by_ids.keys() + )) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + last_version_by_subset_id = get_last_versions( + project_name, subset_entity_by_ids.keys() + ) + last_version_docs_by_id = { + version["_id"]: version + for version in last_version_by_subset_id.values() + } + repre_docs = get_representations( + project_name, version_ids=last_version_docs_by_id.keys() + ) + + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = last_version_docs_by_id[version_id] + + subset_id = version_doc["parent"] + subset_doc = subset_entity_by_ids[subset_id] + + asset_id = subset_doc["parent"] + asset_doc = asset_docs_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset_doc, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset_doc, + "version": { + "version_entity": version_doc, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre_doc + ) + + return output From 65268fbc09e946aaa623ed178773fa2fa2961ac4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 15:11:33 +0200 Subject: [PATCH 379/432] changed import of 'BuildWorkfile' in code --- openpype/hosts/maya/api/menu.py | 2 +- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/nuke/api/pipeline.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c3ce8b0227..b7ab529a55 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,9 +6,9 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile from openpype.settings import get_project_settings from openpype.pipeline import legacy_io +from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib, lib_rendersettings from .lib import get_main_window, IS_HEADLESS diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 501ab4ba93..cf659344f0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -21,7 +21,6 @@ from openpype.client import ( ) from openpype.api import ( Logger, - BuildWorkfile, get_version_from_path, get_current_project_settings, ) @@ -40,6 +39,7 @@ from openpype.pipeline import ( Anatomy, ) from openpype.pipeline.context_tools import get_current_project_asset +from openpype.pipeline.workfile import BuildWorkfile from . import gizmo_menu diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 0afc56d2f7..c1cd8f771a 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -9,7 +9,6 @@ import pyblish.api import openpype from openpype.api import ( Logger, - BuildWorkfile, get_current_project_settings ) from openpype.lib import register_event_callback @@ -22,6 +21,7 @@ from openpype.pipeline import ( deregister_inventory_action_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop From 4db98639274917c908c5866c49c477779eb69d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 16:17:00 +0200 Subject: [PATCH 380/432] moved 'get_custom_workfile_template' and 'get_custom_workfile_template_by_string_context' to 'openpype.pipeline.workfile' --- openpype/pipeline/workfile/__init__.py | 6 + openpype/pipeline/workfile/path_resolving.py | 185 +++++++++++++++++-- 2 files changed, 176 insertions(+), 15 deletions(-) diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py index 3bc125cfc4..0aad29b6f9 100644 --- a/openpype/pipeline/workfile/__init__.py +++ b/openpype/pipeline/workfile/__init__.py @@ -6,6 +6,9 @@ from .path_resolving import ( get_last_workfile_with_version, get_last_workfile, + + get_custom_workfile_template, + get_custom_workfile_template_by_string_context, ) from .build_workfile import BuildWorkfile @@ -20,5 +23,8 @@ __all__ = ( "get_last_workfile_with_version", "get_last_workfile", + "get_custom_workfile_template", + "get_custom_workfile_template_by_string_context", + "BuildWorkfile", ) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 7362902bcd..6740b710f5 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -3,9 +3,13 @@ import re import copy import platform -from openpype.client import get_asset_by_name +from openpype.client import get_project, get_asset_by_name from openpype.settings import get_project_settings -from openpype.lib import filter_profiles, StringTemplate +from openpype.lib import ( + filter_profiles, + Logger, + StringTemplate, +) from openpype.pipeline import Anatomy from openpype.pipeline.template_data import get_template_data @@ -189,11 +193,20 @@ def get_last_workfile_with_version( ): """Return last workfile version. + Usign workfile template and it's filling data find most possible last + version of workfile which was created for the context. + + Functionality is fully based on knowing which keys are optional or what + values are expected as value. + + The last modified file is used if more files can be considered as + last workfile. + Args: - workdir(str): Path to dir where workfiles are stored. - file_template(str): Template of file name. - fill_data(Dict[str, Any]): Data for filling template. - extensions(Iterable[str]): All allowed file extensions of workfile. + workdir (str): Path to dir where workfiles are stored. + file_template (str): Template of file name. + fill_data (Dict[str, Any]): Data for filling template. + extensions (Iterable[str]): All allowed file extensions of workfile. Returns: Tuple[Union[str, None], Union[int, None]]: Last workfile with version @@ -203,23 +216,26 @@ def get_last_workfile_with_version( if not os.path.exists(workdir): return None, None + dotted_extensions = { + ".{}".format(ext) + for ext in extensions + if not ext.startswith(".") + } # Fast match on extension filenames = [ filename for filename in os.listdir(workdir) - if os.path.splitext(filename)[1] in extensions + if os.path.splitext(filename)[1] in dotted_extensions ] # Build template without optionals, version to digits only regex # and comment to any definable value. - _ext = [] - for ext in extensions: - if not ext.startswith("."): - ext = "." + ext - # Escape dot for regex - ext = "\\" + ext - _ext.append(ext) - ext_expression = "(?:" + "|".join(_ext) + ")" + # Escape extensions dot for regex + regex_exts = [ + "\\" + ext + for ext in dotted_extensions + ] + ext_expression = "(?:" + "|".join(regex_exts) + ")" # Replace `.{ext}` with `{ext}` so we are sure there is not dot at the end file_template = re.sub(r"\.?{ext}", ext_expression, file_template) @@ -306,3 +322,142 @@ def get_last_workfile( return os.path.normpath(os.path.join(workdir, filename)) return filename + + +def get_custom_workfile_template( + project_doc, + asset_doc, + task_name, + host_name, + anatomy=None, + project_settings=None +): + """Filter and fill workfile template profiles by passed context. + + Custom workfile template can be used as first version of workfiles. + Template is a file on a disk which is set in settings. Expected settings + structure to have this feature enabled is: + project settings + |- + |- workfile_builder + |- create_first_version - a bool which must be set to 'True' + |- custom_templates - profiles based on task name/type which + points to a file which is copied as + first workfile + + It is expected that passed argument are already queried documents of + project and asset as parents of processing task name. + + Args: + project_doc (Dict[str, Any]): Project document from MongoDB. + asset_doc (Dict[str, Any]): Asset document from MongoDB. + task_name (str): Name of task for which templates are filtered. + host_name (str): Name of host. + anatomy (Anatomy): Optionally passed anatomy object for passed project + name. + project_settings(Dict[str, Any]): Preloaded project settings. + + Returns: + str: Path to template or None if none of profiles match current + context. Existence of formatted path is not validated. + None: If no profile is matching context. + """ + + log = Logger.get_logger("CustomWorkfileResolve") + + project_name = project_doc["name"] + if project_settings is None: + project_settings = get_project_settings(project_name) + + host_settings = project_settings.get(host_name) + if not host_settings: + log.info("Host \"{}\" doesn't have settings".format(host_name)) + return None + + workfile_builder_settings = host_settings.get("workfile_builder") + if not workfile_builder_settings: + log.info(( + "Seems like old version of settings is used." + " Can't access custom templates in host \"{}\"." + ).format(host_name)) + return + + if not workfile_builder_settings["create_first_version"]: + log.info(( + "Project \"{}\" has turned off to create first workfile for" + " host \"{}\"" + ).format(project_name, host_name)) + return + + # Backwards compatibility + template_profiles = workfile_builder_settings.get("custom_templates") + if not template_profiles: + log.info( + "Custom templates are not filled. Skipping template copy." + ) + return + + if anatomy is None: + anatomy = Anatomy(project_name) + + # get project, asset, task anatomy context data + anatomy_context_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + # add root dict + anatomy_context_data["root"] = anatomy.roots + + # get task type for the task in context + current_task_type = anatomy_context_data["task"]["type"] + + # get path from matching profile + matching_item = filter_profiles( + template_profiles, + {"task_types": current_task_type} + ) + # when path is available try to format it in case + # there are some anatomy template strings + if matching_item: + template = matching_item["path"][platform.system().lower()] + return StringTemplate.format_strict_template( + template, anatomy_context_data + ).normalized() + + return None + + +def get_custom_workfile_template_by_string_context( + project_name, + asset_name, + task_name, + host_name, + anatomy=None, + project_settings=None +): + """Filter and fill workfile template profiles by passed context. + + Passed context are string representations of project, asset and task. + Function will query documents of project and asset to be able use + `get_custom_workfile_template` for rest of logic. + + Args: + project_name(str): Project name. + asset_name(str): Asset name. + task_name(str): Task name. + host_name (str): Name of host. + anatomy(Anatomy): Optionally prepared anatomy object for passed + project. + project_settings(Dict[str, Any]): Preloaded project settings. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + None: If no profile is matching context. + """ + + project_doc = get_project(project_name) + asset_doc = get_asset_by_name(project_name, asset_name) + + return get_custom_workfile_template( + project_doc, asset_doc, task_name, host_name, anatomy, project_settings + ) From c9289630e01245342a8ff5e7652301643638efc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 16:17:23 +0200 Subject: [PATCH 381/432] moved 'get_custom_workfile_template' as 'get_custom_workfile_template_from_session' into context tools --- openpype/pipeline/context_tools.py | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13185c72b2..5f763cd249 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -22,7 +22,10 @@ from openpype.settings import get_project_settings from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy from .template_data import get_template_data_with_names -from .workfile import get_workfile_template_key +from .workfile import ( + get_workfile_template_key, + get_custom_workfile_template_by_string_context, +) from . import ( legacy_io, register_loader_plugin_path, @@ -412,3 +415,33 @@ def get_workdir_from_session(session=None, template_key=None): if path: path = os.path.normpath(path) return path + + +def get_custom_workfile_template_from_session( + session=None, project_settings=None +): + """Filter and fill workfile template profiles by current context. + + Current context is defined by `legacy_io.Session`. That's why this + function should be used only inside host where context is set and stable. + + Args: + session (Union[None, Dict[str, str]]): Session from which are taken + data. + project_settings(Dict[str, Any]): Template profiles from settings. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + """ + + if session is None: + session = legacy_io.Session + + return get_custom_workfile_template_by_string_context( + session["AVALON_PROJECT"], + session["AVALON_ASSET"], + session["AVALON_TASK"], + session["AVALON_APP"], + project_settings=project_settings + ) From fbe1a773c016e94569913cbe8837deebea90bcb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 16:17:39 +0200 Subject: [PATCH 382/432] marked functions in avalon context as deprecated --- openpype/lib/avalon_context.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b32c9bce6d..b970cbf4e6 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -528,6 +528,7 @@ def template_data_from_session(session=None): """ from openpype.pipeline.context_tools import get_template_data_from_session + return get_template_data_from_session(session) @@ -908,6 +909,8 @@ def _get_task_context_data_for_anatomy( return data +@deprecated( + "openpype.pipeline.workfile.get_custom_workfile_template_by_context") def get_custom_workfile_template_by_context( template_profiles, project_doc, asset_doc, task_name, anatomy=None ): @@ -961,6 +964,9 @@ def get_custom_workfile_template_by_context( return None +@deprecated( + "openpype.pipeline.workfile.get_custom_workfile_template_by_string_context" +) def get_custom_workfile_template_by_string_context( template_profiles, project_name, asset_name, task_name, dbcon=None, anatomy=None @@ -1005,7 +1011,7 @@ def get_custom_workfile_template_by_string_context( ) -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.get_custom_workfile_template") def get_custom_workfile_template(template_profiles): """Filter and fill workfile template profiles by current context. @@ -1020,6 +1026,8 @@ def get_custom_workfile_template(template_profiles): context. (Existence of formatted path is not validated.) """ + from openpype.pipeline import legacy_io + return get_custom_workfile_template_by_string_context( template_profiles, legacy_io.Session["AVALON_PROJECT"], From 939955339c46c0aa02634546286a5e6217bf2cd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 16:18:23 +0200 Subject: [PATCH 383/432] use moved functions in code --- openpype/hooks/pre_copy_template_workfile.py | 52 +++++++------------- openpype/hosts/nuke/api/lib.py | 17 +++---- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index dffac22ee2..70c549919f 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -1,11 +1,11 @@ import os import shutil -from openpype.lib import ( - PreLaunchHook, - get_custom_workfile_template_by_context, +from openpype.lib import PreLaunchHook +from openpype.settings import get_project_settings +from openpype.pipeline.workfile import ( + get_custom_workfile_template, get_custom_workfile_template_by_string_context ) -from openpype.settings import get_project_settings class CopyTemplateWorkfile(PreLaunchHook): @@ -54,41 +54,22 @@ class CopyTemplateWorkfile(PreLaunchHook): project_name = self.data["project_name"] asset_name = self.data["asset_name"] task_name = self.data["task_name"] + host_name = self.application.host_name project_settings = get_project_settings(project_name) - host_settings = project_settings[self.application.host_name] - - workfile_builder_settings = host_settings.get("workfile_builder") - if not workfile_builder_settings: - # TODO remove warning when deprecated - self.log.warning(( - "Seems like old version of settings is used." - " Can't access custom templates in host \"{}\"." - ).format(self.application.full_label)) - return - - if not workfile_builder_settings["create_first_version"]: - self.log.info(( - "Project \"{}\" has turned off to create first workfile for" - " application \"{}\"" - ).format(project_name, self.application.full_label)) - return - - # Backwards compatibility - template_profiles = workfile_builder_settings.get("custom_templates") - if not template_profiles: - self.log.info( - "Custom templates are not filled. Skipping template copy." - ) - return project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") if project_doc and asset_doc: self.log.debug("Started filtering of custom template paths.") - template_path = get_custom_workfile_template_by_context( - template_profiles, project_doc, asset_doc, task_name, anatomy + template_path = get_custom_workfile_template( + project_doc, + asset_doc, + task_name, + host_name, + anatomy, + project_settings ) else: @@ -96,10 +77,13 @@ class CopyTemplateWorkfile(PreLaunchHook): "Global data collection probably did not execute." " Using backup solution." )) - dbcon = self.data.get("dbcon") template_path = get_custom_workfile_template_by_string_context( - template_profiles, project_name, asset_name, task_name, - dbcon, anatomy + project_name, + asset_name, + task_name, + host_name, + anatomy, + project_settings ) if not template_path: diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cf659344f0..a5f2631a02 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -38,7 +38,10 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) -from openpype.pipeline.context_tools import get_current_project_asset +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_custom_workfile_template_from_session +) from openpype.pipeline.workfile import BuildWorkfile from . import gizmo_menu @@ -2444,15 +2447,12 @@ def _launch_workfile_app(): def process_workfile_builder(): - from openpype.lib import ( - env_value_to_bool, - get_custom_workfile_template - ) # to avoid looping of the callback, remove it! nuke.removeOnCreate(process_workfile_builder, nodeClass="Root") # get state from settings - workfile_builder = get_current_project_settings()["nuke"].get( + project_settings = get_current_project_settings() + workfile_builder = project_settings["nuke"].get( "workfile_builder", {}) # get all imortant settings @@ -2462,7 +2462,6 @@ def process_workfile_builder(): # get settings createfv_on = workfile_builder.get("create_first_version") or None - custom_templates = workfile_builder.get("custom_templates") or None builder_on = workfile_builder.get("builder_on_start") or None last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") @@ -2470,8 +2469,8 @@ def process_workfile_builder(): # generate first version in file not existing and feature is enabled if createfv_on and not os.path.exists(last_workfile_path): # get custom template path if any - custom_template_path = get_custom_workfile_template( - custom_templates + custom_template_path = get_custom_workfile_template_from_session( + project_settings=project_settings ) # if custom template is defined From 27a62892a02ea1a7f15c4c0bbea13988e80f44d3 Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 9 Aug 2022 16:43:24 +0200 Subject: [PATCH 384/432] Kitsu|Fix: Movie project type fails & first loop children names Fix #3635 --- openpype/modules/kitsu/utils/update_op_with_zou.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 8f5566e8ec..e03cf2b30e 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -219,18 +219,23 @@ def update_op_assets( # Add parents for hierarchy item_data["parents"] = [] - while parent_zou_id is not None: - parent_doc = asset_doc_ids[parent_zou_id] + ancestor_id = parent_zou_id + while ancestor_id is not None: + parent_doc = asset_doc_ids[ancestor_id] item_data["parents"].insert(0, parent_doc["name"]) # Get parent entity parent_entity = parent_doc["data"]["zou"] - parent_zou_id = parent_entity.get("parent_id") + ancestor_id = parent_entity.get("parent_id") - if item_type in ["Shot", "Sequence"]: + # Build OpenPype compatible name + if item_type in ["Shot", "Sequence"] and parent_zou_id is not None: # Name with parents hierarchy "({episode}_){sequence}_{shot}" # to avoid duplicate name issue item_name = f"{item_data['parents'][-1]}_{item['name']}" + + # Update doc name + asset_doc_ids[item["id"]]["name"] = item_name else: item_name = item["name"] From 6ef14510e161f01713150f383b172f8d4239aa07 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:00:23 +0200 Subject: [PATCH 385/432] implemented method to stop timer using web server --- .../modules/timers_manager/timers_manager.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 3453e4bc4c..28702510f6 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -415,6 +415,36 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): return requests.post(rest_api_url, json=data) + @staticmethod + def stop_timer_with_webserver(logger=None): + """Prepared method for calling stop timers on REST api. + + Args: + logger (logging.Logger): Logger used for logging messages. + """ + + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + if not webserver_url: + msg = "Couldn't find webserver url" + if logger is not None: + logger.warning(msg) + else: + print(msg) + return + + rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) + try: + import requests + except Exception: + msg = "Couldn't start timer ('requests' is not available)" + if logger is not None: + logger.warning(msg) + else: + print(msg) + return + + return requests.post(rest_api_url) + def on_host_install(self, host, host_name, project_name): self.log.debug("Installing task changed callback") register_event_callback("taskChanged", self._on_host_task_change) From 29239178cba6cb3b5e6462771f301b5c104cae75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:00:41 +0200 Subject: [PATCH 386/432] timers manager is adding plugin paths --- .../modules/timers_manager/timers_manager.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 28702510f6..bfd450ce8c 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -6,7 +6,8 @@ from openpype.client import get_asset_by_name from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayService, - ILaunchHookPaths + ILaunchHookPaths, + IPluginPaths ) from openpype.lib.events import register_event_callback @@ -72,7 +73,12 @@ class ExampleTimersManagerConnector: self._timers_manager_module.timer_stopped(self._module.id) -class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): +class TimersManager( + OpenPypeModule, + ITrayService, + ILaunchHookPaths, + IPluginPaths +): """ Handles about Timers. Should be able to start/stop all timers at once. @@ -177,11 +183,21 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): def get_launch_hook_paths(self): """Implementation of `ILaunchHookPaths`.""" + return os.path.join( os.path.dirname(os.path.abspath(__file__)), "launch_hooks" ) + def get_plugin_paths(self): + """Implementation of `IPluginPaths`.""" + + timer_module_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(timer_module_dir, "plugins", "publish")] + } + @staticmethod def get_timer_data_for_context( project_name, asset_name, task_name, logger=None @@ -388,6 +404,7 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): logger (logging.Logger): Logger object. Using 'print' if not passed. """ + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") if not webserver_url: msg = "Couldn't find webserver url" From 70bcd6bf9062df6bb72948b02b3344c153f242fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:04:48 +0200 Subject: [PATCH 387/432] moved start and stop plugins into timers manager --- .../plugins/publish/start_timer.py | 39 +++++++++++++++++++ .../plugins/publish/stop_timer.py | 27 +++++++++++++ openpype/plugins/publish/start_timer.py | 14 ------- openpype/plugins/publish/stop_timer.py | 17 -------- 4 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 openpype/modules/timers_manager/plugins/publish/start_timer.py create mode 100644 openpype/modules/timers_manager/plugins/publish/stop_timer.py delete mode 100644 openpype/plugins/publish/start_timer.py delete mode 100644 openpype/plugins/publish/stop_timer.py diff --git a/openpype/modules/timers_manager/plugins/publish/start_timer.py b/openpype/modules/timers_manager/plugins/publish/start_timer.py new file mode 100644 index 0000000000..6408327ca1 --- /dev/null +++ b/openpype/modules/timers_manager/plugins/publish/start_timer.py @@ -0,0 +1,39 @@ +""" +Requires: + context -> system_settings + context -> openPypeModules +""" + +import pyblish.api + +from openpype.pipeline import legacy_io + + +class StartTimer(pyblish.api.ContextPlugin): + label = "Start Timer" + order = pyblish.api.IntegratorOrder + 1 + hosts = ["*"] + + def process(self, context): + timers_manager = context.data["openPypeModules"]["timers_manager"] + if not timers_manager.enabled: + self.log.debug("TimersManager is disabled") + return + + modules_settings = context.data["system_settings"]["modules"] + if not modules_settings["timers_manager"]["disregard_publishing"]: + self.log.debug("Publish is not affecting running timers.") + return + + project_name = legacy_io.active_project() + asset_name = legacy_io.Session.get("AVALON_ASSET") + task_name = legacy_io.Session.get("AVALON_TASK") + if not project_name or not asset_name or not task_name: + self.log.info(( + "Current context does not contain all" + " required information to start a timer." + )) + return + timers_manager.start_timer_with_webserver( + project_name, asset_name, task_name, self.log + ) diff --git a/openpype/modules/timers_manager/plugins/publish/stop_timer.py b/openpype/modules/timers_manager/plugins/publish/stop_timer.py new file mode 100644 index 0000000000..a8674ff2ca --- /dev/null +++ b/openpype/modules/timers_manager/plugins/publish/stop_timer.py @@ -0,0 +1,27 @@ +""" +Requires: + context -> system_settings + context -> openPypeModules +""" + + +import pyblish.api + + +class StopTimer(pyblish.api.ContextPlugin): + label = "Stop Timer" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["*"] + + def process(self, context): + timers_manager = context.data["openPypeModules"]["timers_manager"] + if not timers_manager.enabled: + self.log.debug("TimersManager is disabled") + return + + modules_settings = context.data["system_settings"]["modules"] + if not modules_settings["timers_manager"]["disregard_publishing"]: + self.log.debug("Publish is not affecting running timers.") + return + + timers_manager.stop_timer_with_webserver(self.log) diff --git a/openpype/plugins/publish/start_timer.py b/openpype/plugins/publish/start_timer.py deleted file mode 100644 index 112d92bef0..0000000000 --- a/openpype/plugins/publish/start_timer.py +++ /dev/null @@ -1,14 +0,0 @@ -import pyblish.api - -from openpype.lib import change_timer_to_current_context - - -class StartTimer(pyblish.api.ContextPlugin): - label = "Start Timer" - order = pyblish.api.IntegratorOrder + 1 - hosts = ["*"] - - def process(self, context): - modules_settings = context.data["system_settings"]["modules"] - if modules_settings["timers_manager"]["disregard_publishing"]: - change_timer_to_current_context() diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py deleted file mode 100644 index 414e43a3c4..0000000000 --- a/openpype/plugins/publish/stop_timer.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -import requests - -import pyblish.api - - -class StopTimer(pyblish.api.ContextPlugin): - label = "Stop Timer" - order = pyblish.api.ExtractorOrder - 0.49 - hosts = ["*"] - - def process(self, context): - modules_settings = context.data["system_settings"]["modules"] - if modules_settings["timers_manager"]["disregard_publishing"]: - webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") - rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) - requests.post(rest_api_url) From 51f58340617a225d872f7a99aea8e75b514a0f87 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:05:17 +0200 Subject: [PATCH 388/432] changed order of collect modules manager --- openpype/plugins/publish/collect_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_modules.py b/openpype/plugins/publish/collect_modules.py index 2f6cb1ef0e..d76096bcd9 100644 --- a/openpype/plugins/publish/collect_modules.py +++ b/openpype/plugins/publish/collect_modules.py @@ -7,7 +7,7 @@ import pyblish.api class CollectModules(pyblish.api.ContextPlugin): """Collect OpenPype modules.""" - order = pyblish.api.CollectorOrder - 0.45 + order = pyblish.api.CollectorOrder - 0.5 label = "OpenPype Modules" def process(self, context): From e35fd6e476dd3fb1cab539b1e39aaa1704ef62b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:19:01 +0200 Subject: [PATCH 389/432] use constant to define timer module dir --- openpype/modules/timers_manager/timers_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index bfd450ce8c..93332ace4f 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -13,6 +13,8 @@ from openpype.lib.events import register_event_callback from .exceptions import InvalidContextError +TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + class ExampleTimersManagerConnector: """Timers manager can handle timers of multiple modules/addons. @@ -34,6 +36,7 @@ class ExampleTimersManagerConnector: } ``` """ + # Not needed at all def __init__(self, module): # Store timer manager module to be able call it's methods when needed @@ -185,17 +188,15 @@ class TimersManager( """Implementation of `ILaunchHookPaths`.""" return os.path.join( - os.path.dirname(os.path.abspath(__file__)), + TIMER_MODULE_DIR, "launch_hooks" ) def get_plugin_paths(self): """Implementation of `IPluginPaths`.""" - timer_module_dir = os.path.dirname(os.path.abspath(__file__)) - return { - "publish": [os.path.join(timer_module_dir, "plugins", "publish")] + "publish": [os.path.join(TIMER_MODULE_DIR, "plugins", "publish")] } @staticmethod From 77d78aadf979632938cae81f94468f919490cdc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:19:38 +0200 Subject: [PATCH 390/432] mark 'change_timer_to_current_context' in 'openpype.lib' as deprecated --- openpype/lib/avalon_context.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 42854f39d6..eb98ec1d9c 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1515,13 +1515,21 @@ def get_creator_by_name(creator_name, case_sensitive=False): return None -@with_pipeline_io +@deprecated def change_timer_to_current_context(): """Called after context change to change timers. + Deprecated: + This method is specific for TimersManager module so please use the + functionality from there. Function will be removed after release + version 3.14.* + TODO: - use TimersManager's static method instead of reimplementing it here """ + + from openpype.pipeline import legacy_io + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") if not webserver_url: log.warning("Couldn't find webserver url") From 1c133cf6b126cf4f4a0277ddd455c75455dc93b1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 Aug 2022 17:46:58 +0100 Subject: [PATCH 391/432] FIx to use project name instead of code in update for ReferenceLoader --- openpype/hosts/maya/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 2b0c6131b4..8c3f6f071a 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -235,7 +235,7 @@ class ReferenceLoader(Loader): path = self.prepare_root_value(path, representation["context"] ["project"] - ["code"]) + ["name"]) content = cmds.file(path, loadReference=reference_node, type=file_type, From 4bb98863bd5476794faeb28fb37b9c77cc837dfe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Aug 2022 18:47:28 +0200 Subject: [PATCH 392/432] add all keys from anatomy data to representation context even if it's already there --- openpype/plugins/publish/integrate_hero_version.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 735b7e50fa..7d698ff98d 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -313,13 +313,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): } repre_context = template_filled.used_values for key in self.db_representation_context_keys: - if ( - key in repre_context or - key not in anatomy_data - ): - continue - - repre_context[key] = anatomy_data[key] + value = anatomy_data.get(key) + if value is not None: + repre_context[key] = value # Prepare new repre repre = copy.deepcopy(repre_info["representation"]) From eb0e014beaac279ef019fa13c8213c3ff2196754 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 Aug 2022 18:35:32 +0100 Subject: [PATCH 393/432] Fix call to load file in case of fbx file --- openpype/hosts/maya/api/plugin.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 8c3f6f071a..652874997c 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -236,10 +236,16 @@ class ReferenceLoader(Loader): representation["context"] ["project"] ["name"]) + + params = { + "loadReference": reference_node, + "returnNewNodes": True + } + if file_type != "fbx": + params["type"] = file_type + content = cmds.file(path, - loadReference=reference_node, - type=file_type, - returnNewNodes=True) + **params) except RuntimeError as exc: # When changing a reference to a file that has load errors the # command will raise an error even if the file is still loaded From 6c10d4412320867ff40422196b562db2ca128ca5 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 10 Aug 2022 03:43:25 +0000 Subject: [PATCH 394/432] [Automated] Bump version --- CHANGELOG.md | 3 +-- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3124201758..b7ef795f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.2...3.13.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.13.0-nightly.1...3.13.0) **🆕 New features** @@ -89,7 +89,6 @@ - General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) - Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) -- NewPublisher: Publish attributes are properly collected [\#3510](https://github.com/pypeclub/OpenPype/pull/3510) - TrayPublisher: Make sure host name is filled [\#3504](https://github.com/pypeclub/OpenPype/pull/3504) - NewPublisher: Groups work and enum multivalue [\#3501](https://github.com/pypeclub/OpenPype/pull/3501) diff --git a/openpype/version.py b/openpype/version.py index d2eb3a8ab6..c41e69d00d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.0" +__version__ = "3.13.1-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 03922a8e67..994c83d369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.0" # OpenPype +version = "3.13.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4d477592492407e806e636175b72dd06ed7a42c1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Aug 2022 11:29:46 +0100 Subject: [PATCH 395/432] Fixed with the right type parameter for FBX --- openpype/hosts/maya/api/plugin.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 652874997c..e50ebfccad 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -209,7 +209,7 @@ class ReferenceLoader(Loader): "ma": "mayaAscii", "mb": "mayaBinary", "abc": "Alembic", - "fbx": "fbx" + "fbx": "FBX" }.get(representation["name"]) assert file_type, "Unsupported representation: %s" % representation @@ -236,16 +236,10 @@ class ReferenceLoader(Loader): representation["context"] ["project"] ["name"]) - - params = { - "loadReference": reference_node, - "returnNewNodes": True - } - if file_type != "fbx": - params["type"] = file_type - content = cmds.file(path, - **params) + loadReference=reference_node, + type=file_type, + returnNewNodes=True) except RuntimeError as exc: # When changing a reference to a file that has load errors the # command will raise an error even if the file is still loaded From f03e63502e80dc7d3a8717db54e22132d0276bdc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 13:59:26 +0200 Subject: [PATCH 396/432] fixed dotted extensions --- openpype/pipeline/workfile/path_resolving.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 6740b710f5..aa75d29372 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -216,11 +216,13 @@ def get_last_workfile_with_version( if not os.path.exists(workdir): return None, None - dotted_extensions = { - ".{}".format(ext) - for ext in extensions - if not ext.startswith(".") - } + + dotted_extensions = set() + for ext in extensions: + if not ext.startswith("."): + ext = ".{}".format(ext) + dotted_extensions.add(ext) + # Fast match on extension filenames = [ filename From 8858377019184f17ddf00b8bd7d3a1e8f06f0e8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 14:32:07 +0200 Subject: [PATCH 397/432] formatting changes --- openpype/pipeline/workfile/path_resolving.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index aa75d29372..ed1d1d793e 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -216,7 +216,6 @@ def get_last_workfile_with_version( if not os.path.exists(workdir): return None, None - dotted_extensions = set() for ext in extensions: if not ext.startswith("."): @@ -227,7 +226,7 @@ def get_last_workfile_with_version( filenames = [ filename for filename in os.listdir(workdir) - if os.path.splitext(filename)[1] in dotted_extensions + if os.path.splitext(filename)[-1] in dotted_extensions ] # Build template without optionals, version to digits only regex From 0528494d9e53368275754befa73bea7dcf7948dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 16:10:52 +0200 Subject: [PATCH 398/432] extract review can scale to match pixel ratio --- openpype/plugins/publish/extract_review.py | 63 ++++++++-------------- 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 7442d3aacb..e16f324e0a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1210,7 +1210,6 @@ class ExtractReview(pyblish.api.InstancePlugin): # Get instance data pixel_aspect = temp_data["pixel_aspect"] - if reformat_in_baking: self.log.debug(( "Using resolution from input. It is already " @@ -1230,6 +1229,10 @@ class ExtractReview(pyblish.api.InstancePlugin): # - settings value can't have None but has value of 0 output_width = output_def.get("width") or output_width or None output_height = output_def.get("height") or output_height or None + # Force to use input resolution if output resolution was not defined + # in settings. Resolution from instance is not used when + # 'use_input_res' is set to 'True'. + use_input_res = False # Overscal color overscan_color_value = "black" @@ -1241,6 +1244,17 @@ class ExtractReview(pyblish.api.InstancePlugin): ) self.log.debug("Overscan color: `{}`".format(overscan_color_value)) + # Scale input to have proper pixel aspect ratio + # - scale width by the pixel aspect ratio + scale_pixel_aspect = output_def.get("scale_pixel_aspect", True) + if scale_pixel_aspect and pixel_aspect != 1: + # Change input width after pixel aspect + input_width = int(input_width * pixel_aspect) + use_input_res = True + filters.append(( + "scale={}x{}:flags=lanczos".format(input_width, input_height) + )) + # Convert overscan value video filters overscan_crop = output_def.get("overscan_crop") overscan = OverscanCrop( @@ -1251,13 +1265,10 @@ class ExtractReview(pyblish.api.InstancePlugin): # resolution by it's values if overscan_crop_filters: filters.extend(overscan_crop_filters) + # Change input resolution after overscan crop input_width = overscan.width() input_height = overscan.height() - # Use output resolution as inputs after cropping to skip usage of - # instance data resolution - if output_width is None or output_height is None: - output_width = input_width - output_height = input_height + use_input_res = True # Make sure input width and height is not an odd number input_width_is_odd = bool(input_width % 2 != 0) @@ -1283,8 +1294,10 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("input_width: `{}`".format(input_width)) self.log.debug("input_height: `{}`".format(input_height)) - # Use instance resolution if output definition has not set it. - if output_width is None or output_height is None: + # Use instance resolution if output definition has not set it + # - use instance resolution only if there were not scale changes + # that may massivelly affect output 'use_input_res' + if not use_input_res and output_width is None or output_height is None: output_width = temp_data["resolution_width"] output_height = temp_data["resolution_height"] @@ -1326,7 +1339,6 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width == input_width and output_height == input_height and not letter_box_enabled - and pixel_aspect == 1 ): self.log.debug( "Output resolution is same as input's" @@ -1336,39 +1348,8 @@ class ExtractReview(pyblish.api.InstancePlugin): new_repre["resolutionHeight"] = input_height return filters - # defining image ratios - input_res_ratio = ( - (float(input_width) * pixel_aspect) / input_height - ) - output_res_ratio = float(output_width) / float(output_height) - self.log.debug("input_res_ratio: `{}`".format(input_res_ratio)) - self.log.debug("output_res_ratio: `{}`".format(output_res_ratio)) - - # Round ratios to 2 decimal places for comparing - input_res_ratio = round(input_res_ratio, 2) - output_res_ratio = round(output_res_ratio, 2) - - # get scale factor - scale_factor_by_width = ( - float(output_width) / (input_width * pixel_aspect) - ) - scale_factor_by_height = ( - float(output_height) / input_height - ) - - self.log.debug( - "scale_factor_by_with: `{}`".format(scale_factor_by_width) - ) - self.log.debug( - "scale_factor_by_height: `{}`".format(scale_factor_by_height) - ) - # scaling none square pixels and 1920 width - if ( - input_height != output_height - or input_width != output_width - or pixel_aspect != 1 - ): + if input_height != output_height or input_width != output_width: filters.extend([ ( "scale={}x{}" From 3d62093224be2b3786823b175f1bfd1ffa3aad3d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Aug 2022 16:14:50 +0200 Subject: [PATCH 399/432] Refactor moved usage of CreateRender settings --- openpype/hosts/maya/api/lib_rendersettings.py | 3 +-- .../hosts/maya/plugins/publish/validate_render_image_rule.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 9aea55a03b..7cd2193086 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -60,8 +60,7 @@ class RenderSettings(object): try: aov_separator = self._aov_chars[( self._project_settings["maya"] - ["create"] - ["CreateRender"] + ["RenderSettings"] ["aov_separator"] )] except KeyError: diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 642ca9e25d..0abcf2f12a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -41,6 +41,5 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def get_default_render_image_folder(instance): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('create') \ - .get('CreateRender') \ + .get('RenderSettings') \ .get('default_render_image_folder') From 7a16cb723b8329d493697c153881533808a2c0e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 16:42:34 +0200 Subject: [PATCH 400/432] added settings for rescaling when pixel aspect ratio is not 1 --- openpype/settings/defaults/project_settings/global.json | 1 + .../projects_schema/schemas/schema_global_publish.json | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index e509db2791..0ff9363ba7 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -85,6 +85,7 @@ ], "width": 0, "height": 0, + "scale_pixel_aspect": true, "bg_color": [ 0, 0, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index b9d0b7daba..e1aa230b49 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -319,6 +319,15 @@ "minimum": 0, "maximum": 100000 }, + { + "type": "label", + "label": "Rescale input when it's pixel aspect ratio is not 1. Usefull for anamorph reviews." + }, + { + "key": "scale_pixel_aspect", + "label": "Scale pixel aspect", + "type": "boolean" + }, { "type": "label", "label": "Background color is used only when input have transparency and Alpha is higher than 0." From f74101be342ced01df8057f353123692fb559ff3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Aug 2022 16:45:19 +0200 Subject: [PATCH 401/432] Remove unused get current renderer logic The `renderer` variable wasn't used --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e6fc8a01e5..e1f4efcc07 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -154,12 +154,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): layer_name = "rs_{}".format(expected_layer_name) # collect all frames we are expecting to be rendered - renderer = self.get_render_attribute("currentRenderer", - layer=layer_name) - # handle various renderman names - if renderer.startswith("renderman"): - renderer = "renderman" - # return all expected files for all cameras and aovs in given # frame range layer_render_products = get_layer_render_products(layer_name) From 74a91f4d22ebcacbab07f05ca44fd8e1dbf1d6c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Aug 2022 17:01:42 +0200 Subject: [PATCH 402/432] Fix more missing refactors --- openpype/hosts/maya/plugins/publish/collect_render.py | 3 +-- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e6fc8a01e5..085403bdf7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -203,8 +203,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): aov_dict = {} default_render_file = context.data.get('project_settings')\ .get('maya')\ - .get('create')\ - .get('CreateRender')\ + .get('RenderSettings')\ .get('default_render_image_folder') or "" # replace relative paths with absolute. Render products are # returned as list of dictionaries. diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index f253ceb21a..13dfc0183a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -413,8 +413,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # Gather needed data ------------------------------------------------ default_render_file = instance.context.data.get('project_settings')\ .get('maya')\ - .get('create')\ - .get('CreateRender')\ + .get('RenderSettings')\ .get('default_render_image_folder') filename = os.path.basename(filepath) comment = context.data.get("comment", "") From bbf113cac4c8dd0dde7cca18646641107a505b44 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 11:54:07 +0200 Subject: [PATCH 403/432] Set default value for default render image folder to "renders" --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ac0f161cf2..ce9cd4d606 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -33,7 +33,7 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "", + "default_render_image_folder": "renders", "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { From f0a6a6414ea86178f0d02ed83d8816919a86beb1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 11:54:35 +0200 Subject: [PATCH 404/432] Tweak ValidateRenderImageRule docstring and invalidation error message --- .../publish/validate_render_image_rule.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 0abcf2f12a..a9be996e0c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -11,7 +11,11 @@ def get_file_rule(rule): class ValidateRenderImageRule(pyblish.api.InstancePlugin): - """Validates "images" file rule is set to "renders/" + """Validates Maya Workpace "images" file rule matches project settings. + + This validates against the configured default render image folder: + Studio Settings > Project > Maya > + Render Settings > Default render image folder. """ @@ -23,11 +27,13 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): - default_render_file = self.get_default_render_image_folder(instance) + required_images_rule = self.get_default_render_image_folder(instance) + current_images_rule = get_file_rule("images") - assert get_file_rule("images") == default_render_file, ( - "Workspace's `images` file rule must be set to: {}".format( - default_render_file + assert current_images_rule == required_images_rule, ( + "Invalid workspace `images` file rule value: '{}'. " + "Must be set to: '{}'".format( + current_images_rule, required_images_rule ) ) From 8fe20486a91a8943b847b610d342df163dee3e1b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 12:51:01 +0200 Subject: [PATCH 405/432] Remove usage of mel eval and pymel --- .../plugins/publish/validate_render_image_rule.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index a9be996e0c..b94bdb0b14 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,15 +1,9 @@ -import maya.mel as mel -import pymel.core as pm +from maya import cmds import pyblish.api import openpype.api -def get_file_rule(rule): - """Workaround for a bug in python with cmds.workspace""" - return mel.eval('workspace -query -fileRuleEntry "{}"'.format(rule)) - - class ValidateRenderImageRule(pyblish.api.InstancePlugin): """Validates Maya Workpace "images" file rule matches project settings. @@ -28,7 +22,7 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): required_images_rule = self.get_default_render_image_folder(instance) - current_images_rule = get_file_rule("images") + current_images_rule = cmds.workspace(fileRuleEntry="images") assert current_images_rule == required_images_rule, ( "Invalid workspace `images` file rule value: '{}'. " @@ -40,8 +34,8 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): default = cls.get_default_render_image_folder(instance) - pm.workspace.fileRules["images"] = default - pm.system.Workspace.save() + cmds.workspace(fileRule=("images", default)) + cmds.workspace(saveWorkspace=True) @staticmethod def get_default_render_image_folder(instance): From 7cd47ff6c4641d7e78c8d3e9823f4d58fdea1135 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 12:54:33 +0200 Subject: [PATCH 406/432] Only update and save the workspace once This avoids saving it many times on repair in scenes with many renderlayers and thus many renderlayer instances since repair runs per instance. --- .../maya/plugins/publish/validate_render_image_rule.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index b94bdb0b14..4d3796e429 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -33,9 +33,13 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - default = cls.get_default_render_image_folder(instance) - cmds.workspace(fileRule=("images", default)) - cmds.workspace(saveWorkspace=True) + + required_images_rule = cls.get_default_render_image_folder(instance) + current_images_rule = cmds.workspace(fileRuleEntry="images") + + if current_images_rule != required_images_rule: + cmds.workspace(fileRule=("images", required_images_rule)) + cmds.workspace(saveWorkspace=True) @staticmethod def get_default_render_image_folder(instance): From 7f48af4bdcf524ae91447038b658a60aa256f80e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 00:11:52 +0800 Subject: [PATCH 407/432] Collect full_exp_files instead of leaving it empty --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e6fc8a01e5..26ad0818e0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -219,6 +219,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) aov_dict[aov_first_key] = full_paths + full_exp_files = [aov_dict] frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) From 782a393a20ba61538fcacd181d0f1a7f47bc798b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 00:16:41 +0800 Subject: [PATCH 408/432] Collect full_exp_files instead of leaving it empty --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 26ad0818e0..e132cffe53 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -199,7 +199,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): ) # append full path - full_exp_files = [] aov_dict = {} default_render_file = context.data.get('project_settings')\ .get('maya')\ From d9e3815878b3868b18478b9dea2328d140bf2d92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Aug 2022 12:23:06 +0200 Subject: [PATCH 409/432] Refactored content of help, eg error message --- openpype/plugins/publish/help/validate_containers.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/help/validate_containers.xml b/openpype/plugins/publish/help/validate_containers.xml index e540c3c7a9..8424ee919c 100644 --- a/openpype/plugins/publish/help/validate_containers.xml +++ b/openpype/plugins/publish/help/validate_containers.xml @@ -3,9 +3,9 @@ Not up-to-date assets -## Obsolete containers found +## Outdated containers found -Scene contains one or more obsolete loaded containers, eg. items loaded into scene by Loader. +Scene contains one or more outdated loaded containers, eg. versions of items loaded into scene by Loader are not latest. ### How to repair? @@ -17,8 +17,7 @@ Use 'Scene Inventory' and update all highlighted old container to latest OR ### __Detailed Info__ (optional) -This validator protects you from rendering obsolete content, someone modified some referenced asset in this scene, eg. - by skipping this you would ignore changes to that asset. +This validates whether you're working with the latest versions of published content loaded into your scene. This protects you from using outdated versions of an asset. \ No newline at end of file From ec157e0a2a3a04aa18caf3135846ff3ad29486aa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 18:34:12 +0800 Subject: [PATCH 410/432] fix the bug of failing to extract look when UDIM format used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 80d82a4f58..bf7f5bc757 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -436,6 +436,16 @@ class ExtractLook(openpype.api.Extractor): # set color space to raw if we linearized it color_space = "Raw" else: + + # if the files are unresolved from `source` + # assume color space from the first file of + # the resource + first_file = next(iter(resource.get("files", [])), None) + if not first_file: + # No files for this resource? Can this happen? Should this error? + continue + + filepath = os.path.normpath(first_file) # if the files are unresolved if files_metadata[filepath]["color_space"] == "Raw": # set color space to raw if we linearized it From 82c4f19979ea7055cb742c3321a0bcd9b2d5a73d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 18:36:05 +0800 Subject: [PATCH 411/432] fix the bug of failing to extract look when UDIM format used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index bf7f5bc757..8e09a564d0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -436,15 +436,12 @@ class ExtractLook(openpype.api.Extractor): # set color space to raw if we linearized it color_space = "Raw" else: - # if the files are unresolved from `source` # assume color space from the first file of # the resource first_file = next(iter(resource.get("files", [])), None) if not first_file: - # No files for this resource? Can this happen? Should this error? continue - filepath = os.path.normpath(first_file) # if the files are unresolved if files_metadata[filepath]["color_space"] == "Raw": From 7526d4cfa5252b646469c79db782b1b4a04373ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Aug 2022 13:34:37 +0200 Subject: [PATCH 412/432] Update openpype/plugins/publish/help/validate_containers.xml Co-authored-by: Roy Nieterau --- openpype/plugins/publish/help/validate_containers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/help/validate_containers.xml b/openpype/plugins/publish/help/validate_containers.xml index 8424ee919c..5d18bb4c19 100644 --- a/openpype/plugins/publish/help/validate_containers.xml +++ b/openpype/plugins/publish/help/validate_containers.xml @@ -5,7 +5,7 @@ ## Outdated containers found -Scene contains one or more outdated loaded containers, eg. versions of items loaded into scene by Loader are not latest. +Scene contains one or more outdated loaded containers, eg. versions loaded into scene by Loader are not latest. ### How to repair? From 2cf01d8605e2588ce437579b55d409cf2027b452 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Aug 2022 13:44:13 +0200 Subject: [PATCH 413/432] Fix Scene Inventory select actions --- openpype/tools/sceneinventory/view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 63d181b2d6..e0e43aaba7 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -551,16 +551,16 @@ class SceneInventoryView(QtWidgets.QTreeView): "toggle": selection_model.Toggle, }[options.get("mode", "select")] - for item in iter_model_rows(model, 0): - item = item.data(InventoryModel.ItemRole) + for index in iter_model_rows(model, 0): + item = index.data(InventoryModel.ItemRole) if item.get("isGroupNode"): continue name = item.get("objectName") if name in object_names: - self.scrollTo(item) # Ensure item is visible + self.scrollTo(index) # Ensure item is visible flags = select_mode | selection_model.Rows - selection_model.select(item, flags) + selection_model.select(index, flags) object_names.remove(name) From dc73bbdb13044d077b5576cd33ebc7b51597a70c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 20:49:34 +0800 Subject: [PATCH 414/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- .../maya/plugins/publish/extract_look.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 8e09a564d0..991f44c74f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -430,22 +430,25 @@ class ExtractLook(openpype.api.Extractor): color_space = "Raw" else: # get all the resolved files in Maya File Path Editor - src = files_metadata.get(source) - if src: - if files_metadata[source]["color_space"] == "Raw": + metadata = files_metadata.get(source) + if metadata: + metadata = files_metadata[source] + if metadata["color_space"] == "Raw": # set color space to raw if we linearized it color_space = "Raw" else: # if the files are unresolved from `source` # assume color space from the first file of # the resource - first_file = next(iter(resource.get("files", [])), None) - if not first_file: - continue - filepath = os.path.normpath(first_file) - # if the files are unresolved - if files_metadata[filepath]["color_space"] == "Raw": - # set color space to raw if we linearized it + metadata = files_metadata.get(source) + if not metadata: + first_file = next(iter(resource.get("files", [])), None) + if not first_file: + continue + first_filepath = os.path.normpath(first_file) + metadata = files_metadata[first_filepath] + if metadata["color_space"] == "Raw": + # set color space to raw if we linearized it color_space = "Raw" # Remap file node filename to destination remap[color_space_attr] = color_space From fc65721838a90111c9137b45f062d1f51ad06c08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 20:52:47 +0800 Subject: [PATCH 415/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 991f44c74f..02957bb0ad 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -442,7 +442,8 @@ class ExtractLook(openpype.api.Extractor): # the resource metadata = files_metadata.get(source) if not metadata: - first_file = next(iter(resource.get("files", [])), None) + first_file = next(iter(resource.get( + "files", [])), None) if not first_file: continue first_filepath = os.path.normpath(first_file) From f5578cf664321d4c2488c2ac46dbb893f8822cf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 20:57:18 +0800 Subject: [PATCH 416/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 02957bb0ad..68d80de5b8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,7 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - # get all the resolved files in Maya File Path Editor + # get all the resolved files metadata = files_metadata.get(source) if metadata: metadata = files_metadata[source] From ffea3e85fee6058fd3fc38982d228c51f463645c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:34:30 +0800 Subject: [PATCH 417/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- .../maya/plugins/publish/extract_look.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 68d80de5b8..5ece5e2e1b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -431,24 +431,17 @@ class ExtractLook(openpype.api.Extractor): else: # get all the resolved files metadata = files_metadata.get(source) - if metadata: - metadata = files_metadata[source] - if metadata["color_space"] == "Raw": - # set color space to raw if we linearized it - color_space = "Raw" - else: - # if the files are unresolved from `source` - # assume color space from the first file of - # the resource - metadata = files_metadata.get(source) - if not metadata: - first_file = next(iter(resource.get( - "files", [])), None) - if not first_file: - continue + # if the files are unresolved from `source` + # assume color space from the first file of + # the resource + if not metadata: + first_file = next(iter(resource.get( + "files", [])), None) + if not first_file: + continue first_filepath = os.path.normpath(first_file) metadata = files_metadata[first_filepath] - if metadata["color_space"] == "Raw": + if metadata["color_space"] == "Raw": # set color space to raw if we linearized it color_space = "Raw" # Remap file node filename to destination From 9b01e6e0326b4750c043da207adc2b8495a8ebce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:36:40 +0800 Subject: [PATCH 418/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 5ece5e2e1b..63a695cecf 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -442,8 +442,8 @@ class ExtractLook(openpype.api.Extractor): first_filepath = os.path.normpath(first_file) metadata = files_metadata[first_filepath] if metadata["color_space"] == "Raw": - # set color space to raw if we linearized it - color_space = "Raw" + # set color space to raw if we linearized it + color_space = "Raw" # Remap file node filename to destination remap[color_space_attr] = color_space attr = resource["attribute"] From a9cee020b5f2044af533c06323c697162821624f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:38:45 +0800 Subject: [PATCH 419/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 63a695cecf..95f319a924 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -431,7 +431,7 @@ class ExtractLook(openpype.api.Extractor): else: # get all the resolved files metadata = files_metadata.get(source) - # if the files are unresolved from `source` + # if the files are unresolved from `source` # assume color space from the first file of # the resource if not metadata: From 85575e3a99f5618304fc41f5e73a117fe66abc0b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:40:40 +0800 Subject: [PATCH 420/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 95f319a924..c9e41503da 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,7 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - # get all the resolved files + # get all resolved files metadata = files_metadata.get(source) # if the files are unresolved from `source` # assume color space from the first file of From f9f275f6a0555c5e1250b6f2b19aa606ce2fb6e3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:47:08 +0800 Subject: [PATCH 421/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c9e41503da..93bfa8c913 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,7 +429,6 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: - # get all resolved files metadata = files_metadata.get(source) # if the files are unresolved from `source` # assume color space from the first file of From cd64ffb8f8a85b30edb4e7c01fb2d90d33bd77ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:51:45 +0800 Subject: [PATCH 422/432] fix the bug of failing to extract look when UDIM formats used in AiImage --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 93bfa8c913..8be0c7aae5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -429,9 +429,10 @@ class ExtractLook(openpype.api.Extractor): # node doesn't have color space attribute color_space = "Raw" else: + # get the resolved files metadata = files_metadata.get(source) - # if the files are unresolved from `source` - # assume color space from the first file of + # if the files are unresolved from `source` + # assume color space from the first file of # the resource if not metadata: first_file = next(iter(resource.get( From fd56f09c8423ea6438d6606c69dfa6c45ba9e8eb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 13 Aug 2022 03:48:01 +0000 Subject: [PATCH 423/432] [Automated] Bump version --- CHANGELOG.md | 25 +++++++++++++++++++------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ef795f0a..2adb4ac154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [3.13.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) + +**🐛 Bug fixes** + +- General: Hero version representations have full context [\#3638](https://github.com/pypeclub/OpenPype/pull/3638) +- Maya: FBX support for update in reference loader [\#3631](https://github.com/pypeclub/OpenPype/pull/3631) + +**🔀 Refactored code** + +- TimersManager: Plugins are in timers manager module [\#3639](https://github.com/pypeclub/OpenPype/pull/3639) +- General: Move workfiles functions into pipeline [\#3637](https://github.com/pypeclub/OpenPype/pull/3637) + +**Merged pull requests:** + +- Kitsu|Fix: Movie project type fails & first loop children names [\#3636](https://github.com/pypeclub/OpenPype/pull/3636) + ## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.13.0-nightly.1...3.13.0) @@ -37,6 +55,7 @@ - General: Update imports in start script [\#3579](https://github.com/pypeclub/OpenPype/pull/3579) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) - Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) +- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** @@ -68,13 +87,9 @@ - Maya: add additional validators to Settings [\#3540](https://github.com/pypeclub/OpenPype/pull/3540) - General: Interactive console in cli [\#3526](https://github.com/pypeclub/OpenPype/pull/3526) - Ftrack: Automatic daily review session creation can define trigger hour [\#3516](https://github.com/pypeclub/OpenPype/pull/3516) -- Ftrack: add source into Note [\#3509](https://github.com/pypeclub/OpenPype/pull/3509) -- Add pack and unpack convenience scripts [\#3502](https://github.com/pypeclub/OpenPype/pull/3502) -- NewPublisher: Keep plugins with mismatch target in report [\#3498](https://github.com/pypeclub/OpenPype/pull/3498) **🐛 Bug fixes** -- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Maya: Fix animated attributes \(ie. overscan\) on loaded cameras breaking review publishing. [\#3562](https://github.com/pypeclub/OpenPype/pull/3562) - NewPublisher: Python 2 compatible html escape [\#3559](https://github.com/pypeclub/OpenPype/pull/3559) - Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) @@ -89,8 +104,6 @@ - General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) - Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) -- TrayPublisher: Make sure host name is filled [\#3504](https://github.com/pypeclub/OpenPype/pull/3504) -- NewPublisher: Groups work and enum multivalue [\#3501](https://github.com/pypeclub/OpenPype/pull/3501) **🔀 Refactored code** diff --git a/openpype/version.py b/openpype/version.py index c41e69d00d..6ff5dfb7b5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.1-nightly.1" +__version__ = "3.13.1-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index 994c83d369..9cbdc295ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.1-nightly.1" # OpenPype +version = "3.13.1-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e6584a9b940782bb6927e807b6a19412a1fd2fe4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 12:08:20 +0200 Subject: [PATCH 424/432] removed pype 2 compatibility --- .../custom/plugins/GlobalJobPreLoad.py | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 172649c951..cd36e45921 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -260,52 +260,6 @@ def pype_command_line(executable, arguments, workingDirectory): return executable, arguments, workingDirectory -def pype(deadlinePlugin): - """Remaps `PYPE_METADATA_FILE` and `PYPE_PYTHON_EXE` environment vars. - - `PYPE_METADATA_FILE` is used on farm to point to rendered data. This path - originates on platform from which this job was published. To be able to - publish on different platform, this path needs to be remapped. - - `PYPE_PYTHON_EXE` can be used to specify custom location of python - interpreter to use for Pype. This is remappeda also if present even - though it probably doesn't make much sense. - - Arguments: - deadlinePlugin: Deadline job plugin passed by Deadline - - """ - print(">>> Getting job ...") - job = deadlinePlugin.GetJob() - # PYPE should be here, not OPENPYPE - backward compatibility!! - pype_metadata = job.GetJobEnvironmentKeyValue("PYPE_METADATA_FILE") - pype_python = job.GetJobEnvironmentKeyValue("PYPE_PYTHON_EXE") - print(">>> Having backward compatible env vars {}/{}".format(pype_metadata, - pype_python)) - # test if it is pype publish job. - if pype_metadata: - pype_metadata = RepositoryUtils.CheckPathMapping(pype_metadata) - if platform.system().lower() == "linux": - pype_metadata = pype_metadata.replace("\\", "/") - - print("- remapping PYPE_METADATA_FILE: {}".format(pype_metadata)) - job.SetJobEnvironmentKeyValue("PYPE_METADATA_FILE", pype_metadata) - deadlinePlugin.SetProcessEnvironmentVariable( - "PYPE_METADATA_FILE", pype_metadata) - - if pype_python: - pype_python = RepositoryUtils.CheckPathMapping(pype_python) - if platform.system().lower() == "linux": - pype_python = pype_python.replace("\\", "/") - - print("- remapping PYPE_PYTHON_EXE: {}".format(pype_python)) - job.SetJobEnvironmentKeyValue("PYPE_PYTHON_EXE", pype_python) - deadlinePlugin.SetProcessEnvironmentVariable( - "PYPE_PYTHON_EXE", pype_python) - - deadlinePlugin.ModifyCommandLineCallback += pype_command_line - - def __main__(deadlinePlugin): print("*** GlobalJobPreload start ...") print(">>> Getting job ...") @@ -329,5 +283,3 @@ def __main__(deadlinePlugin): inject_render_job_id(deadlinePlugin) elif openpype_render_job == '1' or openpype_remote_job == '1': inject_openpype_environment(deadlinePlugin) - else: - pype(deadlinePlugin) # backward compatibility with Pype2 From 919a6146c6c16d5f98caff3eb79792e876b2de49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 12:08:32 +0200 Subject: [PATCH 425/432] removed unused function --- .../custom/plugins/GlobalJobPreLoad.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index cd36e45921..98c727f618 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -234,32 +234,6 @@ def inject_render_job_id(deadlinePlugin): print(">>> Injection end.") -def pype_command_line(executable, arguments, workingDirectory): - """Remap paths in comand line argument string. - - Using Deadline rempper it will remap all path found in command-line. - - Args: - executable (str): path to executable - arguments (str): arguments passed to executable - workingDirectory (str): working directory path - - Returns: - Tuple(executable, arguments, workingDirectory) - - """ - print("-" * 40) - print("executable: {}".format(executable)) - print("arguments: {}".format(arguments)) - print("workingDirectory: {}".format(workingDirectory)) - print("-" * 40) - print("Remapping arguments ...") - arguments = RepositoryUtils.CheckPathMapping(arguments) - print("* {}".format(arguments)) - print("-" * 40) - return executable, arguments, workingDirectory - - def __main__(deadlinePlugin): print("*** GlobalJobPreload start ...") print(">>> Getting job ...") From 963b66eb5808249dd47ae5e6bd62a53972352655 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 12:15:35 +0200 Subject: [PATCH 426/432] fixed python 2 compatibility --- .../custom/plugins/GlobalJobPreLoad.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 98c727f618..61b95cf06d 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -34,7 +34,7 @@ def get_openpype_version_from_path(path, build=True): # if only builds are requested if build and not os.path.isfile(exe): # noqa: E501 - print(f" ! path is not a build: {path}") + print(" ! path is not a build: {}".format(path)) return None version = {} @@ -70,11 +70,12 @@ def inject_openpype_environment(deadlinePlugin): # lets go over all available and find compatible build. requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") if requested_version: - print((">>> Scanning for compatible requested " - f"version {requested_version}")) + print(( + ">>> Scanning for compatible requested version {}" + ).format(requested_version)) install_dir = DirectoryUtils.SearchDirectoryList(dir_list) if install_dir: - print(f"--- Looking for OpenPype at: {install_dir}") + print("--- Looking for OpenPype at: {}".format(install_dir)) sub_dirs = [ f.path for f in os.scandir(install_dir) if f.is_dir() @@ -83,18 +84,20 @@ def inject_openpype_environment(deadlinePlugin): version = get_openpype_version_from_path(subdir) if not version: continue - print(f" - found: {version} - {subdir}") + print(" - found: {} - {}".format(version, subdir)) openpype_versions.append((version, subdir)) exe = FileUtils.SearchFileList(exe_list) if openpype_versions: # if looking for requested compatible version, # add the implicitly specified to the list too. - print(f"Looking for OpenPype at: {os.path.dirname(exe)}") + print("Looking for OpenPype at: {}".format(os.path.dirname(exe))) version = get_openpype_version_from_path( os.path.dirname(exe)) if version: - print(f" - found: {version} - {os.path.dirname(exe)}") + print(" - found: {} - {}".format( + version, os.path.dirname(exe) + )) openpype_versions.append((version, os.path.dirname(exe))) if requested_version: @@ -106,8 +109,9 @@ def inject_openpype_environment(deadlinePlugin): int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", ver[0]) ]) - print(("*** Latest available version found is " - f"{openpype_versions[-1][0]}")) + print(( + "*** Latest available version found is {}" + ).format(openpype_versions[-1][0])) requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 compatible_versions = [] for version in openpype_versions: @@ -127,8 +131,9 @@ def inject_openpype_environment(deadlinePlugin): int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", ver[0]) ]) - print(("*** Latest compatible version found is " - f"{compatible_versions[-1][0]}")) + print(( + "*** Latest compatible version found is {}" + ).format(compatible_versions[-1][0])) # create list of executables for different platform and let # Deadline decide. exe_list = [ From 874d95270f35f305650a05649a6344379ccbe4e1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 16 Aug 2022 10:11:45 +0200 Subject: [PATCH 427/432] Fix logic --- openpype/tools/loader/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 48c038418a..bb943303bc 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -584,9 +584,9 @@ class SubsetWidget(QtWidgets.QWidget): for repre_doc in repre_docs: repre_ids.append(repre_doc["_id"]) + # keep only version ids without representation with that name version_id = repre_doc["parent"] - if version_id not in version_ids: - version_ids.remove(version_id) + version_ids.remove(version_id) for version_id in version_ids: joined_subset_names = ", ".join([ From 48706101137e544dc0ad66eb3f36e153a77ed699 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 16 Aug 2022 10:17:11 +0200 Subject: [PATCH 428/432] Report subsets without representation in one go --- openpype/tools/loader/widgets.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index bb943303bc..48a23e053a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -567,12 +567,12 @@ class SubsetWidget(QtWidgets.QWidget): # Trigger project_name = self.dbcon.active_project() - subset_names_by_version_id = collections.defaultdict(set) + subset_name_by_version_id = dict() for item in items: version_id = item["version_document"]["_id"] - subset_names_by_version_id[version_id].add(item["subset"]) + subset_name_by_version_id[version_id] = item["subset"] - version_ids = set(subset_names_by_version_id.keys()) + version_ids = set(subset_name_by_version_id.keys()) repre_docs = get_representations( project_name, representation_names=[representation_name], @@ -588,10 +588,11 @@ class SubsetWidget(QtWidgets.QWidget): version_id = repre_doc["parent"] version_ids.remove(version_id) - for version_id in version_ids: + if version_ids: + # report versions that didn't have valid representation joined_subset_names = ", ".join([ - '"{}"'.format(subset) - for subset in subset_names_by_version_id[version_id] + '"{}"'.format(subset_name_by_version_id[version_id]) + for version_id in version_ids ]) self.echo("Subsets {} don't have representation '{}'".format( joined_subset_names, representation_name From 345a476159ed6d0f684f89316a488bba52347d93 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 11:55:09 +0200 Subject: [PATCH 429/432] prepared settings to be able change task status on creation --- .../defaults/project_settings/ftrack.json | 3 ++ .../schema_project_ftrack.json | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..9847e58cfa 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -434,6 +434,9 @@ "enabled": false, "custom_attribute_keys": [] }, + "IntegrateHierarchyToFtrack": { + "create_task_status_profiles": [] + }, "IntegrateFtrackNote": { "enabled": true, "note_template": "{intent}: {comment}", 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 c06bec0f58..3f472c6c6a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -841,6 +841,44 @@ } ] }, + { + "type": "dict", + "key": "IntegrateHierarchyToFtrack", + "label": "Integrate Hierarchy to ftrack", + "is_group": true, + "collapsible": true, + "children": [ + { + "type": "label", + "label": "Set task status on new task creation. Ftrack's default status is used otherwise." + }, + { + "type": "list", + "key": "create_task_status_profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "text", + "key": "status_name", + "label": "Status name" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 63e6088391b59f575c5406d4183e041d4f38c724 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 16 Aug 2022 11:55:23 +0200 Subject: [PATCH 430/432] Refactor `.remove` to `.discard` to fix bug if version wasn't in version_ids --- openpype/tools/loader/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 48a23e053a..2d8b4b048d 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -586,7 +586,7 @@ class SubsetWidget(QtWidgets.QWidget): # keep only version ids without representation with that name version_id = repre_doc["parent"] - version_ids.remove(version_id) + version_ids.discard(version_id) if version_ids: # report versions that didn't have valid representation From 088a2d2003e111769084821ac1e2c2ece3cf2e35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 11:55:23 +0200 Subject: [PATCH 431/432] use task status profiles to change task status id on creation --- .../publish/integrate_hierarchy_ftrack.py | 62 ++++++++++++++----- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index b8855ee2bd..8d39baa8d7 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,9 +1,12 @@ import sys import collections import six -import pyblish.api from copy import deepcopy + +import pyblish.api + from openpype.client import get_asset_by_id +from openpype.lib import filter_profiles # Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` @@ -73,6 +76,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): "traypublisher" ] optional = False + create_task_status_profiles = [] def process(self, context): self.context = context @@ -82,14 +86,16 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): hierarchy_context = self._get_active_assets(context) self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) - self.session = self.context.data["ftrackSession"] + session = self.context.data["ftrackSession"] project_name = self.context.data["projectEntity"]["name"] query = 'Project where full_name is "{}"'.format(project_name) - project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] + project = session.query(query).one() + auto_sync_state = project["custom_attributes"][CUST_ATTR_AUTO_SYNC] - self.ft_project = None + self.session = session + self.ft_project = project + self.task_types = self.get_all_task_types(project) + self.task_statuses = self.get_task_statuses(project) # disable termporarily ftrack project's autosyncing if auto_sync_state: @@ -121,10 +127,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.log.debug(entity_type) if entity_type.lower() == 'project': - query = 'Project where full_name is "{}"'.format(entity_name) - entity = self.session.query(query).one() - self.ft_project = entity - self.task_types = self.get_all_task_types(entity) + entity = self.ft_project elif self.ft_project is None or parent is None: raise AssertionError( @@ -217,13 +220,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): task_type=task_type, parent=entity ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) # Incoming links. self.create_links(project_name, entity_data, entity) @@ -303,7 +299,37 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return tasks + def get_task_statuses(self, project_entity): + project_schema = project_entity["project_schema"] + task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + return { + status["id"]: status + for status in task_workflow_statuses + } + def create_task(self, name, task_type, parent): + filter_data = { + "task_names": name, + "task_types": task_type + } + profile = filter_profiles( + self.create_task_status_profiles, + filter_data + ) + status_id = None + if profile: + status_name = profile["status_name"] + status_name_low = status_name.lower() + for _status_id, status in self.task_statuses.items(): + if status["name"].lower() == status_name_low: + status_id = _status_id + break + + if status_id is None: + self.log.warning( + "Task status \"{}\" was not found".format(status_name) + ) + task = self.session.create('Task', { 'name': name, 'parent': parent @@ -312,6 +338,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.log.info(task_type) self.log.info(self.task_types) task['type'] = self.task_types[task_type] + if status_id is not None: + task["status_id"] = status_id try: self.session.commit() From b36b8ebee0ae424ab1189bc3d9629ef672097bb4 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 17 Aug 2022 04:12:09 +0000 Subject: [PATCH 432/432] [Automated] Bump version --- CHANGELOG.md | 19 ++++++++----------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adb4ac154..80673e9f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog -## [3.13.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.13.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) **🐛 Bug fixes** +- General: Extract Review can scale with pixel aspect ratio [\#3644](https://github.com/pypeclub/OpenPype/pull/3644) +- Maya: Refactor moved usage of CreateRender settings [\#3643](https://github.com/pypeclub/OpenPype/pull/3643) - General: Hero version representations have full context [\#3638](https://github.com/pypeclub/OpenPype/pull/3638) +- Nuke: color settings for render write node is working now [\#3632](https://github.com/pypeclub/OpenPype/pull/3632) - Maya: FBX support for update in reference loader [\#3631](https://github.com/pypeclub/OpenPype/pull/3631) **🔀 Refactored code** @@ -16,7 +19,10 @@ **Merged pull requests:** +- Deadline: Global job pre load is not Pype 2 compatible [\#3666](https://github.com/pypeclub/OpenPype/pull/3666) +- Maya: Remove unused get current renderer logic [\#3645](https://github.com/pypeclub/OpenPype/pull/3645) - Kitsu|Fix: Movie project type fails & first loop children names [\#3636](https://github.com/pypeclub/OpenPype/pull/3636) +- fix the bug of failing to extract look when UDIMs format used in AiImage [\#3628](https://github.com/pypeclub/OpenPype/pull/3628) ## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09) @@ -55,7 +61,6 @@ - General: Update imports in start script [\#3579](https://github.com/pypeclub/OpenPype/pull/3579) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) - Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) -- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** @@ -85,11 +90,10 @@ - General: Global thumbnail extractor is ready for more cases [\#3561](https://github.com/pypeclub/OpenPype/pull/3561) - Maya: add additional validators to Settings [\#3540](https://github.com/pypeclub/OpenPype/pull/3540) -- General: Interactive console in cli [\#3526](https://github.com/pypeclub/OpenPype/pull/3526) -- Ftrack: Automatic daily review session creation can define trigger hour [\#3516](https://github.com/pypeclub/OpenPype/pull/3516) **🐛 Bug fixes** +- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Maya: Fix animated attributes \(ie. overscan\) on loaded cameras breaking review publishing. [\#3562](https://github.com/pypeclub/OpenPype/pull/3562) - NewPublisher: Python 2 compatible html escape [\#3559](https://github.com/pypeclub/OpenPype/pull/3559) - Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) @@ -98,12 +102,6 @@ - Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) - Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) - General: Create workfile documents works again [\#3538](https://github.com/pypeclub/OpenPype/pull/3538) -- Additional fixes for powershell scripts [\#3525](https://github.com/pypeclub/OpenPype/pull/3525) -- Maya: Added wrapper around cmds.setAttr [\#3523](https://github.com/pypeclub/OpenPype/pull/3523) -- Nuke: double slate [\#3521](https://github.com/pypeclub/OpenPype/pull/3521) -- General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) -- Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) -- TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) **🔀 Refactored code** @@ -112,7 +110,6 @@ - Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) - General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) - General: Move load related functions into pipeline [\#3527](https://github.com/pypeclub/OpenPype/pull/3527) -- General: Get current context document functions [\#3522](https://github.com/pypeclub/OpenPype/pull/3522) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 6ff5dfb7b5..9ae52e8370 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.1-nightly.2" +__version__ = "3.13.1-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 9cbdc295ff..83ccf233d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.1-nightly.2" # OpenPype +version = "3.13.1-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License"

(Zfh%a(0ju?YwE!YFunz8QgZg!+ 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 TAO39h=#iqV>f2Ij<1haQx}=q) literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_openpype_tools_load.png b/website/docs/assets/unreal_openpype_tools_load.png new file mode 100644 index 0000000000000000000000000000000000000000..4909feac3b26adad018829cf4a26d07d9ab324aa GIT binary patch literal 27465 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/unreal_level_streaming_method_no_sequences.png b/website/docs/assets/unreal_level_streaming_method_no_sequences.png new file mode 100644 index 0000000000000000000000000000000000000000..77a2754ded51a563ad3426d13f65edff7c2ce83f GIT binary patch literal 82416 zcmX`SWmKD8*R>rSiWgek-6`%~v{-S6;>9gEgyIgxT|#jy4#i!AySuylo9lkQ@%_lo zIP)iCBzvzl=Um4QS5cBd|3LTw005xN$x3|(0AP^;02l#eMCd0jg{9iiHyD@iG7^AF z2=O8G0p3zvQ5*pH8;km4f&hI+ag^0@0RS+1{(Hd;I24-!0Pla~q{P)d3{SFuL!x#vU4B!`e}+McKtcFsqsvLZ#dyS6={d$9X`^3T`2&MKBI4~UyxeaW$Zt!5 z*YWer(kk!tY!eC0(T#sb9aW|Ft1WJ2YxCzTAI{sX%c|O+tPYF8F#<>bErc?@+$aD4 z)6pa~C-U@gUBIDvnt2M7@9Xrg#7$)1q?WW>RG;NO4e@qH==&GEv@eU7twZMCPKTKZ zCp&K6Hd0WYh+|UHlzlH&A;VxK85wy35=~nvr(k(r!|vy+b0!xfoq>^GM`aw_w&~?7 zQogk#qvDzZ14o%;zVQTvnL1}}`)3>iT=dT^C>1dR>3A)di~f`Mb=0&IYn*SYz#PBq z8aLtjF9IC-3jFh>(%^QH`;w+NoLho=;*$3EL;0>#P6w&?B43E4yp|xl_c}u4!4Db+ITe(6+R&$W+FFq zU}gFK(k~W^+$dn&Ts=}drf+vGPTwJAn^o`=UtXT{*JO8UfSt=CHLVAmN{nR{gyXRNz)Kl??O ze*TsaxX(l9jV|QH047`k<@txnc)(trwG&$5<`-G3C9ckpCTApgbyyygHh_eHG=McX z7UnkwF|sc};s%xk@C9TCfb$4unpvI~RS5~PNQMcf0@b@JCfN1#1b*y_nVFrBeZk*E z1wgDZjcr+tnk|}?oU9VC>x`Jic<<0KAiptYONa$2wZU=p3<*t^b2!u}|Z5`vQ3QC8_3{OeI6kaL-|2 zg)qgmW7!pnwad|jjQy-S$`clGNLr!I!Ll7|9zKU9TH-GaLU-PAJ)$8Zi6@p(U~6;5 z1sX|v|F?xd2i;{l*)N(t@jrzUs>tK{!|@o?gV;U70LWj%IBP6b7Up?- zoI#A_g6JS1vN5uShS`jfn;C;+0tg@#iVGw43j69e0$`qrF1|rWHr|w*;3RDJ4KyHa4?QptJnSYpG z@}X_Xs=7dOlOZ0GrJkKUAW|7u7kk6 z#TWw!5^SzGf!FFPzps)|g%4{0E6iC61kM!eMtg%=Xsa6wfI*m3;kNSSW%)z{kdKA5;-xae-dHaH1<&`T2% zDtuzX&Evl0zV16Ro%42cRg=*&{qo4mgY&B*?-*5PHBD1nD|4 zS`8twFR*)=&l%%;1}cLvWr=5t{j98tS3I`scb9A`YtY@-JY~|jT{LI^fQKzTK}(FP zIk&;{Ag+?(JP#HzR=%8*#NfzQQy&d^LohvlRZi3L2=XfYLj#9KV`TKKq#`X%&nA