From ed76a59d9d6fd691c8eaadd3e678952b583aa2a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 22:53:00 +0100 Subject: [PATCH 0001/1030] 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 0002/1030] 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 0003/1030] 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 0004/1030] 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 0005/1030] 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 0006/1030] 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 0007/1030] 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 0008/1030] 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 0009/1030] 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 0010/1030] 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 0011/1030] 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 0012/1030] 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 0013/1030] 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 0014/1030] 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 0015/1030] 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 0016/1030] 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 0017/1030] 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 0018/1030] 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 d88ed919e6a4a566b8ff8b289415c471de454b00 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 16 Mar 2022 22:09:23 +0100 Subject: [PATCH 0019/1030] First draft pass of refactoring the Integrator --- openpype/plugins/publish/integrate_new.py | 1076 ++++++++++----------- 1 file changed, 508 insertions(+), 568 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e8dab089af..e4986e3b3f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -7,9 +7,8 @@ import clique import errno import six import re -import shutil -from pymongo import DeleteOne, InsertOne +from pymongo import DeleteOne, InsertOne, UpdateOne import pyblish.api from avalon import io from avalon.api import format_template_with_optional_keys @@ -31,6 +30,17 @@ else: log = logging.getLogger(__name__) +def get_frame_padded(frame, padding): + """Return frame number as string with `padding` amount of padded zeros""" + 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 IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -108,7 +118,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username" + "family", "hierarchy", "task", "username", "frame", "udim" ] default_template_name = "publish" @@ -116,38 +126,40 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): TMP_FILE_EXT = 'tmp' # file_url : file_size of all published and uploaded files - integrated_file_sizes = {} + destinations = list() # Attributes set by settings template_name_profiles = None subset_grouping_profiles = None def process(self, instance): - self.integrated_file_sizes = {} - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: + self.destinations = [] + + # Exclude instances that also contain families from exclude families + families = set( + # Consider family and families data + [instance.data["family"]] + instance.data.get("families", []) + ) + if families & set(self.exclude_families): return try: self.register(instance) self.log.info("Integrated Asset in to the database ...") - self.log.info("instance.data: {}".format(instance.data)) - self.handle_destination_files(self.integrated_file_sizes, + self.handle_destination_files(self.destinations, 'finalize') except Exception: # clean destination self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.integrated_file_sizes, 'remove') + self.handle_destination_files(self.destinations, 'remove') six.reraise(*sys.exc_info()) - def register(self, instance): - # Required environment variables - anatomy_data = instance.data["anatomyData"] - - io.install() + def prepare_anatomy(self, instance): + """Prepare anatomy data used to define representation destinations""" context = instance.context + anatomy_data = instance.data["anatomyData"] project_entity = instance.data["projectEntity"] context_asset_name = None @@ -206,8 +218,36 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") - stagingdir = instance.data.get("stagingDir") - if not stagingdir: + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + anatomy_data["intent"] = intent_value + + # Get profile + key_values = { + "families": self.main_family_from_instance(instance), + "tasks": task_name, + "hosts": instance.context.data["hostName"], + "task_types": task_type + } + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log + ) + + template_name = "publish" + if profile: + template_name = profile["template_name"] + + return template_name, anatomy_data + + def register(self, instance): + + instance_stagingdir = instance.data.get("stagingDir") + if not instance_stagingdir: self.log.info(( "{0} is missing reference to staging directory." " Will try to get it from representation." @@ -215,7 +255,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: self.log.debug( - "Establishing staging directory @ {0}".format(stagingdir) + "Establishing staging directory " + "@ {0}".format(instance_stagingdir) ) # Ensure at least one file is set up for transfer in staging dir. @@ -227,28 +268,74 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) - subset = self.get_subset(asset_entity, instance) - instance.data["subsetEntity"] = subset + subset = self.register_subset(instance) + + version = self.register_version(instance, subset) + instance.data["versionEntity"] = version + instance.data['version'] = version['name'] + + existing_repres = list(io.find({ + "parent": version["_id"], + "type": "archived_representation" + })) + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + template_name, anatomy_data = self.prepare_anatomy(instance) + published_representations = {} + representations = [] + for repre in instance.data["representations"]: + + if "delete" in repre.get("tags", []): + self.log.debug("Skipping representation marked for deletion: " + "{}".format(repre)) + continue + + prepared = self.prepare_representation(repre, + anatomy_data, + template_name, + existing_repres, + version, + instance_stagingdir, + instance) + + # todo: simplify this? + representation = prepared["representation"] + representations.append(representation) + published_representations[representation["_id"]] = prepared + + # Remove old representations if there are any (before insertion of new) + if existing_repres: + repre_ids_to_remove = [repre["_id"] for repre in existing_repres] + io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + + # Write the new representations to the database + io.insert_many(representations) + + instance.data["published_representations"] = published_representations + + self.log.info("Registered {} representations" + "".format(len(representations))) + + def register_version(self, instance, subset): version_number = instance.data["version"] self.log.debug("Next version: v{}".format(version_number)) - version_data = self.create_version_data(context, instance) - + version_data = self.create_version_data(instance) version_data_instance = instance.data.get('versionData') if version_data_instance: version_data.update(version_data_instance) - # TODO rename method from `create_version` to - # `prepare_version` or similar... - version = self.create_version( - subset=subset, - version_number=version_number, - data=version_data - ) - - self.log.debug("Creating version ...") + version = { + "schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": version_data + } + repres = instance.data.get("representations", []) new_repre_names_low = [_repre["name"].lower() for _repre in repres] existing_version = io.find_one({ @@ -258,29 +345,28 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): }) if existing_version is None: + self.log.debug("Creating new version ...") version_id = io.insert_one(version).inserted_id else: + self.log.debug("Updating existing version ...") # Check if instance have set `append` mode which cause that # only replicated representations are set to archive append_repres = instance.data.get("append", False) + bulk_writes = [] # Update version data - # TODO query by _id and - io.update_many({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number + version_id = existing_version['_id'] + bulk_writes.append(UpdateOne({ + '_id': version_id }, { '$set': version - }) - version_id = existing_version['_id'] + })) # Find representations of existing version and archive them - current_repres = list(io.find({ + current_repres = io.find({ "type": "representation", "parent": version_id - })) - bulk_writes = [] + }) for repre in current_repres: if append_repres: # archive only duplicated representations @@ -304,346 +390,248 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) version = io.find_one({"_id": version_id}) - instance.data["versionEntity"] = version + return version - existing_repres = list(io.find({ - "parent": version_id, - "type": "archived_representation" - })) + def prepare_representation(self, repre, + anatomy_data, + template_name, + existing_repres, + version, + instance_stagingdir, + instance): - instance.data['version'] = version['name'] + # create template data for Anatomy + template_data = copy.deepcopy(anatomy_data) - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") + # pre-flight validations + if repre["ext"].startswith("."): + raise ValueError("Extension must not start with a dot '.': " + "{}".format(repre["ext"])) - if intent_value: - anatomy_data["intent"] = intent_value + if repre.get("transfers"): + raise ValueError("Representation is not allowed to have transfers" + "data before integration. " + "Got: {}".format(repre["transfers"])) - anatomy = instance.context.data['anatomy'] + # required representation keys + files = repre['files'] + template_data["representation"] = repre["name"] + template_data["ext"] = repre["ext"] - # Find the representations to transfer amongst the files - # Each should be a single representation (as such, a single extension) - representations = [] - destination_list = [] + # optionals + # retrieve additional anatomy data from representation if exists + for representation_key, anatomy_key in { + # Representation Key: Anatomy data key + "resolutionWidth": "resolution_width", + "resolutionHeight": "resolution_height", + "fps": "fps", + "outputName": "output", + }.items(): + value = repre.get(representation_key) + if value: + template_data[anatomy_key] = value - orig_transfers = [] - if 'transfers' not in instance.data: - instance.data['transfers'] = [] + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] else: - orig_transfers = list(instance.data['transfers']) + # 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)) + stagingdir = instance_stagingdir - family = self.main_family_from_instance(instance) + self.log.debug("Anatomy template name: {}".format(template_name)) + anatomy = instance.context.data['anatomy'] + template = os.path.normpath( + anatomy.templates[template_name]["path"]) - key_values = { - "families": family, - "tasks": task_name, - "hosts": instance.context.data["hostName"], - "task_types": task_type - } - profile = filter_profiles( - self.template_name_profiles, - key_values, - logger=self.log - ) + is_sequence_representation = isinstance(files, (list, tuple)) + if is_sequence_representation: + # Collection of files (sequence) + # Get the sequence as a collection. The files must be of a single + # sequence and have no remainder outside of the collections. + collections, remainder = clique.assemble(files, + minimum_items=1) + 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)) + src_collection = collections[0] - template_name = "publish" - if profile: - template_name = profile["template_name"] - - published_representations = {} - for idx, repre in enumerate(instance.data["representations"]): - # reset transfers for next representation - # instance.data['transfers'] is used as a global variable - # in current codebase - instance.data['transfers'] = list(orig_transfers) - - if "delete" in repre.get("tags", []): - continue - - published_files = [] - - # create template data for Anatomy - template_data = copy.deepcopy(anatomy_data) - if intent_value is not None: - template_data["intent"] = intent_value - - resolution_width = repre.get("resolutionWidth") - resolution_height = repre.get("resolutionHeight") - fps = instance.data.get("fps") - - if resolution_width: - template_data["resolution_width"] = resolution_width - if resolution_width: - template_data["resolution_height"] = resolution_height - if resolution_width: - template_data["fps"] = fps - - files = repre['files'] - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - - if repre.get("outputName"): - template_data["output"] = repre['outputName'] - - template_data["representation"] = repre["name"] - - ext = repre["ext"] - if ext.startswith("."): - self.log.warning(( - "Implementaion warning: <\"{}\">" - " Representation's extension stored under \"ext\" key " - " started with dot (\"{}\")." - ).format(repre["name"], ext)) - ext = ext[1:] - repre["ext"] = ext - template_data["ext"] = ext - - self.log.info(template_name) - template = os.path.normpath( - anatomy.templates[template_name]["path"]) - - sequence_repre = isinstance(files, list) - repre_context = None - if sequence_repre: - self.log.debug( - "files: {}".format(files)) - src_collections, remainder = clique.assemble(files) - self.log.debug( - "src_tail_collections: {}".format(str(src_collections))) - src_collection = src_collections[0] - - # Assert that each member has identical suffix - src_head = src_collection.format("{head}") - src_tail = src_collection.format("{tail}") - - # fix dst_padding - valid_files = [x for x in files if src_collection.match(x)] - padd_len = len( - valid_files[0].replace(src_head, "").replace(src_tail, "") - ) - src_padding_exp = "%0{}d".format(padd_len) - - test_dest_files = list() - for i in [1, 2]: - template_data["representation"] = repre['ext'] - if not repre.get("udim"): - template_data["frame"] = src_padding_exp % i - else: - template_data["udim"] = src_padding_exp % i - - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - if repre_context is None: - repre_context = template_filled.used_values - test_dest_files.append( - os.path.normpath(template_filled) - ) - if not repre.get("udim"): - template_data["frame"] = repre_context["frame"] - else: - template_data["udim"] = repre_context["udim"] - - self.log.debug( - "test_dest_files: {}".format(str(test_dest_files))) - - dst_collections, remainder = clique.assemble(test_dest_files) - dst_collection = dst_collections[0] - dst_head = dst_collection.format("{head}") - dst_tail = dst_collection.format("{tail}") - - index_frame_start = None + # 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) + destination_padding = len(get_first_frame_padded(src_collection)) + if repre.get("frameStart") is not None: + index_frame_start = int(repre.get("frameStart")) # TODO use frame padding from right template group - if repre.get("frameStart") is not None: - frame_start_padding = int( - anatomy.templates["render"].get( - "frame_padding", - anatomy.templates["render"].get("padding") - ) + render_template = anatomy.templates["render"] + frame_start_padding = int( + render_template.get( + "frame_padding", + render_template.get("padding") ) - - index_frame_start = int(repre.get("frameStart")) - - # exception for slate workflow - if index_frame_start and "slate" in instance.data["families"]: - index_frame_start -= 1 - - dst_padding_exp = src_padding_exp - dst_start_frame = None - collection_start = list(src_collection.indexes)[0] - for i in src_collection.indexes: - # TODO 1.) do not count padding in each index iteration - # 2.) do not count dst_padding from src_padding before - # index_frame_start check - frame_number = i - collection_start - src_padding = src_padding_exp % i - - src_file_name = "{0}{1}{2}".format( - src_head, src_padding, src_tail) - - dst_padding = src_padding_exp % frame_number - - if index_frame_start is not None: - dst_padding_exp = "%0{}d".format(frame_start_padding) - dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 - elif repre.get("udim"): - dst_padding = int(i) - - dst = "{0}{1}{2}".format( - dst_head, - dst_padding, - dst_tail - ) - - self.log.debug("destination: `{}`".format(dst)) - src = os.path.join(stagingdir, src_file_name) - - self.log.debug("source: {}".format(src)) - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - - # for adding first frame into db - if not dst_start_frame: - dst_start_frame = dst_padding - - # Store used frame value to template data - if repre.get("frame"): - template_data["frame"] = dst_start_frame - - dst = "{0}{1}{2}".format( - dst_head, - dst_start_frame, - dst_tail - ) - repre['published_path'] = dst - - else: - # Single file - # _______ - # | |\ - # | | - # | | - # | | - # |_______| - # - template_data.pop("frame", None) - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" ) - template_data["representation"] = repre['ext'] - # Store used frame value to template data - if repre.get("udim"): - template_data["udim"] = repre["udim"][0] - src = os.path.join(stagingdir, fname) - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - dst = os.path.normpath(template_filled) - - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - repre['published_path'] = dst - self.log.debug("__ dst: {}".format(dst)) + # 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 + # To construct the destination template with anatomy we require + # 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 repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list + # UDIM representations handle ranges in a different manner + template_data["udim"] = first_index_padded + else: + template_data["frame"] = first_index_padded - repre["publishedFiles"] = published_files + # Construct destination collection from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + self.log.debug("Template filled: {}".format(str(template_filled))) + dst_collections, _remainder = clique.assemble( + [os.path.normpath(template_filled)], minimum_items=1 + ) + assert not _remainder, "This is a bug" + assert len(dst_collections) == 1, "This is a bug" + dst_collection = dst_collections[0] - for key in self.db_representation_context_keys: - value = template_data.get(key) - if not value: - continue - repre_context[key] = template_data[key] + # Update the destination indexes and padding + dst_collection.indexes = destination_indexes + dst_collection.padding = destination_padding + assert len(src_collection) == len(dst_collection), "This is a bug" - # Use previous representation's id if there are any - repre_id = None - repre_name_low = repre["name"].lower() - for _repre in existing_repres: - # NOTE should we check lowered names? - if repre_name_low == _repre["name"]: - repre_id = _repre["orig_id"] - break + transfers = [] + for src_file_name, dst in zip(src_collection, dst_collection): + src = os.path.join(stagingdir, src_file_name) + self.log.debug("source: {}".format(src)) + self.log.debug("destination: `{}`".format(dst)) + transfers.append(src, dst) - # Create new id if existing representations does not match - if repre_id is None: - repre_id = io.ObjectId() + # Store first frame as published path + # todo: remove `published_path` since it can be retrieved from + # `transfers` by taking the first destination transfers[0][1] + repre['published_path'] = next(iter(dst_collection)) + repre["transfers"].extend(transfers) - data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) - representation = { - "_id": repre_id, - "schema": "openpype:representation-2.0", - "type": "representation", - "parent": version_id, - "name": repre['name'], - "data": data, - "dependencies": instance.data.get("dependencies", "").split(), + else: + # Single file + template_data.pop("frame", None) + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] + src = os.path.join(stagingdir, fname) + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) - # Imprint shortcut to context - # for performance reasons. - "context": repre_context - } + # Single file transfer + self.log.debug("source: {}".format(src)) + self.log.debug("destination: `{}`".format(dst)) + repre["transfers"] = [src, dst] - if repre.get("outputName"): - representation["context"]["output"] = repre['outputName'] + repre['published_path'] = dst - if sequence_repre and repre.get("frameStart") is not None: - representation['context']['frame'] = ( - dst_padding_exp % int(repre.get("frameStart")) - ) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list - # any file that should be physically copied is expected in - # 'transfers' or 'hardlinks' - if instance.data.get('transfers', False) or \ - instance.data.get('hardlinks', False): - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed - self.log.debug("Integrating source files to destination ...") - self.integrated_file_sizes.update(self.integrate(instance)) - self.log.debug("Integrated files {}". - format(self.integrated_file_sizes)) + for key in self.db_representation_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] - # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - self.integrated_file_sizes) + # Use previous representation's id if there are any + repre_id = None + repre_name_lower = repre["name"].lower() + for _existing_repre in existing_repres: + # NOTE should we check lowered names? + if repre_name_lower == _existing_repre["name"].lower(): + repre_id = _existing_repre["orig_id"] + break - self.log.debug("__ representation: {}".format(representation)) - destination_list.append(dst) - self.log.debug("__ destination_list: {}".format(destination_list)) - instance.data['destination_list'] = destination_list - representations.append(representation) - published_representations[repre_id] = { - "representation": representation, - "anatomy_data": template_data, - "published_files": published_files - } - self.log.debug("__ representations: {}".format(representations)) + # Create new id if existing representations does not match + if repre_id is None: + repre_id = io.ObjectId() - # Remove old representations if there are any (before insertion of new) - if existing_repres: - repre_ids_to_remove = [] - for repre in existing_repres: - repre_ids_to_remove.append(repre["_id"]) - io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + # todo: `repre` is not the actual `representation` entity + # we should simplify/clarify difference between data above + # and the actual representation entity for the database + data = repre.get("data") or {} + data.update({'path': dst, 'template': template}) + representation = { + "_id": repre_id, + "schema": "openpype:representation-2.0", + "type": "representation", + "parent": version["_id"], + "name": repre['name'], + "data": data, + "dependencies": instance.data.get("dependencies", "").split(), - for rep in instance.data["representations"]: - self.log.debug("__ rep: {}".format(rep)) + # Imprint shortcut to context for performance reasons. + "context": repre_context + } - io.insert_many(representations) - instance.data["published_representations"] = ( - published_representations + 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"] + + # any file that should be physically copied is expected in + # 'transfers' or 'hardlinks' + integrated_files = [] + if instance.data.get('transfers', False) or \ + instance.data.get('hardlinks', False): + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + # todo: separate the actual integrating of the files onto its own + # taking just a list of transfers as inputs (potentially + # with copy mode flag, like hardlink/copy, etc.) + self.log.debug("Integrating source files to destination ...") + integrated_files = self.integrate(instance) + self.log.debug("Integrated files {}".format(integrated_files)) + + # get 'files' info for representation and all attached resources + self.log.debug("Preparing files information ...") + representation["files"] = self.get_files_info( + instance, + integrated_files ) - # self.log.debug("Representation: {}".format(representations)) - self.log.info("Registered {} items".format(len(representations))) + + return { + "representation": representation, + "anatomy_data": template_data, + # todo: avoid the need for 'published_files'? + # backwards compatibility + "published_files": [transfer[1] for transfer in repre["transfers"]] + } def integrate(self, instance): """ Move the files. @@ -653,92 +641,93 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Args: instance: the instance to integrate Returns: - integrated_file_sizes: dictionary of destination file url and - its size in bytes + list: destination full paths of integrated files """ - # store destination url and size for reporting and rollback - integrated_file_sizes = {} + # store destinations for potential rollback and measuring sizes + destinations = [] transfers = list(instance.data.get("transfers", list())) for src, dest in transfers: - if os.path.normpath(src) != os.path.normpath(dest): + src = os.path.normpath(src) + dest = os.path.normpath(dest) + if src != dest: dest = self.get_dest_temp_url(dest) self.copy_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + destinations.append(dest) # Produce hardlinked copies - # Note: hardlink can only be produced between two files on the same - # server/disk and editing one of the two will edit both files at once. - # As such it is recommended to only make hardlinks between static files - # to ensure publishes remain safe and non-edited. hardlinks = instance.data.get("hardlinks", list()) for src, dest in hardlinks: dest = self.get_dest_temp_url(dest) - self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) if not os.path.exists(dest): self.hardlink_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + destinations.append(dest) - return integrated_file_sizes + return destinations + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) def copy_file(self, src, dst): - """ Copy given source to destination + """Copy source filepath to destination filepath Arguments: src (str): the source file which needs to be copied - dst (str): the destination of the sourc file + dst (str): the destination filepath + + Returns: + None + + """ + self._create_folder_for_file(dst) + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + + def hardlink_file(self, src, dst): + """Hardlink source filepath to destination filepath. + + Note: + Hardlink can only be produced between two files on the same + server/disk and editing one of the two will edit both files at + once. As such it is recommended to only make hardlinks between + static files to ensure publishes remain safe and non-edited. + + Arguments: + src (str): the source file which needs to be hardlinked + dst (str): the destination filepath + Returns: None """ - src = os.path.normpath(src) - dst = os.path.normpath(dst) - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - dirname = os.path.dirname(dst) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - # copy file with speedcopy and check if size of files are simetrical - while True: - if not shutil._samefile(src, dst): - copyfile(src, dst) - else: - self.log.critical( - "files are the same {} to {}".format(src, dst) - ) - os.remove(dst) - try: - shutil.copyfile(src, dst) - self.log.debug("Copying files with shutil...") - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) - if str(getsize(src)) in str(getsize(dst)): - break - - def hardlink_file(self, src, dst): - dirname = os.path.dirname(dst) - - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - + self._create_folder_for_file(dst) + self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) create_hard_link(src, dst) - def get_subset(self, asset, instance): + def _get_instance_families(self, instance): + """Get all families of the instance""" + # todo: move this to lib? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + def register_subset(self, instance): + # todo: rely less on self.prepare_anatomy to create this value + asset = instance.data.get("assetEntity") # <- from prepare_anatomy :( subset_name = instance.data["subset"] subset = io.find_one({ "type": "subset", @@ -748,18 +737,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if subset is None: self.log.info("Subset '%s' not found, creating ..." % subset_name) - self.log.debug("families. %s" % instance.data.get('families')) - self.log.debug( - "families. %s" % type(instance.data.get('families'))) - - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) + families = self._get_instance_families(instance) _id = io.insert_one({ "schema": "openpype:subset-3.0", @@ -773,8 +751,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset = io.find_one({"_id": _id}) - # QUESTION Why is changing of group and updating it's - # families in 'get_subset'? + # Update subset group self._set_subset_group(instance, subset["_id"]) # Update families on subset. @@ -838,7 +815,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.subset_grouping_profiles, filtering_criteria ) - # Skip if there is not matchin profile + # Skip if there is not matching profile if not matching_profile: return None @@ -867,41 +844,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return filled_template - def create_version(self, subset, version_number, data=None): - """ Copy given source to destination - - Args: - subset (dict): the registered subset of the asset - version_number (int): the version number - - Returns: - dict: collection of data to create a version - """ - - return {"schema": "openpype:version-3.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "data": data} - - def create_version_data(self, context, instance): + def create_version_data(self, instance): """Create the data collection for the version Args: - context: the current context instance: the current instance being published Returns: dict: the required information with instance.data as key """ - families = [] - current_families = instance.data.get("families", list()) - instance_family = instance.data.get("family", None) - - if instance_family is not None: - families.append(instance_family) - families += current_families + context = instance.context # create relative source path for DB if "source" in instance.data: @@ -910,10 +863,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] source = self.get_rootless_path(anatomy, source) - self.log.debug("Source: {}".format(source)) + version_data = { - "families": families, + "families": self._get_instance_families(instance), "time": context.data["time"], "author": context.data["user"], "source": source, @@ -924,7 +877,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) } - intent_value = instance.context.data.get("intent") + intent_value = context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") @@ -944,10 +897,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def main_family_from_instance(self, instance): """Returns main family of entered instance.""" - family = instance.data.get("family") - if not family: - family = instance.data["families"][0] - return family + return self._get_instance_families(instance)[0] def get_rootless_path(self, anatomy, path): """ Returns, if possible, path without absolute portion from host @@ -976,7 +926,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, instance, integrated_file_sizes): + def get_files_info(self, instance): """ Prepare 'files' portion for attached resources and main asset. Combining records from 'transfers' and 'hardlinks' parts from instance. @@ -991,27 +941,18 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): output_resources: array of dictionaries to be added to 'files' key in representation """ + # todo: refactor to use transfers/hardlinks of representations + # currently broken logic resources = list(instance.data.get("transfers", [])) resources.extend(list(instance.data.get("hardlinks", []))) + self.log.debug("get_files_info.resources:{}".format(resources)) - self.log.debug("get_resource_files_info.resources:{}". - format(resources)) + sites = self.compute_resource_sync_sites(instance) output_resources = [] anatomy = instance.context.data["anatomy"] for _src, dest in resources: - path = self.get_rootless_path(anatomy, dest) - dest = self.get_dest_temp_url(dest) - file_hash = openpype.api.source_hash(dest) - if self.TMP_FILE_EXT and \ - ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), - '') - - file_info = self.prepare_file_info(path, - integrated_file_sizes[dest], - file_hash, - instance=instance) + file_info = self.prepare_file_info(dest, anatomy, sites=sites) output_resources.append(file_info) return output_resources @@ -1031,8 +972,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dest += '.{}'.format(self.TMP_FILE_EXT) return dest - def prepare_file_info(self, path, size=None, file_hash=None, - sites=None, instance=None): + def get_dest_final_url(self, temp_file_url): + """Temporary destination file url to final destination file url""" + return re.sub(r'\.{}$'.format(self.TMP_FILE_EXT), '', temp_file_url) + + def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) Arguments: @@ -1042,74 +986,78 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): sites(optional): array of published locations, [ {'name':'studio', 'created_dt':date} by default keys expected ['studio', 'site1', 'gdrive1'] - instance(dict, optional): to get collected settings Returns: rec: dictionary with filled info """ + file_hash = openpype.api.source_hash(path) + + # todo: Avoid this logic + # Strip the temporary file extension from the file hash + if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') + + return { + "_id": io.ObjectId(), + "path": self.get_rootless_path(anatomy, path), + "size": os.path.getsize(path), + "hash": file_hash, + "sites": sites + } + + def compute_resource_sync_sites(self, instance): + """Get available resource sync sites""" + # Sync server logic + # TODO: Clean up sync settings local_site = 'studio' # default remote_site = None - always_accesible = [] + always_accessible = [] sync_project_presets = None - rec = { - "_id": io.ObjectId(), - "path": path - } - if size: - rec["size"] = size + system_sync_server_presets = ( + instance.context.data["system_settings"] + ["modules"] + ["sync_server"]) + log.debug("system_sett:: {}".format(system_sync_server_presets)) - if file_hash: - rec["hash"] = file_hash - - if sites: - rec["sites"] = sites - else: - system_sync_server_presets = ( - instance.context.data["system_settings"] - ["modules"] + if system_sync_server_presets["enabled"]: + sync_project_presets = ( + instance.context.data["project_settings"] + ["global"] ["sync_server"]) - log.debug("system_sett:: {}".format(system_sync_server_presets)) - if system_sync_server_presets["enabled"]: - sync_project_presets = ( - instance.context.data["project_settings"] - ["global"] - ["sync_server"]) + if sync_project_presets and sync_project_presets["enabled"]: + local_site, remote_site = self._get_sites(sync_project_presets) + always_accessible = sync_project_presets["config"]. \ + get("always_accessible_on", []) - if sync_project_presets and sync_project_presets["enabled"]: - local_site, remote_site = self._get_sites(sync_project_presets) + already_attached_sites = {} + meta = {"name": local_site, "created_dt": datetime.now()} + sites = [meta] + already_attached_sites[meta["name"]] = meta["created_dt"] - always_accesible = sync_project_presets["config"]. \ - get("always_accessible_on", []) + if sync_project_presets and sync_project_presets["enabled"]: + if remote_site and \ + remote_site not in already_attached_sites.keys(): + # add remote + meta = {"name": remote_site.strip()} + sites.append(meta) + already_attached_sites[meta["name"]] = None - already_attached_sites = {} - meta = {"name": local_site, "created_dt": datetime.now()} - rec["sites"] = [meta] - already_attached_sites[meta["name"]] = meta["created_dt"] - - if sync_project_presets and sync_project_presets["enabled"]: - if remote_site and \ - remote_site not in already_attached_sites.keys(): - # add remote - meta = {"name": remote_site.strip()} - rec["sites"].append(meta) + # add skeleton for site where it should be always synced to + for always_on_site in always_accessible: + if always_on_site not in already_attached_sites.keys(): + meta = {"name": always_on_site.strip()} + sites.append(meta) already_attached_sites[meta["name"]] = None - # add skeleton for site where it should be always synced to - for always_on_site in always_accesible: - if always_on_site not in already_attached_sites.keys(): - meta = {"name": always_on_site.strip()} - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = None + # add alternative sites + alt = self._add_alternative_sites(system_sync_server_presets, + already_attached_sites) + sites.extend(alt) - # add alternative sites - rec = self._add_alternative_sites(system_sync_server_presets, - already_attached_sites, - rec) + log.debug("final sites:: {}".format(sites)) - log.debug("final sites:: {}".format(rec["sites"])) - - return rec + return sites def _get_sites(self, sync_project_presets): """Returns tuple (local_site, remote_site)""" @@ -1129,14 +1077,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def _add_alternative_sites(self, system_sync_server_presets, - already_attached_sites, - rec): + already_attached_sites): """Loop through all configured sites and add alternatives. See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) + alternative_sites = [] for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) already_attached_keys = list(already_attached_sites.keys()) @@ -1149,12 +1097,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # alt site inherits state of 'created_dt' if real_created: meta["created_dt"] = real_created - rec["sites"].append(meta) + alternative_sites.append(meta) already_attached_sites[meta["name"]] = real_created - return rec + return alternative_sites - def handle_destination_files(self, integrated_file_sizes, mode): + def handle_destination_files(self, destinations, mode): """ Clean destination files Called when error happened during integrating to DB or to disk OR called to rename uploaded files from temporary name to final to @@ -1162,46 +1110,38 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Used to clean unwanted files Arguments: - integrated_file_sizes: dictionary, file urls as keys, size as value + destinations (list): file paths mode: 'remove' - clean files, 'finalize' - rename files, remove TMP_FILE_EXT suffix denoting temp file """ - if integrated_file_sizes: - for file_url, _file_size in integrated_file_sizes.items(): - if not os.path.exists(file_url): + if not destinations: + return + + for file_url in destinations: + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + + try: + if mode == 'remove': + self.log.debug("Removing file {}".format(file_url)) + os.remove(file_url) + if mode == 'finalize': + + new_name = self.get_dest_final_url(file_url) + if os.path.exists(new_name): + self.log.debug("Removing existing " + "file: {}".format(new_name)) + os.remove(new_name) + self.log.debug( - "File {} was not found.".format(file_url) + "Renaming file {} to {}".format(file_url, new_name) ) - continue - - try: - if mode == 'remove': - self.log.debug("Removing file {}".format(file_url)) - os.remove(file_url) - if mode == 'finalize': - new_name = re.sub( - r'\.{}$'.format(self.TMP_FILE_EXT), - '', - file_url - ) - - if os.path.exists(new_name): - self.log.debug( - "Overwriting file {} to {}".format( - file_url, new_name - ) - ) - shutil.copy(file_url, new_name) - os.remove(file_url) - else: - self.log.debug( - "Renaming file {} to {}".format( - file_url, new_name - ) - ) - os.rename(file_url, new_name) - except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url), - exc_info=True) - six.reraise(*sys.exc_info()) + os.rename(file_url, new_name) + except OSError: + self.log.error("Cannot {} file {}".format(mode, file_url), + exc_info=True) + six.reraise(*sys.exc_info()) From ae1a9ff4cf996445bd74dcd7641639ed8342592e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 11:49:12 +0100 Subject: [PATCH 0020/1030] More refactoring + draft (untested) implementation for separating File Transaction logic --- openpype/plugins/publish/integrate_new.py | 421 +++++++++++----------- 1 file changed, 215 insertions(+), 206 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e4986e3b3f..500456eaed 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -1,12 +1,10 @@ import os -from os.path import getsize import logging import sys import copy import clique import errno import six -import re from pymongo import DeleteOne, InsertOne, UpdateOne import pyblish.api @@ -14,7 +12,6 @@ from avalon import io from avalon.api import format_template_with_optional_keys import openpype.api from datetime import datetime -# from pype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib import ( prepare_template_data, @@ -41,6 +38,160 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) +class FileTransaction(object): + """ + + The file transaction is a three step process. + + 1) Rename any existing files to a "temporary backup" during `process()` + 2) Copy the files to final destination during `process()` + 3) Remove any backed up files (*no rollback possible!) during `finalize()` + + Step 3 is done during `finalize()`. If not called the .bak files will + remain on disk. + + These steps try to ensure that we don't overwrite half of any existing + files e.g. if they are currently in use. + + Note: + A regular filesystem is *not* a transactional file system and even + though this implementation tries to produce a 'safe copy' with a + potential rollback do keep in mind that it's inherently unsafe due + to how filesystem works and a myriad of things could happen during + the transaction that break the logic. A file storage could go down, + permissions could be changed, other machines could be moving or writing + files. A lot can happen. + + Warning: + Any folders created during the transfer will not be removed. + + """ + + MODE_COPY = 0 + MODE_HARDLINK = 1 + + def __init__(self, log=None): + + if log is None: + log = logging.getLogger("FileTransaction") + + self.log = log + + # The transfer queue + # todo: make this an actual FIFO queue? + self._transfers = {} + + # Destination file paths that a file was transferred to + self._transferred = [] + + # Backup file location mapping to original locations + self._backup_to_original = {} + + def add(self, src, dst, mode=MODE_COPY): + """Add a new file to transfer queue""" + opts = {"mode": mode} + + src = os.path.normpath(src) + dst = os.path.normpath(dst) + + if dst in self._transfers: + queued_src = self._transfers[dst][0] + if src == queued_src: + self.log.debug("File transfer was already " + "in queue: {} -> {}".format(src, dst)) + return + else: + self.log.warning("File transfer in queue overwritten") + + self._transfers[dst] = (src, opts) + + def process(self): + + # Backup any existing files + for dst in self._transfers.keys(): + if os.path.exists(dst): + # Backup original file + # todo: add timestamp or uuid to ensure unique + backup = dst + ".bak" + self._backup_to_original[backup] = dst + self.log.debug("Backup existing file: " + "{} -> {}".format(dst, backup)) + os.rename(dst, backup) + + # Copy the files to transfer + for dst, (src, opts) in self._transfers.items(): + self._create_folder_for_file(dst) + + if opts["mode"] == self.MODE_COPY: + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + elif opts["mode"] == self.MODE_HARDLINK: + self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) + create_hard_link(src, dst) + + self._transferred.append(dst) + + def finalize(self): + # Delete any backed up files + for backup in self._backup_to_original.keys(): + try: + os.remove(backup) + except OSError: + self.log.error("Failed to remove backup file: " + "{}".format(backup), + exc_info=True) + + def rollback(self): + + errors = 0 + + # Rollback any transferred files + for path in self._transferred: + try: + os.remove(path) + except OSError: + errors += 1 + self.log.error("Failed to rollback created file: " + "{}".format(path), + exc_info=True) + + # Rollback the backups + for backup, original in self._backup_to_original.items(): + try: + os.rename(backup, original) + except OSError: + errors +=1 + self.log.error("Failed to restore original file: " + "{} -> {}".format(backup, original), + exc_info=True) + + if errors: + self.log.error("{} errors occurred during " + "rollback.".format(errors), exc_info=True) + six.reraise(*sys.exc_info()) + + @property + def transferred(self): + """Return the processed transfers destination paths""" + return list(self._transferred) + + @property + def backups(self): + """Return the backup file paths""" + return list(self._backup_to_original.keys()) + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -122,18 +273,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ] default_template_name = "publish" - # suffix to denote temporary files, use without '.' - TMP_FILE_EXT = 'tmp' - - # file_url : file_size of all published and uploaded files - destinations = list() - # Attributes set by settings template_name_profiles = None subset_grouping_profiles = None def process(self, instance): - self.destinations = [] # Exclude instances that also contain families from exclude families families = set( @@ -143,17 +287,20 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return + file_transactions = FileTransaction(log=self.log) try: - self.register(instance) - self.log.info("Integrated Asset in to the database ...") - self.handle_destination_files(self.destinations, - 'finalize') + self.register(instance, file_transactions) except Exception: # clean destination + # todo: rollback any registered entities? (or how safe are we?) + file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.destinations, 'remove') six.reraise(*sys.exc_info()) + # Finalizing can't be rollbacked safely so no use for moving it to + # the try, except. + file_transactions.finalize() + def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" @@ -244,7 +391,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return template_name, anatomy_data - def register(self, instance): + def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: @@ -272,9 +419,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version = self.register_version(instance, subset) instance.data["versionEntity"] = version - instance.data['version'] = version['name'] - existing_repres = list(io.find({ + archived_repres = list(io.find({ "parent": version["_id"], "type": "archived_representation" })) @@ -294,19 +440,47 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): prepared = self.prepare_representation(repre, anatomy_data, template_name, - existing_repres, + archived_repres, version, instance_stagingdir, instance) + representation = prepared["representation"] + + # todo: register the file transfers correctly + for src, dst in representation["transfers"]: + file_transactions.add(src, dst, + mode=file_transactions.MODE_COPY) + for src, dst in representation["hardlinks"]: + file_transactions.add(src, dst, + mode=file_transactions.MODE_HARDLINK) # todo: simplify this? - representation = prepared["representation"] representations.append(representation) published_representations[representation["_id"]] = prepared + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + self.log.debug("Integrating source files to destination ...") + file_transactions.process() + self.log.debug("Backup files " + "{}".format(file_transactions.backups)) + self.log.debug("Integrated files " + "{}".format(file_transactions.transferred)) + + # todo: fix get file info for transferred files per representation + # currently it'd set all files for all representations + # get 'files' info for representation and all attached resources + integrated_files = file_transactions.transferred + self.log.debug("Preparing files information ...") + representation["files"] = self.get_files_info( + instance, + integrated_files + ) + # Remove old representations if there are any (before insertion of new) - if existing_repres: - repre_ids_to_remove = [repre["_id"] for repre in existing_repres] + if archived_repres: + repre_ids_to_remove = [repre["_id"] for repre in archived_repres] io.delete_many({"_id": {"$in": repre_ids_to_remove}}) # Write the new representations to the database @@ -395,7 +569,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_representation(self, repre, anatomy_data, template_name, - existing_repres, + archived_repres, version, instance_stagingdir, instance): @@ -439,11 +613,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Representation uses instance staging dir: " "{}".format(instance_stagingdir)) stagingdir = instance_stagingdir + if not stagingdir: + raise ValueError("No staging directory set for representation: " + "{}".format(repre)) self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data['anatomy'] - template = os.path.normpath( - anatomy.templates[template_name]["path"]) + template = os.path.normpath(anatomy.templates[template_name]["path"]) is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: @@ -566,24 +742,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): continue repre_context[key] = template_data[key] - # Use previous representation's id if there are any - repre_id = None - repre_name_lower = repre["name"].lower() - for _existing_repre in existing_repres: - # NOTE should we check lowered names? - if repre_name_lower == _existing_repre["name"].lower(): - repre_id = _existing_repre["orig_id"] - break + # Define representation id + repre_id = io.ObjectId() - # Create new id if existing representations does not match - if repre_id is None: - repre_id = io.ObjectId() + # Use previous representation's id if there is a name match + repre_name_lower = repre["name"].lower() + for _archived_repres in archived_repres: + if repre_name_lower == _archived_repres["name"].lower(): + repre_id = _archived_repres["orig_id"] + break # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) + data.update({'path': repre["published_path"], 'template': template}) representation = { "_id": repre_id, "schema": "openpype:representation-2.0", @@ -597,34 +770,14 @@ class IntegrateAssetNew(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"] - # any file that should be physically copied is expected in - # 'transfers' or 'hardlinks' - integrated_files = [] - if instance.data.get('transfers', False) or \ - instance.data.get('hardlinks', False): - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed - # todo: separate the actual integrating of the files onto its own - # taking just a list of transfers as inputs (potentially - # with copy mode flag, like hardlink/copy, etc.) - self.log.debug("Integrating source files to destination ...") - integrated_files = self.integrate(instance) - self.log.debug("Integrated files {}".format(integrated_files)) - - # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - integrated_files - ) - return { "representation": representation, "anatomy_data": template_data, @@ -633,84 +786,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "published_files": [transfer[1] for transfer in repre["transfers"]] } - def integrate(self, instance): - """ Move the files. - - Through `instance.data["transfers"]` - - Args: - instance: the instance to integrate - Returns: - list: destination full paths of integrated files - """ - # store destinations for potential rollback and measuring sizes - destinations = [] - transfers = list(instance.data.get("transfers", list())) - for src, dest in transfers: - src = os.path.normpath(src) - dest = os.path.normpath(dest) - if src != dest: - dest = self.get_dest_temp_url(dest) - self.copy_file(src, dest) - destinations.append(dest) - - # Produce hardlinked copies - hardlinks = instance.data.get("hardlinks", list()) - for src, dest in hardlinks: - dest = self.get_dest_temp_url(dest) - if not os.path.exists(dest): - self.hardlink_file(src, dest) - - destinations.append(dest) - - return destinations - - def _create_folder_for_file(self, path): - dirname = os.path.dirname(path) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - def copy_file(self, src, dst): - """Copy source filepath to destination filepath - - Arguments: - src (str): the source file which needs to be copied - dst (str): the destination filepath - - Returns: - None - - """ - self._create_folder_for_file(dst) - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - copyfile(src, dst) - - def hardlink_file(self, src, dst): - """Hardlink source filepath to destination filepath. - - Note: - Hardlink can only be produced between two files on the same - server/disk and editing one of the two will edit both files at - once. As such it is recommended to only make hardlinks between - static files to ensure publishes remain safe and non-edited. - - Arguments: - src (str): the source file which needs to be hardlinked - dst (str): the destination filepath - - Returns: - None - """ - self._create_folder_for_file(dst) - self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) - create_hard_link(src, dst) - def _get_instance_families(self, instance): """Get all families of the instance""" # todo: move this to lib? @@ -727,7 +802,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def register_subset(self, instance): # todo: rely less on self.prepare_anatomy to create this value - asset = instance.data.get("assetEntity") # <- from prepare_anatomy :( + asset = instance.data.get("assetEntity") # stored by prepare_anatomy subset_name = instance.data["subset"] subset = io.find_one({ "type": "subset", @@ -957,25 +1032,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return output_resources - def get_dest_temp_url(self, dest): - """ Enhance destination path with TMP_FILE_EXT to denote temporary - file. - Temporary files will be renamed after successful registration - into DB and full copy to destination - - Arguments: - dest: destination url of published file (absolute) - Returns: - dest: destination path + '.TMP_FILE_EXT' - """ - if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: - dest += '.{}'.format(self.TMP_FILE_EXT) - return dest - - def get_dest_final_url(self, temp_file_url): - """Temporary destination file url to final destination file url""" - return re.sub(r'\.{}$'.format(self.TMP_FILE_EXT), '', temp_file_url) - def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) @@ -991,11 +1047,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ file_hash = openpype.api.source_hash(path) - # todo: Avoid this logic - # Strip the temporary file extension from the file hash - if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') - return { "_id": io.ObjectId(), "path": self.get_rootless_path(anatomy, path), @@ -1004,6 +1055,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "sites": sites } + # region sync sites def compute_resource_sync_sites(self, instance): """Get available resource sync sites""" # Sync server logic @@ -1101,47 +1153,4 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): already_attached_sites[meta["name"]] = real_created return alternative_sites - - def handle_destination_files(self, destinations, mode): - """ Clean destination files - Called when error happened during integrating to DB or to disk - OR called to rename uploaded files from temporary name to final to - highlight publishing in progress/broken - Used to clean unwanted files - - Arguments: - destinations (list): file paths - mode: 'remove' - clean files, - 'finalize' - rename files, - remove TMP_FILE_EXT suffix denoting temp file - """ - if not destinations: - return - - for file_url in destinations: - if not os.path.exists(file_url): - self.log.debug( - "File {} was not found.".format(file_url) - ) - continue - - try: - if mode == 'remove': - self.log.debug("Removing file {}".format(file_url)) - os.remove(file_url) - if mode == 'finalize': - - new_name = self.get_dest_final_url(file_url) - if os.path.exists(new_name): - self.log.debug("Removing existing " - "file: {}".format(new_name)) - os.remove(new_name) - - self.log.debug( - "Renaming file {} to {}".format(file_url, new_name) - ) - os.rename(file_url, new_name) - except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url), - exc_info=True) - six.reraise(*sys.exc_info()) + # endregion From 9f6cc5df3a11031fb18155c97e0a73bb6f3f6108 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 11:51:06 +0100 Subject: [PATCH 0021/1030] Fix hound --- openpype/plugins/publish/integrate_new.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 500456eaed..e74b528ae7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -126,7 +126,8 @@ class FileTransaction(object): self.log.debug("Copying file ... {} -> {}".format(src, dst)) copyfile(src, dst) elif opts["mode"] == self.MODE_HARDLINK: - self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) + self.log.debug("Hardlinking file ... {} -> {}".format(src, + dst)) create_hard_link(src, dst) self._transferred.append(dst) @@ -160,7 +161,7 @@ class FileTransaction(object): try: os.rename(backup, original) except OSError: - errors +=1 + errors += 1 self.log.error("Failed to restore original file: " "{} -> {}".format(backup, original), exc_info=True) From 56bcd8cec35f201ead80a18c09f6c070b76209c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 16:30:49 +0100 Subject: [PATCH 0022/1030] Continue refactor, restore functionality - now can correctly publish as before (rudimentary tested only) --- openpype/plugins/publish/integrate_new.py | 136 +++++++++++----------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e74b528ae7..c550c1011c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -101,7 +101,10 @@ class FileTransaction(object): "in queue: {} -> {}".format(src, dst)) return else: - self.log.warning("File transfer in queue overwritten") + self.log.warning("File transfer in queue replaced..") + self.log.debug("Removed from queue: " + "{} -> {}".format(queued_src, dst)) + self.log.debug("Added to queue: {} -> {}".format(src, dst)) self._transfers[dst] = (src, opts) @@ -298,7 +301,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.critical("Error when registering", exc_info=True) six.reraise(*sys.exc_info()) - # Finalizing can't be rollbacked safely so no use for moving it to + # Finalizing can't rollback safely so no use for moving it to # the try, except. file_transactions.finalize() @@ -426,11 +429,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "type": "archived_representation" })) - # Find the representations to transfer amongst the files - # Each should be a single representation (as such, a single extension) + # Prepare all representations template_name, anatomy_data = self.prepare_anatomy(instance) - published_representations = {} - representations = [] + prepared_representations = [] for repre in instance.data["representations"]: if "delete" in repre.get("tags", []): @@ -438,6 +439,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "{}".format(repre)) continue + # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, anatomy_data, template_name, @@ -445,23 +447,23 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version, instance_stagingdir, instance) - representation = prepared["representation"] - # todo: register the file transfers correctly - for src, dst in representation["transfers"]: - file_transactions.add(src, dst, - mode=file_transactions.MODE_COPY) - for src, dst in representation["hardlinks"]: - file_transactions.add(src, dst, - mode=file_transactions.MODE_HARDLINK) + for src, dst in prepared["transfers"]: + # todo: add support for hardlink transfers + file_transactions.add(src, dst) - # todo: simplify this? - representations.append(representation) - published_representations[representation["_id"]] = prepared + prepared_representations.append(prepared) - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed + # Each instance can also have pre-defined transfers not explicitly + # part of a representation - like texture resources used by a + # .ma representation. Those destination paths are pre-defined, etc. + # todo: should we move or simplify this logic? + for src, dst in instance.data.get("transfers", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + for src, dst in instance.data.get("hardlinks", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + + # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() self.log.debug("Backup files " @@ -469,17 +471,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Integrated files " "{}".format(file_transactions.transferred)) - # todo: fix get file info for transferred files per representation - # currently it'd set all files for all representations - # get 'files' info for representation and all attached resources - integrated_files = file_transactions.transferred - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - integrated_files - ) + # Finalize the representations now the published files are integrated + # Get 'files' info for representations and its attached resources + self.log.debug("Retrieving Representation files information ...") + sites = self.compute_resource_sync_sites(instance) + anatomy = instance.context.data["anatomy"] + representations = [] + for prepared in prepared_representations: + transfers = prepared["transfers"] + representation = prepared["representation"] + representation["files"] = self.get_files_info( + transfers, sites, anatomy + ) + representations.append(representation) - # Remove old representations if there are any (before insertion of new) + # Remove all archived representations if archived_repres: repre_ids_to_remove = [repre["_id"] for repre in archived_repres] io.delete_many({"_id": {"$in": repre_ids_to_remove}}) @@ -487,7 +493,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Write the new representations to the database io.insert_many(representations) - instance.data["published_representations"] = published_representations + # Backwards compatibility + # todo: can we avoid the need to store this? + instance.data["published_representations"] = { + p["representation"]["_id"]: p for p in prepared_representations + } self.log.info("Registered {} representations" "".format(len(representations))) @@ -495,7 +505,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def register_version(self, instance, subset): version_number = instance.data["version"] - self.log.debug("Next version: v{}".format(version_number)) + self.log.debug("Version: v{0:03d}".format(version_number)) version_data = self.create_version_data(instance) version_data_instance = instance.data.get('versionData') @@ -565,6 +575,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) version = io.find_one({"_id": version_id}) + + self.log.info("Registered version: v{0:03d}".format(version["name"])) + return version def prepare_representation(self, repre, @@ -585,7 +598,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("transfers"): raise ValueError("Representation is not allowed to have transfers" - "data before integration. " + "data before integration. They are computed in " + "the integrator" "Got: {}".format(repre["transfers"])) # required representation keys @@ -698,18 +712,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_collection.padding = destination_padding assert len(src_collection) == len(dst_collection), "This is a bug" + # Multiple file transfers transfers = [] for src_file_name, dst in zip(src_collection, dst_collection): src = os.path.join(stagingdir, src_file_name) - self.log.debug("source: {}".format(src)) - self.log.debug("destination: `{}`".format(dst)) - transfers.append(src, dst) - - # Store first frame as published path - # todo: remove `published_path` since it can be retrieved from - # `transfers` by taking the first destination transfers[0][1] - repre['published_path'] = next(iter(dst_collection)) - repre["transfers"].extend(transfers) + transfers.append((src, dst)) else: # Single file @@ -728,11 +735,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = os.path.normpath(template_filled) # Single file transfer - self.log.debug("source: {}".format(src)) - self.log.debug("destination: `{}`".format(dst)) - repre["transfers"] = [src, dst] - - repre['published_path'] = dst + transfers = [(src, dst)] if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list @@ -753,11 +756,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_id = _archived_repres["orig_id"] break + # Backwards compatibility: + # Store first transferred destination as published path data + # todo: can we remove this? + published_path = transfers[0][1] + # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database data = repre.get("data") or {} - data.update({'path': repre["published_path"], 'template': template}) + data.update({'path': published_path, 'template': template}) representation = { "_id": repre_id, "schema": "openpype:representation-2.0", @@ -782,9 +790,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return { "representation": representation, "anatomy_data": template_data, - # todo: avoid the need for 'published_files'? + "transfers": transfers, + # todo: avoid the need for 'published_files' used by Integrate Hero # backwards compatibility - "published_files": [transfer[1] for transfer in repre["transfers"]] + "published_files": [transfer[1] for transfer in transfers] } def _get_instance_families(self, instance): @@ -805,6 +814,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: rely less on self.prepare_anatomy to create this value asset = instance.data.get("assetEntity") # stored by prepare_anatomy subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + subset = io.find_one({ "type": "subset", "parent": asset["_id"], @@ -838,6 +849,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): {"$set": {"data.families": families}} ) + self.log.info("Registered subset: {}".format(subset_name)) + return subset def _set_subset_group(self, instance, subset_id): @@ -871,9 +884,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return None + # TODO: Resolve below questions # QUESTION - # - is there a chance that task name is not filled in anatomy - # data? + # - is there a chance that task name is not filled in anatomy data? # - should we use context task in that case? anatomy_data = instance.data["anatomyData"] task_name = None @@ -1002,7 +1015,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, instance): + def get_files_info(self, transfers, sites, anatomy): """ Prepare 'files' portion for attached resources and main asset. Combining records from 'transfers' and 'hardlinks' parts from instance. @@ -1017,21 +1030,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): output_resources: array of dictionaries to be added to 'files' key in representation """ - # todo: refactor to use transfers/hardlinks of representations - # currently broken logic - resources = list(instance.data.get("transfers", [])) - resources.extend(list(instance.data.get("hardlinks", []))) - self.log.debug("get_files_info.resources:{}".format(resources)) - - sites = self.compute_resource_sync_sites(instance) - - output_resources = [] - anatomy = instance.context.data["anatomy"] - for _src, dest in resources: + file_infos = [] + for _src, dest in transfers: file_info = self.prepare_file_info(dest, anatomy, sites=sites) - output_resources.append(file_info) + file_infos.append(file_info) - return output_resources + return file_infos def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) From 8996280224aa30ad800e955ff165bdbe48bb8296 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:38:05 +0100 Subject: [PATCH 0023/1030] Reduce duplicated logic by implementing `resolve_profile` method --- openpype/plugins/publish/integrate_new.py | 107 ++++++++++------------ 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2142920a09..e43afbf7f6 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -359,17 +359,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "short": task_code } - elif "task" in anatomy_data: - # Just set 'task_name' variable to context task - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - - else: - task_name = None - task_type = None - # Fill family in anatomy data - anatomy_data["family"] = instance.data.get("family") + anatomy_data["family"] = self.main_family_from_instance(instance) intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): @@ -378,25 +369,44 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if intent_value: anatomy_data["intent"] = intent_value - # Get profile - key_values = { - "families": self.main_family_from_instance(instance), - "tasks": task_name, - "hosts": instance.context.data["hostName"], - "task_types": task_type - } - profile = filter_profiles( - self.template_name_profiles, - key_values, - logger=self.log - ) - + profile, _ = self.resolve_profile(self.template_name_profiles, + instance) template_name = "publish" if profile: template_name = profile["template_name"] return template_name, anatomy_data + def resolve_profile(self, profiles, instance): + """Resolve profile by family, task name, host name and task type""" + + # Anatomy data is pre-filled by Collectors and `self.prepare_anatomy` + anatomy_data = instance.data["anatomyData"] + + # TODO: Resolve below questions + # QUESTION + # - is there a chance that task name is not filled in anatomy data? + # - should we use context task in that case? + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + filter_criteria = { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["host"], + "task_types": task.get("type") + } + # Get profile + profile = filter_profiles( + profiles, + filter_criteria, + logger=self.log + ) + + # TODO: See if we can simplify to avoid needing to return filter + # criteria used in `self._get_subset_group` + return profile, filter_criteria + def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") @@ -886,50 +896,29 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return None - # TODO: Resolve below questions - # QUESTION - # - is there a chance that task name is not filled in anatomy data? - # - should we use context task in that case? - anatomy_data = instance.data["anatomyData"] - task_name = None - task_type = None - if "task" in anatomy_data: - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - filtering_criteria = { - "families": instance.data["family"], - "hosts": instance.context.data["hostName"], - "tasks": task_name, - "task_types": task_type - } - matching_profile = filter_profiles( - self.subset_grouping_profiles, - filtering_criteria - ) - # Skip if there is not matching profile - if not matching_profile: + # Skip if there is no matching profile + profile, criteria = self.resolve_profile(self.subset_grouping_profiles, + instance) + if not profile: return None - filled_template = None - template = matching_profile["template"] - fill_pairs = ( - ("family", filtering_criteria["families"]), - ("task", filtering_criteria["tasks"]), - ("host", filtering_criteria["hosts"]), - ("subset", instance.data["subset"]), - ("renderlayer", instance.data.get("renderlayer")) - ) - fill_pairs = prepare_template_data(fill_pairs) + template = profile["template"] + fill_pairs = prepare_template_data({ + "family": criteria["families"], + "task": criteria["tasks"], + "host": criteria["hosts"], + "subset": instance.data["subset"], + "renderlayer": instance.data.get("renderlayer") + }) + + filled_template = None try: filled_template = StringTemplate.format_strict_template( template, fill_pairs ) except (KeyError, TemplateUnsolved): - keys = [] - if fill_pairs: - keys = fill_pairs.keys() - + keys = fill_pairs.keys() msg = "Subset grouping failed. " \ "Only {} are expected in Settings".format(','.join(keys)) self.log.warning(msg) From 177e244bd80bf0b1d472948cba45f40dfecd672e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:45:24 +0100 Subject: [PATCH 0024/1030] Remove prepare anatomy data logic that is already collected/generated in CollectAnatomyContextData and CollectAnatomyInstanceData. This currently was duplicated logic and should not be handled in the Integrator --- openpype/plugins/publish/integrate_new.py | 51 +---------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e43afbf7f6..a1a116bd43 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -310,58 +310,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" - context = instance.context - anatomy_data = instance.data["anatomyData"] - project_entity = instance.data["projectEntity"] - - context_asset_name = None - context_asset_doc = context.data.get("assetEntity") - if context_asset_doc: - context_asset_name = context_asset_doc["name"] - - asset_name = instance.data["asset"] - asset_entity = instance.data.get("assetEntity") - if not asset_entity or asset_entity["name"] != context_asset_name: - asset_entity = io.find_one({ - "type": "asset", - "name": asset_name, - "parent": project_entity["_id"] - }) - assert asset_entity, ( - "No asset found by the name \"{0}\" in project \"{1}\"" - ).format(asset_name, project_entity["name"]) - - instance.data["assetEntity"] = asset_entity - - # update anatomy data with asset specific keys - # - name should already been set - hierarchy = "" - parents = asset_entity["data"]["parents"] - if parents: - hierarchy = "/".join(parents) - anatomy_data["hierarchy"] = hierarchy - - # Make sure task name in anatomy data is same as on instance.data - asset_tasks = ( - asset_entity.get("data", {}).get("tasks") - ) or {} - task_name = instance.data.get("task") - if task_name: - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") - anatomy_data["task"] = { - "name": task_name, - "type": task_type, - "short": task_code - } - - # Fill family in anatomy data - anatomy_data["family"] = self.main_family_from_instance(instance) + # TODO: This logic should move to CollectAnatomyContextData intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") From 3fd2d020149e5b33c0be0ab7000376a0f30ed96f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:55:40 +0100 Subject: [PATCH 0025/1030] Move logic to clarify what should be removed/moved and bring logic closer to where it's used --- openpype/plugins/publish/integrate_new.py | 36 ++++++++++------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a1a116bd43..e57fbaf294 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -293,6 +293,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return + # TODO: Avoid the need to do any adjustments to anatomy data + # Best case scenario that's all handled by collectors + self.prepare_anatomy(instance) + file_transactions = FileTransaction(log=self.log) try: self.register(instance, file_transactions) @@ -309,24 +313,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" - - anatomy_data = instance.data["anatomyData"] - # TODO: This logic should move to CollectAnatomyContextData intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") - - if intent_value: - anatomy_data["intent"] = intent_value - - profile, _ = self.resolve_profile(self.template_name_profiles, - instance) - template_name = "publish" - if profile: - template_name = profile["template_name"] - - return template_name, anatomy_data + if intent_value: + instance.data["anatomyData"]["intent"] = intent_value def resolve_profile(self, profiles, instance): """Resolve profile by family, task name, host name and task type""" @@ -382,6 +374,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) + # Define publish template name from profiles + profile, _ = self.resolve_profile(self.template_name_profiles, + instance) + template_name = "publish" + if profile: + template_name = profile["template_name"] + subset = self.register_subset(instance) version = self.register_version(instance, subset) @@ -393,7 +392,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): })) # Prepare all representations - template_name, anatomy_data = self.prepare_anatomy(instance) prepared_representations = [] for repre in instance.data["representations"]: @@ -404,7 +402,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, - anatomy_data, template_name, archived_repres, version, @@ -544,16 +541,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return version def prepare_representation(self, repre, - anatomy_data, template_name, archived_repres, version, instance_stagingdir, instance): - # create template data for Anatomy - template_data = copy.deepcopy(anatomy_data) - # pre-flight validations if repre["ext"].startswith("."): raise ValueError("Extension must not start with a dot '.': " @@ -565,6 +558,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "the integrator" "Got: {}".format(repre["transfers"])) + # create template data for Anatomy + template_data = copy.deepcopy(instance.data["anatomyData"]) + # required representation keys files = repre['files'] template_data["representation"] = repre["name"] From 8edfb3f7d3f926539f7f060725b3b7e0b1d697e5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:10:59 +0100 Subject: [PATCH 0026/1030] Simplify profile filtering --- openpype/plugins/publish/integrate_new.py | 42 +++++++++-------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e57fbaf294..bdc045d1db 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -320,35 +320,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if intent_value: instance.data["anatomyData"]["intent"] = intent_value - def resolve_profile(self, profiles, instance): - """Resolve profile by family, task name, host name and task type""" - - # Anatomy data is pre-filled by Collectors and `self.prepare_anatomy` + 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"] - # TODO: Resolve below questions - # QUESTION - # - is there a chance that task name is not filled in anatomy data? - # - should we use context task in that case? # Task can be optional in anatomy data task = anatomy_data.get("task", {}) - filter_criteria = { + # Return filter criteria + return { "families": anatomy_data["family"], "tasks": task.get("name"), "hosts": anatomy_data["host"], "task_types": task.get("type") } - # Get profile - profile = filter_profiles( - profiles, - filter_criteria, - logger=self.log - ) - - # TODO: See if we can simplify to avoid needing to return filter - # criteria used in `self._get_subset_group` - return profile, filter_criteria def register(self, instance, file_transactions): @@ -375,8 +361,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) # Define publish template name from profiles - profile, _ = self.resolve_profile(self.template_name_profiles, - instance) + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) template_name = "publish" if profile: template_name = profile["template_name"] @@ -844,17 +832,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return None # Skip if there is no matching profile - profile, criteria = self.resolve_profile(self.subset_grouping_profiles, - instance) + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.subset_grouping_profiles, + filter_criteria, + logger=self.log) if not profile: return None template = profile["template"] fill_pairs = prepare_template_data({ - "family": criteria["families"], - "task": criteria["tasks"], - "host": criteria["hosts"], + "family": filter_criteria["families"], + "task": filter_criteria["tasks"], + "host": filter_criteria["hosts"], "subset": instance.data["subset"], "renderlayer": instance.data.get("renderlayer") }) From 79286ead4b91504afa30df711e8f751451f53552 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:16:32 +0100 Subject: [PATCH 0027/1030] Re-use get families logic --- openpype/plugins/publish/integrate_new.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bdc045d1db..e66a71c483 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -286,10 +286,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): # Exclude instances that also contain families from exclude families - families = set( - # Consider family and families data - [instance.data["family"]] + instance.data.get("families", []) - ) + families = set(self._get_instance_families(instance)) if families & set(self.exclude_families): return From d6c682723de6eb025b21768ced54b2756373fba6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:19:16 +0100 Subject: [PATCH 0028/1030] Remove todo since assetEntity already comes from Collectors + re-use families variable --- openpype/plugins/publish/integrate_new.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e66a71c483..856f8af163 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -755,8 +755,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return families def register_subset(self, instance): - # todo: rely less on self.prepare_anatomy to create this value - asset = instance.data.get("assetEntity") # stored by prepare_anatomy + asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) @@ -766,9 +765,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "name": subset_name }) + families = self._get_instance_families(instance) if subset is None: self.log.info("Subset '%s' not found, creating ..." % subset_name) - families = self._get_instance_families(instance) _id = io.insert_one({ "schema": "openpype:subset-3.0", @@ -786,8 +785,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self._set_subset_group(instance, subset["_id"]) # Update families on subset. - families = [instance.data["family"]] - families.extend(instance.data.get("families", [])) io.update_many( {"type": "subset", "_id": ObjectId(subset["_id"])}, {"$set": {"data.families": families}} From 47259f8ef7b177892c76a8dbfde6d147cf664d39 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:21:44 +0100 Subject: [PATCH 0029/1030] Add todo to move get subset group logic --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 856f8af163..91d2f3a943 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -821,6 +821,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Attribute 'subset_grouping_profiles' is defined by OpenPype settings. """ + # TODO: This logic is better suited for a Collector to just store + # instance.data["subsetGroup"] # Skip if 'subset_grouping_profiles' is empty if not self.subset_grouping_profiles: return None From b128e0addffc77991e5ff25f2d219d8ed8613136 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:21:32 +0100 Subject: [PATCH 0030/1030] Override stored repre context `udim` for backwards compatibility --- openpype/plugins/publish/integrate_new.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 91d2f3a943..e3abb8f04f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -275,7 +275,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username", "frame", "udim" + "family", "hierarchy", "task", "username", "frame" ] default_template_name = "publish" @@ -681,15 +681,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Single file transfer transfers = [(src, dst)] - if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list - for key in self.db_representation_context_keys: value = template_data.get(key) if not value: continue repre_context[key] = template_data[key] + # Explicitly store the full list even though template data might + # have a different value + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + # Define representation id repre_id = ObjectId() From 9997acbbeae32f1473c39df6cf78a8bfa7257aff Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:22:49 +0100 Subject: [PATCH 0031/1030] Encapsulate version data completely into its own function --- openpype/plugins/publish/integrate_new.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e3abb8f04f..6e92f81b8b 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -452,17 +452,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version_number = instance.data["version"] self.log.debug("Version: v{0:03d}".format(version_number)) - version_data = self.create_version_data(instance) - version_data_instance = instance.data.get('versionData') - if version_data_instance: - version_data.update(version_data_instance) - version = { "schema": "openpype:version-3.0", "type": "version", "parent": subset["_id"], "name": version_number, - "data": version_data + "data": self.create_version_data(instance) } repres = instance.data.get("representations", []) @@ -909,6 +904,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if key in instance.data: version_data[key] = instance.data[key] + # Include instance.data[versionData] directly + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + return version_data def main_family_from_instance(self, instance): From 5b1f6eb30c459011fa685dcf325f39c4af72838e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:23:27 +0100 Subject: [PATCH 0032/1030] Move logic closer to where it's used --- openpype/plugins/publish/integrate_new.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e92f81b8b..a787f8d50d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -460,9 +460,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": self.create_version_data(instance) } - repres = instance.data.get("representations", []) - new_repre_names_low = [_repre["name"].lower() for _repre in repres] - existing_version = io.find_one({ 'type': 'version', 'parent': subset["_id"], @@ -488,6 +485,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): })) # Find representations of existing version and archive them + repres = instance.data.get("representations", []) + new_repre_names_low = [_repre["name"].lower() for _repre in repres] current_repres = io.find({ "type": "representation", "parent": version_id From 3369c15bdf837d6d8e83a8c054794e95fccd061b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:25:15 +0100 Subject: [PATCH 0033/1030] Preparation to delay Version document write to database closer to representation write --- openpype/plugins/publish/integrate_new.py | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a787f8d50d..8dd2d57959 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -466,15 +466,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'name': version_number }) + bulk_writes = [] if existing_version is None: self.log.debug("Creating new version ...") - version_id = io.insert_one(version).inserted_id + version["_id"] = ObjectId() + bulk_writes.append(InsertOne(version)) else: self.log.debug("Updating existing version ...") # Check if instance have set `append` mode which cause that # only replicated representations are set to archive append_repres = instance.data.get("append", False) - bulk_writes = [] # Update version data version_id = existing_version['_id'] @@ -484,6 +485,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): '$set': version })) + # Instead of directly writing and querying we reproduce what + # the resulting version would look like so we can hold off making + # changes to the database to avoid the need for 'rollback' + version = copy.deepcopy(version) + version["_id"] = existing_version["_id"] + # Find representations of existing version and archive them repres = instance.data.get("representations", []) new_repre_names_low = [_repre["name"].lower() for _repre in repres] @@ -507,13 +514,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre["type"] = "archived_representation" bulk_writes.append(InsertOne(repre)) - # bulk updates - if bulk_writes: - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - bulk_writes - ) - - version = io.find_one({"_id": version_id}) + # bulk updates + # todo: Try to avoid writing already until after we've prepared + # representations to allow easier rollback? + io._database[io.Session["AVALON_PROJECT"]].bulk_write( + bulk_writes + ) self.log.info("Registered version: v{0:03d}".format(version["name"])) From 42175ff6f829ce30ef61538243d7bd4b804c8e28 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:41:56 +0100 Subject: [PATCH 0034/1030] Fix `get_profile_filter_criteria` anatomy data key for app name --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 8dd2d57959..e3dcfcc93c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -329,7 +329,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["host"], + "hosts": anatomy_data["app"], "task_types": task.get("type") } From 7713af5a1dac4b0080dc6006f08811dcd9fc9d04 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 17:23:02 +0100 Subject: [PATCH 0035/1030] Fix sequence functionality --- openpype/plugins/publish/integrate_new.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e3dcfcc93c..b5986a62ee 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -645,16 +645,20 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_context = template_filled.used_values self.log.debug("Template filled: {}".format(str(template_filled))) dst_collections, _remainder = clique.assemble( - [os.path.normpath(template_filled)], minimum_items=1 + [os.path.normpath(template_filled)], + minimum_items=1, + patterns=[clique.PATTERNS["frames"]] ) assert not _remainder, "This is a bug" assert len(dst_collections) == 1, "This is a bug" dst_collection = dst_collections[0] # Update the destination indexes and padding - dst_collection.indexes = destination_indexes + dst_collection.indexes.clear() + dst_collection.indexes.update(set(destination_indexes)) dst_collection.padding = destination_padding - assert len(src_collection) == len(dst_collection), "This is a bug" + assert len(src_collection.indexes) == \ + len(dst_collection.indexes), "This is a bug" # Multiple file transfers transfers = [] From 229626bffdbc7e59c2206798b5bb3066a5602228 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 17:36:01 +0100 Subject: [PATCH 0036/1030] Reformat code --- openpype/plugins/publish/integrate_new.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index b5986a62ee..9e3e9de77c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -657,8 +657,9 @@ class IntegrateAssetNew(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" + assert ( + len(src_collection.indexes) == len(dst_collection.indexes) + ), "This is a bug" # Multiple file transfers transfers = [] From e1eb0887e0bdaaf012e95f289bdaddcf9089d65c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 20:26:10 +0100 Subject: [PATCH 0037/1030] Reduce database calls for register subset + prepare for bulk writes logic --- openpype/plugins/publish/integrate_new.py | 72 ++++++++++------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 9e3e9de77c..44768df368 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -766,63 +766,53 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) + # Get existing subset if it exists subset = io.find_one({ "type": "subset", "parent": asset["_id"], "name": subset_name }) - families = self._get_instance_families(instance) - if subset is None: - self.log.info("Subset '%s' not found, creating ..." % subset_name) + # Define subset data + data = { + "families": self._get_instance_families(instance) + } - _id = io.insert_one({ + subset_group = instance.data.get("subsetGroup") + if not subset_group: + # todo: move _get_subset_group fallback to its own collector + subset_group = self._get_subset_group(instance) + if subset_group: + data["subsetGroup"] = subset_group + + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), "schema": "openpype:subset-3.0", "type": "subset", "name": subset_name, - "data": { - "families": families - }, + "data": data, "parent": asset["_id"] - }).inserted_id + } + io.insert_one(subset) - subset = io.find_one({"_id": _id}) - - # Update subset group - self._set_subset_group(instance, subset["_id"]) - - # Update families on subset. - io.update_many( - {"type": "subset", "_id": ObjectId(subset["_id"])}, - {"$set": {"data.families": families}} - ) + 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["data"].update(data) + io.update_many( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + ) self.log.info("Registered subset: {}".format(subset_name)) - return subset - def _set_subset_group(self, instance, subset_id): - """ - Mark subset as belonging to group in DB. - - Uses Settings > Global > Publish plugins > IntegrateAssetNew - - Args: - instance (dict): processed instance - subset_id (str): DB's subset _id - - """ - # Fist look into instance data - subset_group = instance.data.get("subsetGroup") - if not subset_group: - subset_group = self._get_subset_group(instance) - - if subset_group: - io.update_many({ - 'type': 'subset', - '_id': ObjectId(subset_id) - }, {'$set': {'data.subsetGroup': subset_group}}) - def _get_subset_group(self, instance): """Look into subset group profiles set by settings. From b906365f593025bf7bbba67ea6d8a907b717c98e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Mar 2022 21:42:39 +0100 Subject: [PATCH 0038/1030] Separate site sync logic further from Integrator plug-in (Draft) --- openpype/plugins/publish/integrate_new.py | 154 ++++++++++++---------- 1 file changed, 88 insertions(+), 66 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 44768df368..138a4fcc06 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -419,7 +419,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources self.log.debug("Retrieving Representation files information ...") - sites = self.compute_resource_sync_sites(instance) + sites = SiteSync.compute_resource_sync_sites( + system_settings=instance.context.data["system_settings"], + project_settings=instance.context.data["project_settings"] + ) + log.debug("final sites:: {}".format(sites)) + anatomy = instance.context.data["anatomy"] representations = [] for prepared in prepared_representations: @@ -987,63 +992,65 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "sites": sites } - # region sync sites - def compute_resource_sync_sites(self, instance): + +class SiteSync(object): + """Logic for Site Sync Module functionality""" + + @classmethod + def compute_resource_sync_sites(cls, + system_settings, + project_settings): """Get available resource sync sites""" - # Sync server logic - # TODO: Clean up sync settings - local_site = 'studio' # default - remote_site = None - always_accessible = [] - sync_project_presets = None - system_sync_server_presets = ( - instance.context.data["system_settings"] - ["modules"] - ["sync_server"]) + def create_metadata(name, created=True): + """Create sync site metadata for site with `name`""" + metadata = {"name": name} + if created: + metadata["created_dt"] = datetime.now() + return metadata + + default_sites = [create_metadata("studio")] + + # If sync site module is disabled return default fallback site + system_sync_server_presets = system_settings["modules"]["sync_server"] log.debug("system_sett:: {}".format(system_sync_server_presets)) + if not system_sync_server_presets["enabled"]: + return default_sites - if system_sync_server_presets["enabled"]: - sync_project_presets = ( - instance.context.data["project_settings"] - ["global"] - ["sync_server"]) + # If sync site module is disabled in current + # project return default fallback site + sync_project_presets = project_settings["global"]["sync_server"] + if not sync_project_presets["enabled"]: + return default_sites - if sync_project_presets and sync_project_presets["enabled"]: - local_site, remote_site = self._get_sites(sync_project_presets) - always_accessible = sync_project_presets["config"]. \ - get("always_accessible_on", []) + local_site, remote_site = cls._get_sites(sync_project_presets) - already_attached_sites = {} - meta = {"name": local_site, "created_dt": datetime.now()} - sites = [meta] - already_attached_sites[meta["name"]] = meta["created_dt"] + # Attached sites metadata by site name + # That is the local site, remote site, the always accesible sites + # and their alternate sites (alias of sites with different protocol) + attached_sites = dict() + attached_sites[local_site] = create_metadata(local_site) - if sync_project_presets and sync_project_presets["enabled"]: - if remote_site and \ - remote_site not in already_attached_sites.keys(): - # add remote - meta = {"name": remote_site.strip()} - sites.append(meta) - already_attached_sites[meta["name"]] = None + if remote_site and remote_site != local_site: + attached_sites[remote_site] = create_metadata(remote_site, + created=False) - # add skeleton for site where it should be always synced to - for always_on_site in always_accessible: - if always_on_site not in already_attached_sites.keys(): - meta = {"name": always_on_site.strip()} - sites.append(meta) - already_attached_sites[meta["name"]] = None + # add skeleton for sites where it should be always synced to + always_accessible_sites = ( + sync_project_presets["config"].get("always_accessible_on", []) + ) + for site in always_accessible_sites: + site = site.strip() + if site not in attached_sites: + attached_sites[site] = create_metadata(site, created=False) - # add alternative sites - alt = self._add_alternative_sites(system_sync_server_presets, - already_attached_sites) - sites.extend(alt) + # add alternative sites + cls._add_alternative_sites(system_sync_server_presets, attached_sites) - log.debug("final sites:: {}".format(sites)) + return list(attached_sites.values()) - return sites - - def _get_sites(self, sync_project_presets): + @staticmethod + def _get_sites(sync_project_presets): """Returns tuple (local_site, remote_site)""" local_site_id = openpype.api.get_local_site_id() local_site = sync_project_presets["config"]. \ @@ -1053,36 +1060,51 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): local_site = local_site_id remote_site = sync_project_presets["config"].get("remote_site") + if remote_site: + remote_site.strip() if remote_site == 'local': remote_site = local_site_id return local_site, remote_site - def _add_alternative_sites(self, - system_sync_server_presets, - already_attached_sites): + @staticmethod + def _add_alternative_sites(system_sync_server_presets, + attached_sites): """Loop through all configured sites and add alternatives. + For all sites if an alternative site is detected that has an + accessible site then we can also register to that alternative site + with the same "created" state. So we match the existing data. + See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) - alternative_sites = [] for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - already_attached_keys = list(already_attached_sites.keys()) - for added_site in already_attached_keys: - if added_site in alt_sites: - if site_name in already_attached_keys: - continue - meta = {"name": site_name} - real_created = already_attached_sites[added_site] - # alt site inherits state of 'created_dt' - if real_created: - meta["created_dt"] = real_created - alternative_sites.append(meta) - already_attached_sites[meta["name"]] = real_created - return alternative_sites - # endregion + # Skip if already defined + if site_name in attached_sites: + continue + + # Get alternate sites (stripped names) for this site name + alt_sites = site_info.get("alternative_sites", []) + alt_sites = [site.strip() for site in alt_sites] + alt_sites = set(alt_sites) + + # If no alternative sites we don't need to add + if not alt_sites: + continue + + # Take a copy of data of the first alternate site that is already + # defined as an attached site to match the same state. + match_meta = next((attached_sites[site] for site in alt_sites + if site in attached_sites), None) + if not match_meta: + continue + + alt_site_meta = copy.deepcopy(match_meta) + alt_site_meta["name"] = site_name + + # Note: We change mutable `attached_site` dict in-place + attached_sites[site_name] = alt_site_meta From e0aaa5f6cc2fd2a2e6fa708364136d9d6235163d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:20:13 +0100 Subject: [PATCH 0039/1030] Move FileTransaction into lib --- openpype/lib/file_transaction.py | 171 ++++++++++++++++++++++ openpype/plugins/publish/integrate_new.py | 167 +-------------------- 2 files changed, 172 insertions(+), 166 deletions(-) create mode 100644 openpype/lib/file_transaction.py diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py new file mode 100644 index 0000000000..57592e297f --- /dev/null +++ b/openpype/lib/file_transaction.py @@ -0,0 +1,171 @@ +import os +import logging +import sys +import errno +import six + +from openpype.lib import create_hard_link + +# this is needed until speedcopy for linux is fixed +if sys.platform == "win32": + from speedcopy import copyfile +else: + from shutil import copyfile + + +class FileTransaction(object): + """ + + The file transaction is a three step process. + + 1) Rename any existing files to a "temporary backup" during `process()` + 2) Copy the files to final destination during `process()` + 3) Remove any backed up files (*no rollback possible!) during `finalize()` + + Step 3 is done during `finalize()`. If not called the .bak files will + remain on disk. + + These steps try to ensure that we don't overwrite half of any existing + files e.g. if they are currently in use. + + Note: + A regular filesystem is *not* a transactional file system and even + though this implementation tries to produce a 'safe copy' with a + potential rollback do keep in mind that it's inherently unsafe due + to how filesystem works and a myriad of things could happen during + the transaction that break the logic. A file storage could go down, + permissions could be changed, other machines could be moving or writing + files. A lot can happen. + + Warning: + Any folders created during the transfer will not be removed. + + """ + + MODE_COPY = 0 + MODE_HARDLINK = 1 + + def __init__(self, log=None): + + if log is None: + log = logging.getLogger("FileTransaction") + + self.log = log + + # The transfer queue + # todo: make this an actual FIFO queue? + self._transfers = {} + + # Destination file paths that a file was transferred to + self._transferred = [] + + # Backup file location mapping to original locations + self._backup_to_original = {} + + def add(self, src, dst, mode=MODE_COPY): + """Add a new file to transfer queue""" + opts = {"mode": mode} + + src = os.path.normpath(src) + dst = os.path.normpath(dst) + + if dst in self._transfers: + queued_src = self._transfers[dst][0] + if src == queued_src: + self.log.debug("File transfer was already " + "in queue: {} -> {}".format(src, dst)) + return + else: + self.log.warning("File transfer in queue replaced..") + self.log.debug("Removed from queue: " + "{} -> {}".format(queued_src, dst)) + self.log.debug("Added to queue: {} -> {}".format(src, dst)) + + self._transfers[dst] = (src, opts) + + def process(self): + + # Backup any existing files + for dst in self._transfers.keys(): + if os.path.exists(dst): + # Backup original file + # todo: add timestamp or uuid to ensure unique + backup = dst + ".bak" + self._backup_to_original[backup] = dst + self.log.debug("Backup existing file: " + "{} -> {}".format(dst, backup)) + os.rename(dst, backup) + + # Copy the files to transfer + for dst, (src, opts) in self._transfers.items(): + self._create_folder_for_file(dst) + + if opts["mode"] == self.MODE_COPY: + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + elif opts["mode"] == self.MODE_HARDLINK: + self.log.debug("Hardlinking file ... {} -> {}".format(src, + dst)) + create_hard_link(src, dst) + + self._transferred.append(dst) + + def finalize(self): + # Delete any backed up files + for backup in self._backup_to_original.keys(): + try: + os.remove(backup) + except OSError: + self.log.error("Failed to remove backup file: " + "{}".format(backup), + exc_info=True) + + def rollback(self): + + errors = 0 + + # Rollback any transferred files + for path in self._transferred: + try: + os.remove(path) + except OSError: + errors += 1 + self.log.error("Failed to rollback created file: " + "{}".format(path), + exc_info=True) + + # Rollback the backups + for backup, original in self._backup_to_original.items(): + try: + os.rename(backup, original) + except OSError: + errors += 1 + self.log.error("Failed to restore original file: " + "{} -> {}".format(backup, original), + exc_info=True) + + if errors: + self.log.error("{} errors occurred during " + "rollback.".format(errors), exc_info=True) + six.reraise(*sys.exc_info()) + + @property + def transferred(self): + """Return the processed transfers destination paths""" + return list(self._transferred) + + @property + def backups(self): + """Return the backup file paths""" + return list(self._backup_to_original.keys()) + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 138a4fcc06..92976e6151 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -3,7 +3,6 @@ import logging import sys import copy import clique -import errno import six from bson.objectid import ObjectId @@ -13,19 +12,13 @@ from avalon import io import openpype.api from datetime import datetime from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib.file_transaction import FileTransaction from openpype.lib import ( prepare_template_data, - create_hard_link, StringTemplate, TemplateUnsolved ) -# this is needed until speedcopy for linux is fixed -if sys.platform == "win32": - from speedcopy import copyfile -else: - from shutil import copyfile - log = logging.getLogger(__name__) @@ -40,164 +33,6 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) -class FileTransaction(object): - """ - - The file transaction is a three step process. - - 1) Rename any existing files to a "temporary backup" during `process()` - 2) Copy the files to final destination during `process()` - 3) Remove any backed up files (*no rollback possible!) during `finalize()` - - Step 3 is done during `finalize()`. If not called the .bak files will - remain on disk. - - These steps try to ensure that we don't overwrite half of any existing - files e.g. if they are currently in use. - - Note: - A regular filesystem is *not* a transactional file system and even - though this implementation tries to produce a 'safe copy' with a - potential rollback do keep in mind that it's inherently unsafe due - to how filesystem works and a myriad of things could happen during - the transaction that break the logic. A file storage could go down, - permissions could be changed, other machines could be moving or writing - files. A lot can happen. - - Warning: - Any folders created during the transfer will not be removed. - - """ - - MODE_COPY = 0 - MODE_HARDLINK = 1 - - def __init__(self, log=None): - - if log is None: - log = logging.getLogger("FileTransaction") - - self.log = log - - # The transfer queue - # todo: make this an actual FIFO queue? - self._transfers = {} - - # Destination file paths that a file was transferred to - self._transferred = [] - - # Backup file location mapping to original locations - self._backup_to_original = {} - - def add(self, src, dst, mode=MODE_COPY): - """Add a new file to transfer queue""" - opts = {"mode": mode} - - src = os.path.normpath(src) - dst = os.path.normpath(dst) - - if dst in self._transfers: - queued_src = self._transfers[dst][0] - if src == queued_src: - self.log.debug("File transfer was already " - "in queue: {} -> {}".format(src, dst)) - return - else: - self.log.warning("File transfer in queue replaced..") - self.log.debug("Removed from queue: " - "{} -> {}".format(queued_src, dst)) - self.log.debug("Added to queue: {} -> {}".format(src, dst)) - - self._transfers[dst] = (src, opts) - - def process(self): - - # Backup any existing files - for dst in self._transfers.keys(): - if os.path.exists(dst): - # Backup original file - # todo: add timestamp or uuid to ensure unique - backup = dst + ".bak" - self._backup_to_original[backup] = dst - self.log.debug("Backup existing file: " - "{} -> {}".format(dst, backup)) - os.rename(dst, backup) - - # Copy the files to transfer - for dst, (src, opts) in self._transfers.items(): - self._create_folder_for_file(dst) - - if opts["mode"] == self.MODE_COPY: - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - copyfile(src, dst) - elif opts["mode"] == self.MODE_HARDLINK: - self.log.debug("Hardlinking file ... {} -> {}".format(src, - dst)) - create_hard_link(src, dst) - - self._transferred.append(dst) - - def finalize(self): - # Delete any backed up files - for backup in self._backup_to_original.keys(): - try: - os.remove(backup) - except OSError: - self.log.error("Failed to remove backup file: " - "{}".format(backup), - exc_info=True) - - def rollback(self): - - errors = 0 - - # Rollback any transferred files - for path in self._transferred: - try: - os.remove(path) - except OSError: - errors += 1 - self.log.error("Failed to rollback created file: " - "{}".format(path), - exc_info=True) - - # Rollback the backups - for backup, original in self._backup_to_original.items(): - try: - os.rename(backup, original) - except OSError: - errors += 1 - self.log.error("Failed to restore original file: " - "{} -> {}".format(backup, original), - exc_info=True) - - if errors: - self.log.error("{} errors occurred during " - "rollback.".format(errors), exc_info=True) - six.reraise(*sys.exc_info()) - - @property - def transferred(self): - """Return the processed transfers destination paths""" - return list(self._transferred) - - @property - def backups(self): - """Return the backup file paths""" - return list(self._backup_to_original.keys()) - - def _create_folder_for_file(self, path): - dirname = os.path.dirname(path) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues From d3cb32ebe1df79408ff03fddef4d74a55fa1f4b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:32:34 +0100 Subject: [PATCH 0040/1030] Collect subset group in a Collector instead of during Integrator --- .../plugins/publish/collect_subset_group.py | 100 ++++++++++++++++++ openpype/plugins/publish/integrate_new.py | 50 --------- 2 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 openpype/plugins/publish/collect_subset_group.py diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py new file mode 100644 index 0000000000..60c1c04e70 --- /dev/null +++ b/openpype/plugins/publish/collect_subset_group.py @@ -0,0 +1,100 @@ +"""Produces instance.data["subsetGroup"] data used during integration. + +Requires: + dict -> context["anatomyData"] *(pyblish.api.CollectorOrder + 0.49) + +Provides: + instance -> subsetGroup (str) + +""" +import pyblish.api + +from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib import ( + prepare_template_data, + StringTemplate, + TemplateUnsolved +) + + +class CollectSubsetGroup(pyblish.api.ContextPlugin): + """Collect Subset Group for publish.""" + + # Run after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.495 + label = "Collect Subset Group" + + def process(self, instance): + """Look into subset group profiles set by settings. + + Attribute 'subset_grouping_profiles' is defined by OpenPype settings. + """ + + # TODO: Move this setting to this Collector instead of Integrator + project_settings = instance.context.data["project_settings"] + subset_grouping_profiles = ( + project_settings["global"] + ["publish"] + ["IntegrateAssetNew"] + ["subset_grouping_profiles"] + ) + + # Skip if 'subset_grouping_profiles' is empty + if not subset_grouping_profiles: + return + + # Skip if there is no matching profile + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(subset_grouping_profiles, + filter_criteria, + logger=self.log) + if not profile: + return + + if instance.data.get("subsetGroup"): + # If subsetGroup is already set then allow that value to remain + self.log.debug("Skipping collect subset group due to existing " + "value: {}".format(instance.data["subsetGroup"])) + return + + template = profile["template"] + + fill_pairs = prepare_template_data({ + "family": filter_criteria["families"], + "task": filter_criteria["tasks"], + "host": filter_criteria["hosts"], + "subset": instance.data["subset"], + "renderlayer": instance.data.get("renderlayer") + }) + + filled_template = None + try: + filled_template = StringTemplate.format_strict_template( + template, fill_pairs + ) + except (KeyError, TemplateUnsolved): + keys = fill_pairs.keys() + msg = "Subset grouping failed. " \ + "Only {} are expected in Settings".format(','.join(keys)) + self.log.warning(msg) + + if filled_template: + instance.data["subsetGroup"] = filled_template + + def get_profile_filter_criteria(self, instance): + """Return filter criteria for `filter_profiles`""" + # TODO: This logic is used in much more plug-ins in one way or another + # Maybe better suited for lib? + # Anatomy data is pre-filled by Collectors + anatomy_data = instance.data["anatomyData"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 92976e6151..284e110916 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -13,11 +13,6 @@ import openpype.api from datetime import datetime from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction -from openpype.lib import ( - prepare_template_data, - StringTemplate, - TemplateUnsolved -) log = logging.getLogger(__name__) @@ -619,9 +614,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): } subset_group = instance.data.get("subsetGroup") - if not subset_group: - # todo: move _get_subset_group fallback to its own collector - subset_group = self._get_subset_group(instance) if subset_group: data["subsetGroup"] = subset_group @@ -653,48 +645,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("Registered subset: {}".format(subset_name)) return subset - def _get_subset_group(self, instance): - """Look into subset group profiles set by settings. - - Attribute 'subset_grouping_profiles' is defined by OpenPype settings. - """ - # TODO: This logic is better suited for a Collector to just store - # instance.data["subsetGroup"] - # Skip if 'subset_grouping_profiles' is empty - if not self.subset_grouping_profiles: - return None - - # Skip if there is no matching profile - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.subset_grouping_profiles, - filter_criteria, - logger=self.log) - if not profile: - return None - - template = profile["template"] - - fill_pairs = prepare_template_data({ - "family": filter_criteria["families"], - "task": filter_criteria["tasks"], - "host": filter_criteria["hosts"], - "subset": instance.data["subset"], - "renderlayer": instance.data.get("renderlayer") - }) - - filled_template = None - try: - filled_template = StringTemplate.format_strict_template( - template, fill_pairs - ) - except (KeyError, TemplateUnsolved): - keys = fill_pairs.keys() - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) - - return filled_template - def create_version_data(self, instance): """Create the data collection for the version From d7c5ad1f7c9913a39b43087cebbbee7971844f8c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:33:37 +0100 Subject: [PATCH 0041/1030] Remove duplicate "source" in families --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 284e110916..08088479d0 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -86,7 +86,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", - "source", "assembly", "fbx", "textures", From 8fffc60b5016d63d6fad2b8c3b399537a3736171 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:37:23 +0100 Subject: [PATCH 0042/1030] Move remainder of prepare anatomy data to the Collector --- .../plugins/publish/collect_anatomy_context_data.py | 6 ++++++ openpype/plugins/publish/integrate_new.py | 13 ------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index bd8d9e50c4..346caf6b83 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -91,5 +91,11 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) + intent = context.data.get("intent") + if intent and isinstance(intent, dict): + intent_value = intent.get("value") + if intent_value: + context_data["intent"] = intent_value + self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 08088479d0..f598c540e5 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -119,10 +119,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return - # TODO: Avoid the need to do any adjustments to anatomy data - # Best case scenario that's all handled by collectors - self.prepare_anatomy(instance) - file_transactions = FileTransaction(log=self.log) try: self.register(instance, file_transactions) @@ -137,15 +133,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def prepare_anatomy(self, instance): - """Prepare anatomy data used to define representation destinations""" - # TODO: This logic should move to CollectAnatomyContextData - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") - if intent_value: - instance.data["anatomyData"]["intent"] = intent_value - def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" # Anatomy data is pre-filled by Collectors From 177e83ec8bf55e28ca551affefc4ac775570fe98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:43:00 +0100 Subject: [PATCH 0043/1030] Restore "published_path" backwards compatibility for IntegrateFtrackInstance on Farm --- openpype/plugins/publish/integrate_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f598c540e5..05cbb357e3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -532,6 +532,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Store first transferred destination as published path data # todo: can we remove this? published_path = transfers[0][1] + repre["published_path"] = published_path # Backwards compatibility # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above From 7189954a3c29ca00139a9a50b58606a3c335de04 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:44:19 +0100 Subject: [PATCH 0044/1030] Use `os.path.abspath` instead of `os.path.normpath` when adding a transfer --- openpype/lib/file_transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py index 57592e297f..1626bec6b6 100644 --- a/openpype/lib/file_transaction.py +++ b/openpype/lib/file_transaction.py @@ -66,8 +66,8 @@ class FileTransaction(object): """Add a new file to transfer queue""" opts = {"mode": mode} - src = os.path.normpath(src) - dst = os.path.normpath(dst) + src = os.path.abspath(src) + dst = os.path.abspath(dst) if dst in self._transfers: queued_src = self._transfers[dst][0] From 8f8b578f0ce660b1c8182ad2486aca21ed1828e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 19:58:55 +0100 Subject: [PATCH 0045/1030] Move Subset Grouping Profiles settings to Collect Subset Group - This is moved from the Integrate Asset New settings --- .../plugins/publish/collect_subset_group.py | 16 +-- openpype/plugins/publish/integrate_new.py | 1 - .../defaults/project_settings/global.json | 20 ++-- .../schemas/schema_global_publish.json | 101 ++++++++++-------- 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 60c1c04e70..075699e304 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -24,28 +24,22 @@ class CollectSubsetGroup(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.495 label = "Collect Subset Group" + # Defined in OpenPype settings + subset_grouping_profiles = None + def process(self, instance): """Look into subset group profiles set by settings. Attribute 'subset_grouping_profiles' is defined by OpenPype settings. """ - # TODO: Move this setting to this Collector instead of Integrator - project_settings = instance.context.data["project_settings"] - subset_grouping_profiles = ( - project_settings["global"] - ["publish"] - ["IntegrateAssetNew"] - ["subset_grouping_profiles"] - ) - # Skip if 'subset_grouping_profiles' is empty - if not subset_grouping_profiles: + if not self.subset_grouping_profiles: return # Skip if there is no matching profile filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(subset_grouping_profiles, + profile = filter_profiles(self.subset_grouping_profiles, filter_criteria, logger=self.log) if not profile: diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 05cbb357e3..4706d4d093 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -110,7 +110,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Attributes set by settings template_name_profiles = None - subset_grouping_profiles = None def process(self, instance): diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 30a71b044a..528df111f0 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -20,6 +20,17 @@ ], "skip_hosts_headless_publish": [] }, + "CollectSubsetGroup": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ] + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -193,15 +204,6 @@ "tasks": [], "template_name": "render" } - ], - "subset_grouping_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template": "" - } ] }, "CleanUp": { 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 12043d4205..ab968037f6 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 @@ -39,6 +39,61 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectSubsetGroup", + "label": "Collect Subset Group", + "is_group": true, + "children": [ + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, @@ -603,52 +658,6 @@ } ] } - }, - { - "type": "list", - "key": "subset_grouping_profiles", - "label": "Subset grouping profiles", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template", - "label": "Template" - } - ] - } } ] }, From 6ff7167d54e8a70441300ba4d21acb5a01eb5071 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:09:08 +0100 Subject: [PATCH 0046/1030] Separate get_template_name into its own method + use `self.default_template_name` --- openpype/plugins/publish/integrate_new.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4706d4d093..c1fa7ccaf2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -172,14 +172,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - template_name = "publish" - if profile: - template_name = profile["template_name"] + template_name = self._get_template_name(instance) subset = self.register_subset(instance) @@ -582,6 +575,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return families + def _get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + template_name = self.default_template_name + if profile: + template_name = profile["template_name"] + return template_name + def register_subset(self, instance): asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] From 821293d3b855acf2cadd914328a975fd619acd56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:09:31 +0100 Subject: [PATCH 0047/1030] Match comment from Integrator for consistency --- openpype/plugins/publish/collect_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 075699e304..5756563ed3 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -24,7 +24,7 @@ class CollectSubsetGroup(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.495 label = "Collect Subset Group" - # Defined in OpenPype settings + # Attributes set by settings subset_grouping_profiles = None def process(self, instance): From c3e0162c436a081ccf809cd429f5b828202569d0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:11:31 +0100 Subject: [PATCH 0048/1030] Debug log when exclude family was found for the instance --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c1fa7ccaf2..8a71c0d5aa 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -115,7 +115,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Exclude instances that also contain families from exclude families families = set(self._get_instance_families(instance)) - if families & set(self.exclude_families): + exclude = families & set(self.exclude_families) + if exclude: + self.log.debug("Instance not integrated due to exclude " + "families found: {}".format(", ".join(exclude))) return file_transactions = FileTransaction(log=self.log) From fbdb385e5b855c0762583256311501b78a2ca730 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:20:00 +0100 Subject: [PATCH 0049/1030] Perform database registering of Subset and Version in a single Bulk Write --- openpype/plugins/publish/integrate_new.py | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 8a71c0d5aa..6f1d745b9a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -177,11 +177,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = self._get_template_name(instance) - subset = self.register_subset(instance) - - version = self.register_version(instance, subset) + subset, subset_writes = self.register_subset(instance) + version, version_writes = self.register_version(instance, subset) instance.data["versionEntity"] = version + # Bulk write to the database + # todo: Try to avoid writing already until after we've prepared + # representations to allow easier rollback? + io._database[io.Session["AVALON_PROJECT"]].bulk_write( + subset_writes + version_writes + ) + archived_repres = list(io.find({ "parent": version["_id"], "type": "archived_representation" @@ -330,16 +336,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre["type"] = "archived_representation" bulk_writes.append(InsertOne(repre)) - # bulk updates - # todo: Try to avoid writing already until after we've prepared - # representations to allow easier rollback? - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - bulk_writes - ) - self.log.info("Registered version: v{0:03d}".format(version["name"])) - return version + return version, bulk_writes def prepare_representation(self, repre, template_name, @@ -612,6 +611,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if subset_group: data["subsetGroup"] = subset_group + bulk_writes = [] if subset is None: # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) @@ -623,22 +623,22 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": data, "parent": asset["_id"] } - io.insert_one(subset) + bulk_writes.append(InsertOne(subset)) 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["data"].update(data) - io.update_many( + bulk_writes.append(UpdateOne( {"type": "subset", "_id": subset["_id"]}, {"$set": { "data": subset["data"] }} - ) + )) self.log.info("Registered subset: {}".format(subset_name)) - return subset + return subset, bulk_writes def create_version_data(self, instance): """Create the data collection for the version From 1844281c68d0e357eccdc8c277db278ef0651f31 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:41:22 +0100 Subject: [PATCH 0050/1030] Match assertion for collection of files (allow no absolute paths) similar to single files --- openpype/plugins/publish/integrate_new.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6f1d745b9a..ead00452da 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -398,6 +398,10 @@ class IntegrateAssetNew(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" + ) + # Get the sequence as a collection. The files must be of a single # sequence and have no remainder outside of the collections. collections, remainder = clique.assemble(files, From 8e0161bec7353bff8bc581d4d676b3ba7c090ba8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 15:04:24 +0200 Subject: [PATCH 0051/1030] Also Bulk Write representation changes + more cleanup - Don't create intermediate archived representations - Move writing of Subset + Version to database to just before file transactions - Perform ReplaceOne for version instead of update with "$set" for the full version --- openpype/plugins/publish/integrate_new.py | 166 ++++++++++------------ 1 file changed, 79 insertions(+), 87 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index ead00452da..7a3ca2bdf7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -6,7 +6,7 @@ import clique import six from bson.objectid import ObjectId -from pymongo import DeleteOne, InsertOne, UpdateOne +from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api from avalon import io import openpype.api @@ -28,6 +28,11 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) +def bulk_write(writes): + """Convenience function to bulk write into active project database""" + return io._database[io.Session["AVALON_PROJECT"]].bulk_write(writes) + + class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -177,21 +182,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = self._get_template_name(instance) - subset, subset_writes = self.register_subset(instance) - version, version_writes = self.register_version(instance, subset) + subset, subset_writes = self.prepare_subset(instance) + version, version_writes = self.prepare_version(instance, subset) instance.data["versionEntity"] = version - # Bulk write to the database - # todo: Try to avoid writing already until after we've prepared - # representations to allow easier rollback? - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - subset_writes + version_writes - ) - - archived_repres = list(io.find({ - "parent": version["_id"], - "type": "archived_representation" - })) + # Get existing representations (if any) + existing_repres_by_name = { + repres["name"].lower(): repres for repres in io.find({ + "parent": version["_id"], + "type": "representation" + }) + } # Prepare all representations prepared_representations = [] @@ -205,7 +206,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, template_name, - archived_repres, + existing_repres_by_name, version, instance_stagingdir, instance) @@ -225,40 +226,70 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + # Bulk write to the database + # todo: Can we move this even to after the file transfers? + bulk_write(subset_writes + version_writes) + self.log.info("Subset {subset[name]} and Version {version[name]} " + "written to database..".format(subset=subset, + version=version)) + # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() - self.log.debug("Backup files " + self.log.debug("Backed up existing files: " "{}".format(file_transactions.backups)) - self.log.debug("Integrated files " + self.log.debug("Transferred files: " "{}".format(file_transactions.transferred)) # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources - self.log.debug("Retrieving Representation files information ...") + self.log.debug("Retrieving Representation Site Sync information ...") sites = SiteSync.compute_resource_sync_sites( system_settings=instance.context.data["system_settings"], project_settings=instance.context.data["project_settings"] ) - log.debug("final sites:: {}".format(sites)) + self.log.debug("final sites:: {}".format(sites)) anatomy = instance.context.data["anatomy"] - representations = [] + representation_writes = [] + new_repre_names_low = set() for prepared in prepared_representations: transfers = prepared["transfers"] representation = prepared["representation"] representation["files"] = self.get_files_info( transfers, sites, anatomy ) - representations.append(representation) - # Remove all archived representations - if archived_repres: - repre_ids_to_remove = [repre["_id"] for repre in archived_repres] - io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + # 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 + )) - # Write the new representations to the database - io.insert_many(representations) + new_repre_names_low.add(representation["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)} + } + )) + + # Write representations to the database + bulk_write(representation_writes) # Backwards compatibility # todo: can we avoid the need to store this? @@ -267,12 +298,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): } self.log.info("Registered {} representations" - "".format(len(representations))) + "".format(len(prepared_representations))) - def register_version(self, instance, subset): + def prepare_version(self, instance, subset): version_number = instance.data["version"] - self.log.debug("Version: v{0:03d}".format(version_number)) version = { "schema": "openpype:version-3.0", @@ -288,61 +318,26 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'name': version_number }) - bulk_writes = [] - if existing_version is None: + if existing_version: + self.log.debug("Updating existing version ...") + version["_id"] = existing_version["_id"] + else: self.log.debug("Creating new version ...") version["_id"] = ObjectId() - bulk_writes.append(InsertOne(version)) - else: - self.log.debug("Updating existing version ...") - # Check if instance have set `append` mode which cause that - # only replicated representations are set to archive - append_repres = instance.data.get("append", False) - # Update version data - version_id = existing_version['_id'] - bulk_writes.append(UpdateOne({ - '_id': version_id - }, { - '$set': version - })) + bulk_writes = [ReplaceOne( + filter={"_id": version["_id"]}, + replacement=version, + upsert=True + )] - # Instead of directly writing and querying we reproduce what - # the resulting version would look like so we can hold off making - # changes to the database to avoid the need for 'rollback' - version = copy.deepcopy(version) - version["_id"] = existing_version["_id"] - - # Find representations of existing version and archive them - repres = instance.data.get("representations", []) - new_repre_names_low = [_repre["name"].lower() for _repre in repres] - current_repres = io.find({ - "type": "representation", - "parent": version_id - }) - for repre in current_repres: - if append_repres: - # archive only duplicated representations - if repre["name"].lower() not in new_repre_names_low: - continue - # Representation must change type, - # `_id` must be stored to other key and replaced with new - # - that is because new representations should have same ID - repre_id = repre["_id"] - bulk_writes.append(DeleteOne({"_id": repre_id})) - - repre["orig_id"] = repre_id - repre["_id"] = ObjectId() - repre["type"] = "archived_representation" - bulk_writes.append(InsertOne(repre)) - - self.log.info("Registered version: v{0:03d}".format(version["name"])) + self.log.info("Prepared version: v{0:03d}".format(version["name"])) return version, bulk_writes def prepare_representation(self, repre, template_name, - archived_repres, + existing_repres_by_name, version, instance_stagingdir, instance): @@ -516,15 +511,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list - # Define representation id - repre_id = ObjectId() - # Use previous representation's id if there is a name match - repre_name_lower = repre["name"].lower() - for _archived_repres in archived_repres: - if repre_name_lower == _archived_repres["name"].lower(): - repre_id = _archived_repres["orig_id"] - break + existing = existing_repres_by_name.get(repre["name"].lower()) + if existing: + repre_id = existing["_id"] + else: + repre_id = ObjectId() # Backwards compatibility: # Store first transferred destination as published path data @@ -594,7 +586,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = profile["template_name"] return template_name - def register_subset(self, instance): + def prepare_subset(self, instance): asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) @@ -631,7 +623,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 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 + # We also change the found subset in-place so we don't need to # re-query the subset afterwards subset["data"].update(data) bulk_writes.append(UpdateOne( @@ -641,7 +633,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): }} )) - self.log.info("Registered subset: {}".format(subset_name)) + self.log.info("Prepared subset: {}".format(subset_name)) return subset, bulk_writes def create_version_data(self, instance): From ba2c6e6f084e5829f32250735f13f045cabca800 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 15:43:57 +0200 Subject: [PATCH 0052/1030] Fix class type --- openpype/plugins/publish/collect_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 5756563ed3..56cd7de94e 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -17,7 +17,7 @@ from openpype.lib import ( ) -class CollectSubsetGroup(pyblish.api.ContextPlugin): +class CollectSubsetGroup(pyblish.api.InstancePlugin): """Collect Subset Group for publish.""" # Run after CollectAnatomyInstanceData From e6665e579ee069b30a02b1034e53d48c85553761 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 20:32:46 +0200 Subject: [PATCH 0053/1030] Restructure code and more cleanup --- openpype/plugins/publish/integrate_new.py | 250 +++++++++++----------- 1 file changed, 123 insertions(+), 127 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 7a3ca2bdf7..6401806394 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -17,6 +17,21 @@ from openpype.lib.file_transaction import FileTransaction log = logging.getLogger(__name__) +def get_instance_families(instance): + """Get all families of the instance""" + # todo: move this to lib? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + def get_frame_padded(frame, padding): """Return frame number as string with `padding` amount of padded zeros""" return "{frame:0{padding}d}".format(padding=padding, frame=frame) @@ -119,7 +134,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): # Exclude instances that also contain families from exclude families - families = set(self._get_instance_families(instance)) + families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) if exclude: self.log.debug("Instance not integrated due to exclude " @@ -140,22 +155,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - 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"] - - # Task can be optional in anatomy data - task = anatomy_data.get("task", {}) - - # Return filter criteria - return { - "families": anatomy_data["family"], - "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") - } - def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") @@ -171,16 +170,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "@ {0}".format(instance_stagingdir) ) - # Ensure at least one file is set up for transfer in staging dir. + # Ensure at least one representation is set up for registering. repres = instance.data.get("representations") - assert repres, "Instance has no files to transfer" + assert repres, "Instance has representations data" assert isinstance(repres, (list, tuple)), ( - "Instance 'files' must be a list, got: {0} {1}".format( + "Instance 'repres' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) ) ) - template_name = self._get_template_name(instance) + template_name = self.get_template_name(instance) subset, subset_writes = self.prepare_subset(instance) version, version_writes = self.prepare_version(instance, subset) @@ -300,6 +299,56 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("Registered {} representations" "".format(len(prepared_representations))) + def prepare_subset(self, instance): + asset = instance.data.get("assetEntity") + subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + + # Get existing subset if it exists + subset = io.find_one({ + "type": "subset", + "parent": asset["_id"], + "name": subset_name + }) + + # Define subset data + data = { + "families": get_instance_families(instance) + } + + subset_group = instance.data.get("subsetGroup") + if subset_group: + data["subsetGroup"] = subset_group + + bulk_writes = [] + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), + "schema": "openpype:subset-3.0", + "type": "subset", + "name": subset_name, + "data": data, + "parent": asset["_id"] + } + bulk_writes.append(InsertOne(subset)) + + 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["data"].update(data) + bulk_writes.append(UpdateOne( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + )) + + self.log.info("Prepared subset: {}".format(subset_name)) + return subset, bulk_writes + def prepare_version(self, instance, subset): version_number = instance.data["version"] @@ -559,91 +608,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "published_files": [transfer[1] for transfer in transfers] } - def _get_instance_families(self, instance): - """Get all families of the instance""" - # todo: move this to lib? - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) - - return families - - def _get_template_name(self, instance): - """Return anatomy template name to use for integration""" - - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - template_name = self.default_template_name - if profile: - template_name = profile["template_name"] - return template_name - - def prepare_subset(self, instance): - asset = instance.data.get("assetEntity") - subset_name = instance.data["subset"] - self.log.debug("Subset: {}".format(subset_name)) - - # Get existing subset if it exists - subset = io.find_one({ - "type": "subset", - "parent": asset["_id"], - "name": subset_name - }) - - # Define subset data - data = { - "families": self._get_instance_families(instance) - } - - subset_group = instance.data.get("subsetGroup") - if subset_group: - data["subsetGroup"] = subset_group - - bulk_writes = [] - if subset is None: - # Create a new subset - self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset = { - "_id": ObjectId(), - "schema": "openpype:subset-3.0", - "type": "subset", - "name": subset_name, - "data": data, - "parent": asset["_id"] - } - bulk_writes.append(InsertOne(subset)) - - 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["data"].update(data) - bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset["_id"]}, - {"$set": { - "data": subset["data"] - }} - )) - - self.log.info("Prepared subset: {}".format(subset_name)) - return subset, bulk_writes - def create_version_data(self, instance): - """Create the data collection for the version + """Create the data dictionary for the version Args: instance: the current instance being published Returns: - dict: the required information with instance.data as key + dict: the required information for version["data"] """ context = instance.context @@ -658,7 +630,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Source: {}".format(source)) version_data = { - "families": self._get_instance_families(instance), + "families": get_instance_families(instance), "time": context.data["time"], "author": context.data["user"], "source": source, @@ -692,28 +664,52 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return version_data - def main_family_from_instance(self, instance): - """Returns main family of entered instance.""" - return self._get_instance_families(instance)[0] + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + template_name = self.default_template_name + if profile: + template_name = profile["template_name"] + return template_name + + 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"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } def get_rootless_path(self, anatomy, path): - """ Returns, if possible, path without absolute portion from host - (eg. 'c:\' or '/opt/..') - This information is host dependent and shouldn't be captured. - Example: - 'c:/projects/MyProject1/Assets/publish...' > - '{root}/MyProject1/Assets...' + """Returns, if possible, path without absolute portion from root + (eg. 'c:\' or '/opt/..') + + This information is platform dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' Args: - anatomy: anatomy part from instance - path: path (absolute) + anatomy: anatomy part from instance + path: path (absolute) Returns: - path: modified path if possible, or unmodified path - + warning logged + path: modified path if possible, or unmodified path + + warning logged """ - success, rootless_path = ( - anatomy.find_root_template_from_path(path) - ) + success, rootless_path = anatomy.find_root_template_from_path(path) if success: path = rootless_path else: @@ -731,9 +727,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Context info. Arguments: - instance: the current instance being published - integrated_file_sizes: dictionary of destination path (absolute) - and its file size + transfers (list): List of transferred files (source, destination) + sites (list): array of published locations + anatomy: anatomy part from instance Returns: output_resources: array of dictionaries to be added to 'files' key in representation @@ -749,14 +745,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file (rootless) - size(optional): size of file in bytes - file_hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] + path: destination url of published file + anatomy: anatomy part from instance + sites: array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + Returns: - rec: dictionary with filled info + dict: file info dictionary """ file_hash = openpype.api.source_hash(path) From 2777c36eb52e7390b15accc93c9b9a9a771ba21d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 20:34:16 +0200 Subject: [PATCH 0054/1030] Rely on `instance.data["fps"] over `context.data["fps"]` if available --- openpype/plugins/publish/integrate_new.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6401806394..00922b0ed3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -636,9 +636,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source": source, "comment": context.data.get("comment"), "machine": context.data.get("machine"), - "fps": context.data.get( - "fps", instance.data.get("fps") - ) + "fps": instance.data.get("fps", context.data.get("fps")) } intent_value = context.data.get("intent") From add4958d4c9078b6ecad131f6e40beb66ecdd348 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 09:43:27 +0200 Subject: [PATCH 0055/1030] Fix message --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 00922b0ed3..f6aa720dbb 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -172,7 +172,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Ensure at least one representation is set up for registering. repres = instance.data.get("representations") - assert repres, "Instance has representations data" + assert repres, "Instance has no representations data" assert isinstance(repres, (list, tuple)), ( "Instance 'repres' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) From 77b5c24370b61615b2380fdc464137d3eba13ab9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 11:44:30 +0200 Subject: [PATCH 0056/1030] Fix message --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f6aa720dbb..020b1d2b9c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -174,7 +174,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repres = instance.data.get("representations") assert repres, "Instance has no representations data" assert isinstance(repres, (list, tuple)), ( - "Instance 'repres' must be a list, got: {0} {1}".format( + "Instance 'representations' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) ) ) From 127f19873f876d58a2c954c4a56c73ddd4d4d4af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 11:58:52 +0200 Subject: [PATCH 0057/1030] Streamlining some code, optimize some database queries with projection --- openpype/plugins/publish/integrate_new.py | 36 ++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 020b1d2b9c..d869a1b6be 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -187,10 +187,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Get existing representations (if any) existing_repres_by_name = { - repres["name"].lower(): repres for repres in io.find({ - "parent": version["_id"], - "type": "representation" - }) + repres["name"].lower(): repres for repres in io.find( + { + "parent": version["_id"], + "type": "representation" + }, + # Only care about id and name of existing representations + projection={"_id": True, "name": True} + ) } # Prepare all representations @@ -239,16 +243,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "{}".format(file_transactions.backups)) self.log.debug("Transferred files: " "{}".format(file_transactions.transferred)) - - # Finalize the representations now the published files are integrated - # Get 'files' info for representations and its attached resources self.log.debug("Retrieving Representation Site Sync information ...") + + # Get the accessible sites for Site Sync sites = SiteSync.compute_resource_sync_sites( system_settings=instance.context.data["system_settings"], project_settings=instance.context.data["project_settings"] ) - self.log.debug("final sites:: {}".format(sites)) + self.log.debug("Site Sync Sites: {}".format(sites)) + # Finalize the representations now the published files are integrated + # Get 'files' info for representations and its attached resources anatomy = instance.context.data["anatomy"] representation_writes = [] new_repre_names_low = set() @@ -365,7 +370,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'type': 'version', 'parent': subset["_id"], 'name': version_number - }) + }, projection={"_id": True}) if existing_version: self.log.debug("Updating existing version ...") @@ -576,7 +581,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database - data = repre.get("data") or {} + data = repre.get("data", {}) data.update({'path': published_path, 'template': template}) representation = { "_id": repre_id, @@ -664,16 +669,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def get_template_name(self, instance): """Return anatomy template name to use for integration""" - # Define publish template name from profiles filter_criteria = self.get_profile_filter_criteria(instance) profile = filter_profiles(self.template_name_profiles, filter_criteria, logger=self.log) - template_name = self.default_template_name if profile: - template_name = profile["template_name"] - return template_name + return profile["template_name"] + else: + return self.default_template_name def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" @@ -752,13 +756,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Returns: dict: file info dictionary """ - file_hash = openpype.api.source_hash(path) - return { "_id": ObjectId(), "path": self.get_rootless_path(anatomy, path), "size": os.path.getsize(path), - "hash": file_hash, + "hash": openpype.api.source_hash(path), "sites": sites } From 0c2c60d37b05411193acf8c60f6a2562463ba558 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:23:24 +0200 Subject: [PATCH 0058/1030] Unify usage of `clique.assemble` --- openpype/plugins/publish/integrate_new.py | 60 ++++++++++++++--------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index d869a1b6be..1ceb99e9fe 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -17,6 +17,41 @@ from openpype.lib.file_transaction import FileTransaction 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? @@ -451,21 +486,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "Given file names contain full paths" ) - # Get the sequence as a collection. The files must be of a single - # sequence and have no remainder outside of the collections. - collections, remainder = clique.assemble(files, - minimum_items=1) - 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)) - src_collection = collections[0] + src_collection = assemble(files) # If the representation has `frameStart` set it renumbers the # frame indices of the published collection. It will start from @@ -512,14 +533,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_filled = anatomy_filled[template_name]["path"] repre_context = template_filled.used_values self.log.debug("Template filled: {}".format(str(template_filled))) - dst_collections, _remainder = clique.assemble( - [os.path.normpath(template_filled)], - minimum_items=1, - patterns=[clique.PATTERNS["frames"]] - ) - assert not _remainder, "This is a bug" - assert len(dst_collections) == 1, "This is a bug" - dst_collection = dst_collections[0] + dst_collection = assemble([os.path.normpath(template_filled)]) # Update the destination indexes and padding dst_collection.indexes.clear() From 44d6199a9e4ea7342fb2ef6bd583e0e373da2545 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:28:47 +0200 Subject: [PATCH 0059/1030] Organize single file code more like sequence file code --- openpype/plugins/publish/integrate_new.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1ceb99e9fe..1592789390 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -551,21 +551,24 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: # Single file - template_data.pop("frame", None) fname = files assert not os.path.isabs(fname), ( "Given file name is a full path" ) - # Store used frame value to template data + + # Manage anatomy template data + template_data.pop("frame", None) if repre.get("udim"): template_data["udim"] = repre["udim"][0] - src = os.path.join(stagingdir, fname) + + # Construct destination filepath from template anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] repre_context = template_filled.used_values dst = os.path.normpath(template_filled) # Single file transfer + src = os.path.join(stagingdir, fname) transfers = [(src, dst)] for key in self.db_representation_context_keys: From a2a77b8a2099b902e01816ec66a2f308e43004d1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:51:08 +0200 Subject: [PATCH 0060/1030] Cleanup `get_files_info` docstring --- openpype/plugins/publish/integrate_new.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1592789390..0ee2a6286f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -739,11 +739,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return path def get_files_info(self, transfers, sites, anatomy): - """ Prepare 'files' portion for attached resources and main asset. - Combining records from 'transfers' and 'hardlinks' parts from - instance. - All attached resources should be added, currently without - Context info. + """Prepare 'files' info portion for representations. Arguments: transfers (list): List of transferred files (source, destination) From 6fe6841c996594871a535daf2c21914e5cc32575 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 13:18:04 +0200 Subject: [PATCH 0061/1030] Capture edge case where all "representations" are tagged for delete --- openpype/plugins/publish/integrate_new.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 0ee2a6286f..80e1909687 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -255,6 +255,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): prepared_representations.append(prepared) + if not prepared_representations: + # Even though we check `instance.data["representations"]` earlier + # this could still happen if all representations were tagged with + # "delete" and thus are skipped for integration + raise RuntimeError("No representations prepared to publish.") + # Each instance can also have pre-defined transfers not explicitly # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. From a7a908d1348381ab0c4df9c29861d7c02be635cb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 13:20:51 +0200 Subject: [PATCH 0062/1030] Improve docstring --- openpype/plugins/publish/integrate_new.py | 39 +++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 80e1909687..8e666f3400 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -84,29 +84,26 @@ def bulk_write(writes): class IntegrateAssetNew(pyblish.api.InstancePlugin): - """Resolve any dependency issues + """Register publish in the database and transfer files to destinations. - This plug-in resolves any paths which, if not updated might break - the published file. + Steps: + 1) Register the subset and version + 2) Transfer the representation files to the destination + 3) Register the representation - The order of families is important, when working with lookdev you want to - first publish the texture, update the texture paths in the nodes and then - publish the shading network. Same goes for file dependent assets. - - Requirements for instance to be correctly integrated - - instance.data['representations'] - must be a list and each member - must be a dictionary with following data: - 'files': list of filenames for sequence, string for single file. - Only the filename is allowed, without the folder path. - 'stagingDir': "path/to/folder/with/files" - 'name': representation name (usually the same as extension) - 'ext': file extension - optional data - "frameStart" - "frameEnd" - 'fps' - "data": additional metadata for each representation. + Requires: + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. """ label = "Integrate Asset New" From 3ec9684239b7afc326cad7e184a7c6ed4e7a6058 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:28:13 +0200 Subject: [PATCH 0063/1030] Only add `frame` to context if used by the destination template --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3543786949..99a915af73 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -158,7 +158,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username", "frame" + "family", "hierarchy", "task", "username" ] default_template_name = "publish" From 6733df77f1f693b89078f216457621d129eb4f71 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:30:23 +0200 Subject: [PATCH 0064/1030] Remove double entry of "task" --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 99a915af73..da4dafb133 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -158,7 +158,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username" + "family", "hierarchy", "username" ] default_template_name = "publish" From c95c9f92b92f37eca20b1dbc82c3ef0620f8f753 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:34:52 +0200 Subject: [PATCH 0065/1030] Add comment --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index da4dafb133..a2943e2972 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -577,6 +577,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): transfers = [(src, dst)] for key in self.db_representation_context_keys: + # Also add these values to the context even if not used by the + # destination template value = template_data.get(key) if not value: continue From 65691bf5207cf57b679dd4b36b3abb6ae57e0be5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:36:32 +0200 Subject: [PATCH 0066/1030] Explain why we write subset+version first --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a2943e2972..bab46803cb 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -270,7 +270,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) # Bulk write to the database - # todo: Can we move this even to after the file transfers? + # We write the subset and version to the database before the File + # 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. bulk_write(subset_writes + version_writes) self.log.info("Subset {subset[name]} and Version {version[name]} " "written to database..".format(subset=subset, From 0d83f3c76c880d088de718a416370e69529ad4a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:38:43 +0200 Subject: [PATCH 0067/1030] Add to do for potential erroneous case --- openpype/plugins/publish/integrate_new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bab46803cb..84adccb633 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -602,6 +602,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # 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? published_path = transfers[0][1] repre["published_path"] = published_path # Backwards compatibility From 89376a97e4ef85069a3afdfd5e3115b33bd27284 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:47:00 +0200 Subject: [PATCH 0068/1030] Also include file infos of resource files like textures into each representation - This should fix Site Sync for lookdev textures, etc. --- openpype/plugins/publish/integrate_new.py | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 84adccb633..25ab7817c9 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -264,10 +264,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. # todo: should we move or simplify this logic? + resource_destinations = set() for src, dst in instance.data.get("transfers", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + resource_destinations.add(os.path.abspath(dst)) for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + resource_destinations.add(os.path.abspath(dst)) # Bulk write to the database # We write the subset and version to the database before the File @@ -295,18 +298,29 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) self.log.debug("Site Sync Sites: {}".format(sites)) + # Compute the resource file infos once (files belonging to the + # version instance instead of an individual representation) so + # we can re-use those file infos per representation + anatomy = instance.context.data["anatomy"] + resource_file_infos = self.prepare_file_info(resource_destinations, + sites=sites, + anatomy=anatomy) + # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources - anatomy = instance.context.data["anatomy"] representation_writes = [] new_repre_names_low = set() for prepared in prepared_representations: - transfers = prepared["transfers"] representation = prepared["representation"] + transfers = prepared["transfers"] + destinations = [dst for src, dst in transfers] representation["files"] = self.get_files_info( - transfers, sites, anatomy + destinations, sites=sites, anatomy=anatomy ) + # Add the version resource file infos to each representation + representation["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` @@ -751,11 +765,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, transfers, sites, anatomy): + def get_files_info(self, destinations, sites, anatomy): """Prepare 'files' info portion for representations. Arguments: - transfers (list): List of transferred files (source, destination) + destinations (list): List of transferred file destinations sites (list): array of published locations anatomy: anatomy part from instance Returns: @@ -763,10 +777,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): in representation """ file_infos = [] - for _src, dest in transfers: - file_info = self.prepare_file_info(dest, anatomy, sites=sites) + for file_path in destinations: + file_info = self.prepare_file_info(file_path, anatomy, sites=sites) file_infos.append(file_info) - return file_infos def prepare_file_info(self, path, anatomy, sites): From e6209555b01a0330186bc9176c8331a130325186 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:50:13 +0200 Subject: [PATCH 0069/1030] Match behavior more with what integrator did before refactor --- openpype/plugins/publish/collect_anatomy_context_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 346caf6b83..c3fabba2ce 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -93,9 +93,9 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): intent = context.data.get("intent") if intent and isinstance(intent, dict): - intent_value = intent.get("value") - if intent_value: - context_data["intent"] = intent_value + intent = intent.get("value") + if intent: + context_data["intent"] = intent self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) From 52fd21d85494dacd0071a3b08d79dbdd04789b30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:51:56 +0200 Subject: [PATCH 0070/1030] Add todo/question regarding `intent` --- openpype/plugins/publish/collect_anatomy_context_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index c3fabba2ce..3f7e65ecd3 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -91,6 +91,8 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) + # todo: some code actually expects the dict itself and others doesn't + # question: what should it be? intent = context.data.get("intent") if intent and isinstance(intent, dict): intent = intent.get("value") From 4c78976d3d834a5cb1fd0bce44f465cbf3ac6375 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:55:40 +0200 Subject: [PATCH 0071/1030] Add todo --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 25ab7817c9..e0c0632548 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -688,6 +688,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fps": instance.data.get("fps", context.data.get("fps")) } + # todo: preferably we wouldn't need this "if dict" etc. logic and + # instead be able to rely what the input value is if it's set. intent_value = context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") From 3e095bc7554a24ef13282ccfd87e0327eb3b8745 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:57:38 +0200 Subject: [PATCH 0072/1030] Use template name for frame padding anatomy template --- openpype/plugins/publish/integrate_new.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e0c0632548..0f3b11a025 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -520,8 +520,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("frameStart") is not None: index_frame_start = int(repre.get("frameStart")) - # TODO use frame padding from right template group - render_template = anatomy.templates["render"] + render_template = anatomy.templates[template_name] frame_start_padding = int( render_template.get( "frame_padding", From b12b1c80f2facbe343333ba3d70dcbe463383538 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:00:10 +0200 Subject: [PATCH 0073/1030] Never shift udim sequences --- openpype/plugins/publish/integrate_new.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 0f3b11a025..fd0d57c646 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -501,6 +501,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): anatomy = instance.context.data['anatomy'] template = os.path.normpath(anatomy.templates[template_name]["path"]) + is_udim = bool(repre.get("udim")) is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: # Collection of files (sequence) @@ -517,7 +518,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # frame indices from the source collection. destination_indexes = list(src_collection.indexes) destination_padding = len(get_first_frame_padded(src_collection)) - if repre.get("frameStart") is not None: + if repre.get("frameStart") is not None and not is_udim: index_frame_start = int(repre.get("frameStart")) render_template = anatomy.templates[template_name] @@ -543,7 +544,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # from the source indexes, etc. first_index_padded = get_frame_padded(frame=destination_indexes[0], padding=destination_padding) - if repre.get("udim"): + if is_udim: # UDIM representations handle ranges in a different manner template_data["udim"] = first_index_padded else: @@ -579,7 +580,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Manage anatomy template data template_data.pop("frame", None) - if repre.get("udim"): + if is_udim: template_data["udim"] = repre["udim"][0] # Construct destination filepath from template From f7d35c4fed0885c6656da03eb852706c6bf20117 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:01:09 +0200 Subject: [PATCH 0074/1030] add todo/question --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index fd0d57c646..52c7686473 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -522,6 +522,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 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", From 70bfdd09b40936efc45efa6bbd1ea029447058f2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:07:02 +0200 Subject: [PATCH 0075/1030] Remove old "dependencies" data --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 52c7686473..37c68ffa6d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -636,7 +636,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "parent": version["_id"], "name": repre['name'], "data": data, - "dependencies": instance.data.get("dependencies", "").split(), # Imprint shortcut to context for performance reasons. "context": repre_context From 45745cc514236d64cc7f2feddbff9e6217b720fa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:37:28 +0200 Subject: [PATCH 0076/1030] Improve clarity of comment --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 37c68ffa6d..cb469251e6 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -604,7 +604,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_context[key] = template_data[key] # Explicitly store the full list even though template data might - # have a different value + # have a different value because it uses just a single udim tile if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list From fe72197a9feb413c8f6c5f9e02339ed891fdda07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:40:25 +0200 Subject: [PATCH 0077/1030] Add comment --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index cb469251e6..f1cceb9ca7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -156,11 +156,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride" ] exclude_families = ["clip"] + default_template_name = "publish" + + # Representation context keys that should always be written to + # the database even if not used by the destination template db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "username" ] - default_template_name = "publish" # Attributes set by settings template_name_profiles = None From c3c8281e0134222677b32f91ec644322dd996a74 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:41:34 +0200 Subject: [PATCH 0078/1030] tweak comment --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f1cceb9ca7..238ae82bba 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -183,7 +183,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.register(instance, file_transactions) except Exception: # clean destination - # todo: rollback any registered entities? (or how safe are we?) + # todo: preferably we'd also rollback *any* changes to the database file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) six.reraise(*sys.exc_info()) From 1177ee2a25403c143aa0d2639102ee1360ac77d2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 11 Apr 2022 15:21:59 +0300 Subject: [PATCH 0079/1030] 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 0080/1030] 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 2e2deb349d082096d056f878a5cf629c2f95e12c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:06:14 +0200 Subject: [PATCH 0081/1030] Match changes that were made to original IntegrateAsset Changes of: - https://github.com/pypeclub/OpenPype/commit/312d0309ab92de834629c58587f1a758d1d1e90c - https://github.com/pypeclub/OpenPype/commit/507f3615ab8f42f5664afcac01d339e0517afdf5 - https://github.com/pypeclub/OpenPype/commit/29dca65202d45a79e66c619b95d3408e227a9c05 --- openpype/plugins/publish/integrate_new.py | 61 ++++++++++++++++++----- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 9e8dfefc9e..768c413bf9 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -4,6 +4,7 @@ import sys import copy import clique import six +from collections import deque, defaultdict from bson.objectid import ObjectId from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne @@ -871,18 +872,18 @@ class SiteSync(object): attached_sites[remote_site] = create_metadata(remote_site, created=False) + # add alternative sites + cls._add_alternative_sites(system_sync_server_presets, attached_sites) + # add skeleton for sites where it should be always synced to always_accessible_sites = ( sync_project_presets["config"].get("always_accessible_on", []) ) - for site in always_accessible_sites: + for site in set(always_accessible_sites): site = site.strip() if site not in attached_sites: attached_sites[site] = create_metadata(site, created=False) - # add alternative sites - cls._add_alternative_sites(system_sync_server_presets, attached_sites) - return list(attached_sites.values()) @staticmethod @@ -904,8 +905,9 @@ class SiteSync(object): return local_site, remote_site - @staticmethod - def _add_alternative_sites(system_sync_server_presets, + @classmethod + def _add_alternative_sites(cls, + system_sync_server_presets, attached_sites): """Loop through all configured sites and add alternatives. @@ -916,18 +918,14 @@ class SiteSync(object): See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) + alt_site_pairs = cls._get_alt_site_pairs(conf_sites) - for site_name, site_info in conf_sites.items(): + for site_name, alt_sites in alt_site_pairs.items(): # Skip if already defined if site_name in attached_sites: continue - # Get alternate sites (stripped names) for this site name - alt_sites = site_info.get("alternative_sites", []) - alt_sites = [site.strip() for site in alt_sites] - alt_sites = set(alt_sites) - # If no alternative sites we don't need to add if not alt_sites: continue @@ -944,3 +942,42 @@ class SiteSync(object): # Note: We change mutable `attached_site` dict in-place attached_sites[site_name] = alt_site_meta + + @staticmethod + def _get_alt_site_pairs(conf_sites): + """Returns dict of site and its alternative sites. + If `site` has alternative site, it means that alt_site has + 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) + + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() + + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue + + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) + + return alt_site_pairs From 0fdd4f1aecd3b5fa09496d4aa48ee605a003e61d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:07:33 +0200 Subject: [PATCH 0082/1030] Fix indentation --- openpype/plugins/publish/integrate_new.py | 66 +++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 768c413bf9..4eccce4e81 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -943,41 +943,41 @@ class SiteSync(object): # Note: We change mutable `attached_site` dict in-place attached_sites[site_name] = alt_site_meta - @staticmethod - def _get_alt_site_pairs(conf_sites): - """Returns dict of site and its alternative sites. - If `site` has alternative site, it means that alt_site has - 'site' as - alternative site - Args: - conf_sites (dict) - Returns: - (dict): {'site': [alternative sites]...} - """ - alt_site_pairs = defaultdict(list) - for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].extend(alt_sites) + @staticmethod + def _get_alt_site_pairs(conf_sites): + """Returns dict of site and its alternative sites. + If `site` has alternative site, it means that alt_site has + 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) - for alt_site in alt_sites: - alt_site_pairs[alt_site].append(site_name) + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) - for site_name, alt_sites in alt_site_pairs.items(): - sites_queue = deque(alt_sites) - while sites_queue: - alt_site = sites_queue.popleft() + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name or alt_site not in alt_site_pairs: - continue + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue - for alt_alt_site in alt_site_pairs[alt_site]: - if ( - alt_alt_site != site_name - and alt_alt_site not in alt_sites - ): - alt_sites.append(alt_alt_site) - sites_queue.append(alt_alt_site) + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) - return alt_site_pairs + return alt_site_pairs From 1a03bbe48a37cc62918152985488a0bd99d43473 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:11:57 +0200 Subject: [PATCH 0083/1030] Store alt sites in a `set` --- openpype/plugins/publish/integrate_new.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4eccce4e81..2795b59482 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -954,13 +954,13 @@ class SiteSync(object): Returns: (dict): {'site': [alternative sites]...} """ - alt_site_pairs = defaultdict(list) + alt_site_pairs = defaultdict(set) for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].extend(alt_sites) + alt_site_pairs[site_name].update(alt_sites) for alt_site in alt_sites: - alt_site_pairs[alt_site].append(site_name) + alt_site_pairs[alt_site].add(site_name) for site_name, alt_sites in alt_site_pairs.items(): sites_queue = deque(alt_sites) @@ -977,7 +977,7 @@ class SiteSync(object): alt_alt_site != site_name and alt_alt_site not in alt_sites ): - alt_sites.append(alt_alt_site) + alt_sites.add(alt_alt_site) sites_queue.append(alt_alt_site) return alt_site_pairs From b64b0a66b06ea6fc8dfaeedc4a7c3bef1f53a609 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 20:28:56 +0300 Subject: [PATCH 0084/1030] 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 0085/1030] 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 0086/1030] 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 0087/1030] 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 0088/1030] 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 0089/1030] 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 0090/1030] 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 0091/1030] 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 0092/1030] 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 0093/1030] 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 0094/1030] 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 0095/1030] 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 8a970b123c1697d7db28f31debf4f7113c3c3177 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Apr 2022 11:24:49 +0200 Subject: [PATCH 0096/1030] Use logic directly from Sync Server module --- openpype/plugins/publish/integrate_new.py | 165 +--------------------- 1 file changed, 6 insertions(+), 159 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2795b59482..cc6856e407 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -4,14 +4,13 @@ import sys import copy import clique import six -from collections import deque, defaultdict from bson.objectid import ObjectId from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api from avalon import io import openpype.api -from datetime import datetime +from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction @@ -299,11 +298,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync - sites = SiteSync.compute_resource_sync_sites( - system_settings=instance.context.data["system_settings"], - project_settings=instance.context.data["project_settings"] + manager = ModulesManager() + sync_server_module = manager.modules_by_name["sync_server"] + sites = sync_server_module.compute_resource_sync_sites( + project_name=instance.data["projectEntity"]["name"] ) - self.log.debug("Site Sync Sites: {}".format(sites)) + self.log.debug("Sync Server Sites: {}".format(sites)) # Compute the resource file infos once (files belonging to the # version instance instead of an individual representation) so @@ -828,156 +828,3 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hash": openpype.api.source_hash(path), "sites": sites } - - -class SiteSync(object): - """Logic for Site Sync Module functionality""" - - @classmethod - def compute_resource_sync_sites(cls, - system_settings, - project_settings): - """Get available resource sync sites""" - - def create_metadata(name, created=True): - """Create sync site metadata for site with `name`""" - metadata = {"name": name} - if created: - metadata["created_dt"] = datetime.now() - return metadata - - default_sites = [create_metadata("studio")] - - # If sync site module is disabled return default fallback site - system_sync_server_presets = system_settings["modules"]["sync_server"] - log.debug("system_sett:: {}".format(system_sync_server_presets)) - if not system_sync_server_presets["enabled"]: - return default_sites - - # If sync site module is disabled in current - # project return default fallback site - sync_project_presets = project_settings["global"]["sync_server"] - if not sync_project_presets["enabled"]: - return default_sites - - local_site, remote_site = cls._get_sites(sync_project_presets) - - # Attached sites metadata by site name - # That is the local site, remote site, the always accesible sites - # and their alternate sites (alias of sites with different protocol) - attached_sites = dict() - attached_sites[local_site] = create_metadata(local_site) - - if remote_site and remote_site != local_site: - attached_sites[remote_site] = create_metadata(remote_site, - created=False) - - # add alternative sites - cls._add_alternative_sites(system_sync_server_presets, attached_sites) - - # add skeleton for sites where it should be always synced to - always_accessible_sites = ( - sync_project_presets["config"].get("always_accessible_on", []) - ) - for site in set(always_accessible_sites): - site = site.strip() - if site not in attached_sites: - attached_sites[site] = create_metadata(site, created=False) - - return list(attached_sites.values()) - - @staticmethod - def _get_sites(sync_project_presets): - """Returns tuple (local_site, remote_site)""" - local_site_id = openpype.api.get_local_site_id() - local_site = sync_project_presets["config"]. \ - get("active_site", "studio").strip() - - if local_site == 'local': - local_site = local_site_id - - remote_site = sync_project_presets["config"].get("remote_site") - if remote_site: - remote_site.strip() - - if remote_site == 'local': - remote_site = local_site_id - - return local_site, remote_site - - @classmethod - def _add_alternative_sites(cls, - system_sync_server_presets, - attached_sites): - """Loop through all configured sites and add alternatives. - - For all sites if an alternative site is detected that has an - accessible site then we can also register to that alternative site - with the same "created" state. So we match the existing data. - - See SyncServerModule.handle_alternate_site - """ - conf_sites = system_sync_server_presets.get("sites", {}) - alt_site_pairs = cls._get_alt_site_pairs(conf_sites) - - for site_name, alt_sites in alt_site_pairs.items(): - - # Skip if already defined - if site_name in attached_sites: - continue - - # If no alternative sites we don't need to add - if not alt_sites: - continue - - # Take a copy of data of the first alternate site that is already - # defined as an attached site to match the same state. - match_meta = next((attached_sites[site] for site in alt_sites - if site in attached_sites), None) - if not match_meta: - continue - - alt_site_meta = copy.deepcopy(match_meta) - alt_site_meta["name"] = site_name - - # Note: We change mutable `attached_site` dict in-place - attached_sites[site_name] = alt_site_meta - - @staticmethod - def _get_alt_site_pairs(conf_sites): - """Returns dict of site and its alternative sites. - If `site` has alternative site, it means that alt_site has - 'site' as - alternative site - Args: - conf_sites (dict) - Returns: - (dict): {'site': [alternative sites]...} - """ - alt_site_pairs = defaultdict(set) - for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].update(alt_sites) - - for alt_site in alt_sites: - alt_site_pairs[alt_site].add(site_name) - - for site_name, alt_sites in alt_site_pairs.items(): - sites_queue = deque(alt_sites) - while sites_queue: - alt_site = sites_queue.popleft() - - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name or alt_site not in alt_site_pairs: - continue - - for alt_alt_site in alt_site_pairs[alt_site]: - if ( - alt_alt_site != site_name - and alt_alt_site not in alt_sites - ): - alt_sites.add(alt_alt_site) - sites_queue.append(alt_alt_site) - - return alt_site_pairs From ae1acb950bbb69b203c36f19d40e3952eca46bfd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Apr 2022 14:08:53 +0200 Subject: [PATCH 0097/1030] Fix: refactor to use correct function --- openpype/plugins/publish/integrate_new.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index cc6856e407..419e2b4e4b 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -309,9 +309,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # version instance instead of an individual representation) so # we can re-use those file infos per representation anatomy = instance.context.data["anatomy"] - resource_file_infos = self.prepare_file_info(resource_destinations, - sites=sites, - anatomy=anatomy) + resource_file_infos = self.get_files_info(resource_destinations, + sites=sites, + anatomy=anatomy) # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources From 54ff5a8e53449bf4ae895e4ddb142dbe25b0fe95 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 21 Apr 2022 20:17:40 +0300 Subject: [PATCH 0098/1030] 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 0099/1030] 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 0100/1030] 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 0101/1030] 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 0102/1030] 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 0103/1030] 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 0104/1030] 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 0105/1030] 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 0106/1030] 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 0107/1030] 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 0108/1030] 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 0109/1030] 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 0110/1030] 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 0111/1030] 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 0112/1030] 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 0113/1030] 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 0114/1030] 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 0115/1030] 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 0116/1030] 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 0117/1030] 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 0118/1030] 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 0119/1030] 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 0120/1030] 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 0121/1030] 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 0122/1030] 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 0123/1030] 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 0124/1030] 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 0125/1030] 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 0126/1030] 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 0127/1030] 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 0128/1030] 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 0129/1030] 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 0130/1030] 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 0131/1030] 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 0132/1030] 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 0133/1030] 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 0134/1030] 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 0135/1030] 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 0136/1030] 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 0137/1030] 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 0138/1030] 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 0139/1030] 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 0140/1030] 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 0141/1030] 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 0142/1030] 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 0143/1030] 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 397ecb529e2a0524435055cfaedb5f465fa1a103 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:46:35 +0200 Subject: [PATCH 0144/1030] nuke validate backdrop with help --- .../publish/help/validate_backdrop.xml | 36 +++++++++++++++++++ .../nuke/plugins/publish/validate_backdrop.py | 19 ++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml b/openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml new file mode 100644 index 0000000000..ab1b650773 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml @@ -0,0 +1,36 @@ + + + + Found multiple outputs + +## Invalid output amount + +Backdrop is having more than one outgoing connections. + +### How to repair? + +1. Use button `Center node in node graph` and navigate to the backdrop. +2. Reorganize nodes the way only one outgoing connection is present. +3. Hit reload button on the publisher. + + +### How could this happen? + +More than one node, which are found above the backdrop, are linked downstream or more output connections from a node also linked downstream. + + + + Empty backdrop + +## Invalid empty backdrop + +Backdrop is empty and no nodes are found above it. + +### How to repair? + +1. Use button `Center node in node graph` and navigate to the backdrop. +2. Add any node above it or delete it. +3. Hit reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py index e2843d146e..2a0d3309a0 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py @@ -1,6 +1,7 @@ import nuke import pyblish from openpype.hosts.nuke.api.lib import maintained_selection +from openpype.pipeline import PublishXmlValidationError class SelectCenterInNodeGraph(pyblish.api.Action): @@ -63,8 +64,20 @@ class ValidateBackdrop(pyblish.api.InstancePlugin): msg_multiple_outputs = ( "Only one outcoming connection from " "\"{}\" is allowed").format(instance.data["name"]) - assert len(connections_out.keys()) <= 1, msg_multiple_outputs - msg_no_content = "No content on backdrop node: \"{}\"".format( + if len(connections_out.keys()) > 1: + raise PublishXmlValidationError( + self, + msg_multiple_outputs, + "multiple_outputs" + ) + + msg_no_nodes = "No content on backdrop node: \"{}\"".format( instance.data["name"]) - assert len(instance) > 1, msg_no_content + + if len(instance) == 0: + raise PublishXmlValidationError( + self, + msg_no_nodes, + "no_nodes" + ) From d097503f4de4828d4a30071769a26328a71f90f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:54:22 +0200 Subject: [PATCH 0145/1030] nuke validate write nodes with help --- .../publish/help/validate_write_nodes.xml | 17 ++++++++++++++ .../plugins/publish/validate_write_nodes.py | 22 +++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml new file mode 100644 index 0000000000..d209329434 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml @@ -0,0 +1,17 @@ + + + + Knobs values + +## Invalid node's knobs values + +Following write node knobs needs to be repaired: +{xml_msg} + +### How to repair? + +1. Use Repair button. +3. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index c0d5c8f402..94dabecc97 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,10 +1,10 @@ -import os import pyblish.api -import openpype.utils +from openpype.api import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( get_write_node_template_attr, get_node_path ) +from openpype.pipeline import PublishXmlValidationError @pyblish.api.log @@ -14,7 +14,7 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): icon = "wrench" def process(self, context, plugin): - instances = openpype.utils.filter_instances(context, plugin) + instances = get_errored_instances_from_context(context) for instance in instances: node = instance[1] @@ -60,13 +60,17 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): self.log.info(check) - msg = "Node's attribute `{0}` is not correct!\n" \ - "\nCorrect: `{1}` \n\nWrong: `{2}` \n\n" + msg = "Write node's knobs values are not correct!\n" + msg_add = "Knob `{0}` Correct: `{1}` Wrong: `{2}` \n" + xml_msg = "" if check: - print_msg = "" + dbg_msg = msg for item in check: - print_msg += msg.format(item[0], item[1], item[2]) - print_msg += "`RMB` click to the validator and `A` to fix!" + _msg_add = msg_add.format(item[0], item[1], item[2]) + dbg_msg += _msg_add + xml_msg += _msg_add - assert not check, print_msg + raise PublishXmlValidationError( + self, dbg_msg, {"xml_msg": xml_msg} + ) From bdf8c158e5877b1f2bfa08eb8e766e339b52867a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Dec 2021 14:42:25 +0100 Subject: [PATCH 0146/1030] nuke adding better docstring --- openpype/hosts/nuke/plugins/publish/validate_write_nodes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 94dabecc97..069c6f4d8c 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -25,7 +25,11 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): class ValidateNukeWriteNode(pyblish.api.InstancePlugin): - """ Validates file output. """ + """ Validate Write node's knobs. + + Compare knobs on write node inside the render group + with settings. At the moment supporting only `file` knob. + """ order = pyblish.api.ValidatorOrder optional = True From 1a86a2e2655e81e2633841bf26935d31f42a888a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:56:07 +0200 Subject: [PATCH 0147/1030] nuke validate gizmo with help --- .../plugins/publish/help/validate_gizmo.xml | 36 +++++++++++++++++++ .../nuke/plugins/publish/validate_gizmo.py | 31 +++++++++++----- 2 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml b/openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml new file mode 100644 index 0000000000..f39a41a4f9 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml @@ -0,0 +1,36 @@ + + + + Found multiple outputs + +## Invalid amount of Output nodes + +Group node `{node_name}` is having more than one Output node. + +### How to repair? + +1. Use button `Open Group`. +2. Remove redundant Output node. +3. Hit reload button on the publisher. + + +### How could this happen? + +Perhaps you had created exciently more than one Output node. + + + + Missing Input nodes + +## Missing Input nodes + +Make sure there is at least one connected Input node inside the group node with name `{node_name}` + +### How to repair? + +1. Use button `Open Group`. +2. Add at least one Input node and connect to other nodes. +3. Hit reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_gizmo.py b/openpype/hosts/nuke/plugins/publish/validate_gizmo.py index d0d930f50c..2321bd1fd4 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_gizmo.py +++ b/openpype/hosts/nuke/plugins/publish/validate_gizmo.py @@ -1,6 +1,7 @@ -import nuke import pyblish -from openpype.hosts.nuke.api.lib import maintained_selection +from openpype.pipeline import PublishXmlValidationError +from openpype.hosts.nuke.api import maintained_selection +import nuke class OpenFailedGroupNode(pyblish.api.Action): @@ -8,7 +9,7 @@ class OpenFailedGroupNode(pyblish.api.Action): Centering failed instance node in node grap """ - label = "Open Gizmo in Node Graph" + label = "Open Group" icon = "wrench" on = "failed" @@ -48,11 +49,23 @@ class ValidateGizmo(pyblish.api.InstancePlugin): with grpn: connections_out = nuke.allNodes('Output') - msg_multiple_outputs = "Only one outcoming connection from " - "\"{}\" is allowed".format(instance.data["name"]) - assert len(connections_out) <= 1, msg_multiple_outputs + msg_multiple_outputs = ( + "Only one outcoming connection from " + "\"{}\" is allowed").format(instance.data["name"]) + + if len(connections_out) > 1: + raise PublishXmlValidationError( + self, msg_multiple_outputs, "multiple_outputs", + {"node_name": grpn["name"].value()} + ) connections_in = nuke.allNodes('Input') - msg_missing_inputs = "At least one Input node has to be used in: " - "\"{}\"".format(instance.data["name"]) - assert len(connections_in) >= 1, msg_missing_inputs + msg_missing_inputs = ( + "At least one Input node has to be inside Group: " + "\"{}\"").format(instance.data["name"]) + + if len(connections_in) == 0: + raise PublishXmlValidationError( + self, msg_missing_inputs, "no_inputs", + {"node_name": grpn["name"].value()} + ) From 0666bce2be03be03e9a4ff73ff48ee0d36343735 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:58:33 +0200 Subject: [PATCH 0148/1030] nuke validate asset name with help rename plugin, fix order on how to fix in validate write nodes xml --- .../publish/help/validate_asset_name.xml | 18 ++++++++ .../publish/help/validate_write_nodes.xml | 2 +- ...e_in_context.py => validate_asset_name.py} | 43 ++++++++++++------- 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml rename openpype/hosts/nuke/plugins/publish/{validate_instance_in_context.py => validate_asset_name.py} (75%) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml new file mode 100644 index 0000000000..7ea552597a --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml @@ -0,0 +1,18 @@ + + + + Shot/Asset mame + +## Invalid Shot/Asset name in subset + +Following Node with name `{node_name}`: +Is in context of `{context_asset}` but Node _asset_ knob is set as `{asset}`. + +### How to repair? + +1. Either use Repair or Select button. +2. If you chose Select then rename asset knob to correct name. +3. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml index d209329434..c1f59a94f8 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml @@ -11,7 +11,7 @@ Following write node knobs needs to be repaired: ### How to repair? 1. Use Repair button. -3. Hit Reload button on the publisher. +2. Hit Reload button on the publisher. \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py similarity index 75% rename from openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py rename to openpype/hosts/nuke/plugins/publish/validate_asset_name.py index 842f74b6f6..f9adac81f8 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py @@ -3,20 +3,17 @@ from __future__ import absolute_import import nuke - import pyblish.api import openpype.api -from openpype.hosts.nuke.api.lib import ( - recreate_instance, - reset_selection, - select_nodes -) +import openpype.hosts.nuke.api.lib as nlib +import openpype.hosts.nuke.api as nuke_api +from openpype.pipeline import PublishXmlValidationError class SelectInvalidInstances(pyblish.api.Action): """Select invalid instances in Outliner.""" - label = "Select Instances" + label = "Select" icon = "briefcase" on = "failed" @@ -50,12 +47,12 @@ class SelectInvalidInstances(pyblish.api.Action): self.deselect() def select(self, instances): - select_nodes( + nlib.select_nodes( [nuke.toNode(str(x)) for x in instances] ) def deselect(self): - reset_selection() + nlib.reset_selection() class RepairSelectInvalidInstances(pyblish.api.Action): @@ -85,12 +82,12 @@ class RepairSelectInvalidInstances(pyblish.api.Action): context_asset = context.data["assetEntity"]["name"] for instance in instances: origin_node = instance[0] - recreate_instance( + nuke_api.lib.recreate_instance( origin_node, avalon_data={"asset": context_asset} ) -class ValidateInstanceInContext(pyblish.api.InstancePlugin): +class ValidateCorrectAssetName(pyblish.api.InstancePlugin): """Validator to check if instance asset match context asset. When working in per-shot style you always publish data in context of @@ -99,15 +96,29 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin): Action on this validator will select invalid instances in Outliner. """ - order = openpype.api.ValidateContentsOrder - label = "Instance in same Context" + label = "Validate correct asset name" hosts = ["nuke"] - actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + actions = [ + SelectInvalidInstances, + RepairSelectInvalidInstances + ] optional = True def process(self, instance): asset = instance.data.get("asset") context_asset = instance.context.data["assetEntity"]["name"] - msg = "{} has asset {}".format(instance.name, asset) - assert asset == context_asset, msg + + msg = ( + "Instance `{}` has wrong shot/asset name:\n" + "Correct: `{}` | Wrong: `{}`").format( + instance.name, asset, context_asset) + + if asset != context_asset: + PublishXmlValidationError( + self, msg, formatting_data={ + "node_name": instance[0]["name"].value(), + "wrong_name": asset, + "correct_name": context_asset + } + ) From af3623c4dc3626ee474dd7606ac01a757323787a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 18:00:36 +0200 Subject: [PATCH 0149/1030] nuke fixes on validation and precollect --- .../plugins/publish/help/validate_asset_name.xml | 2 +- .../nuke/plugins/publish/validate_asset_name.py | 5 ++++- .../hosts/nuke/plugins/publish/validate_backdrop.py | 12 +++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml index 7ea552597a..1097909a5f 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml @@ -6,7 +6,7 @@ ## Invalid Shot/Asset name in subset Following Node with name `{node_name}`: -Is in context of `{context_asset}` but Node _asset_ knob is set as `{asset}`. +Is in context of `{correct_name}` but Node _asset_ knob is set as `{wrong_name}`. ### How to repair? diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py index f9adac81f8..7647471f8a 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py @@ -36,6 +36,7 @@ class SelectInvalidInstances(pyblish.api.Action): instances = pyblish.api.instances_by_plugin(failed, plugin) if instances: + self.deselect() self.log.info( "Selecting invalid nodes: %s" % ", ".join( [str(x) for x in instances] @@ -114,8 +115,10 @@ class ValidateCorrectAssetName(pyblish.api.InstancePlugin): "Correct: `{}` | Wrong: `{}`").format( instance.name, asset, context_asset) + self.log.debug(msg) + if asset != context_asset: - PublishXmlValidationError( + raise PublishXmlValidationError( self, msg, formatting_data={ "node_name": instance[0]["name"].value(), "wrong_name": asset, diff --git a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py index 2a0d3309a0..17dc79dc56 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py @@ -48,8 +48,9 @@ class SelectCenterInNodeGraph(pyblish.api.Action): @pyblish.api.log class ValidateBackdrop(pyblish.api.InstancePlugin): - """Validate amount of nodes on backdrop node in case user - forgotten to add nodes above the publishing backdrop node""" + """ Validate amount of nodes on backdrop node in case user + forgoten to add nodes above the publishing backdrop node. + """ order = pyblish.api.ValidatorOrder optional = True @@ -75,7 +76,12 @@ class ValidateBackdrop(pyblish.api.InstancePlugin): msg_no_nodes = "No content on backdrop node: \"{}\"".format( instance.data["name"]) - if len(instance) == 0: + self.log.debug( + "Amount of nodes on instance: {}".format( + len(instance)) + ) + + if len(instance) == 1: raise PublishXmlValidationError( self, msg_no_nodes, From 44bd543d131e3b60be44fc79004e8487882ca61c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Dec 2021 18:28:30 +0100 Subject: [PATCH 0150/1030] nuke: fixing name for validate asset name --- openpype/settings/defaults/project_settings/nuke.json | 2 +- .../schemas/projects_schema/schemas/schema_nuke_publish.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 33ddc2f251..fb1e475e9f 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -91,7 +91,7 @@ "write" ] }, - "ValidateInstanceInContext": { + "ValidateCorrectAssetName": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 04df957d67..5635f871d5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -61,8 +61,8 @@ "name": "template_publish_plugin", "template_data": [ { - "key": "ValidateInstanceInContext", - "label": "Validate Instance In Context" + "key": "ValidateCorrectAssetName", + "label": "Validate Correct Asset name" } ] }, From e2b2bdc15d5f919793848e15b3b2fb2f00c2aec5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Dec 2021 15:29:49 +0100 Subject: [PATCH 0151/1030] nuke validate knobs with help --- .../nuke/plugins/publish/help/validate_knobs.xml | 16 ++++++++++++++++ .../hosts/nuke/plugins/publish/validate_knobs.py | 8 +++++--- .../nuke/plugins/publish/validate_write_nodes.py | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml b/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml new file mode 100644 index 0000000000..cb5494729b --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml @@ -0,0 +1,16 @@ + + + + Knobs value + +## Invalid node's knobs values + +Following node knobs needs to be repaired: + +### How to repair? + +1. Use Repair button. +2. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_knobs.py index d290ff4541..d20051a9d5 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_knobs.py +++ b/openpype/hosts/nuke/plugins/publish/validate_knobs.py @@ -2,6 +2,7 @@ import nuke import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateKnobs(pyblish.api.ContextPlugin): @@ -27,11 +28,12 @@ class ValidateKnobs(pyblish.api.ContextPlugin): optional = True def process(self, context): - invalid = self.get_invalid(context, compute=True) if invalid: - raise RuntimeError( - "Found knobs with invalid values:\n{}".format(invalid) + raise PublishXmlValidationError( + self, + "Found knobs with invalid values:\n{}".format(invalid), + formatting_data={} ) @classmethod diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 069c6f4d8c..f8d8393730 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -76,5 +76,5 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): xml_msg += _msg_add raise PublishXmlValidationError( - self, dbg_msg, {"xml_msg": xml_msg} + self, dbg_msg, formatting_data={"xml_msg": xml_msg} ) From 447742506dcf05301c3818d3ac154c0dd4e25d8d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 21:04:45 +0200 Subject: [PATCH 0152/1030] Nuke: improving knob validator with p3 compatibility --- openpype/hosts/nuke/plugins/publish/validate_knobs.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_knobs.py index d20051a9d5..24723edc7a 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_knobs.py +++ b/openpype/hosts/nuke/plugins/publish/validate_knobs.py @@ -1,5 +1,5 @@ import nuke - +import six import pyblish.api import openpype.api from openpype.pipeline import PublishXmlValidationError @@ -64,7 +64,7 @@ class ValidateKnobs(pyblish.api.ContextPlugin): knobs = {} for family in families: for preset in cls.knobs[family]: - knobs.update({preset: cls.knobs[family][preset]}) + knobs[preset] = cls.knobs[family][preset] # Get invalid knobs. nodes = [] @@ -73,8 +73,7 @@ class ValidateKnobs(pyblish.api.ContextPlugin): nodes.append(node) if node.Class() == "Group": node.begin() - for i in nuke.allNodes(): - nodes.append(i) + nodes.extend(iter(nuke.allNodes())) node.end() for node in nodes: @@ -101,7 +100,9 @@ class ValidateKnobs(pyblish.api.ContextPlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) for data in invalid: - if isinstance(data["expected"], unicode): + # TODO: will need to improve type definitions + # with the new settings for knob types + if isinstance(data["expected"], six.text_type): data["knob"].setValue(str(data["expected"])) continue From 568ce1858163a79ad8b63db78c72a5f28bd77a63 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Dec 2021 20:36:49 +0100 Subject: [PATCH 0153/1030] fixing validate knobs plugin --- .../hosts/nuke/plugins/publish/help/validate_knobs.xml | 2 ++ openpype/hosts/nuke/plugins/publish/validate_knobs.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml b/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml index cb5494729b..76c184f653 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml @@ -7,6 +7,8 @@ Following node knobs needs to be repaired: +{invalid_items} + ### How to repair? 1. Use Repair button. diff --git a/openpype/hosts/nuke/plugins/publish/validate_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_knobs.py index 24723edc7a..6df0afd5ba 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_knobs.py +++ b/openpype/hosts/nuke/plugins/publish/validate_knobs.py @@ -30,10 +30,16 @@ class ValidateKnobs(pyblish.api.ContextPlugin): def process(self, context): invalid = self.get_invalid(context, compute=True) if invalid: + invalid_items = [ + ("Node __{node_name}__ with knob _{label}_ " + "expecting _{expected}_, " + "but is set to _{current}_").format(**i) + for i in invalid] raise PublishXmlValidationError( self, "Found knobs with invalid values:\n{}".format(invalid), - formatting_data={} + formatting_data={ + "invalid_items": "\n".join(invalid_items)} ) @classmethod @@ -85,6 +91,7 @@ class ValidateKnobs(pyblish.api.ContextPlugin): if node[knob].value() != expected: invalid_knobs.append( { + "node_name": node.name(), "knob": node[knob], "name": node[knob].name(), "label": node[knob].label(), From d5f5ba5bbfe361349503b0b24c4b0bc06450f796 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 3 Jan 2022 17:40:46 +0100 Subject: [PATCH 0154/1030] nuke validate output resolution with help --- .../help/validate_output_resolution.xml | 16 +++ .../publish/validate_output_resolution.py | 123 ++++++++++-------- 2 files changed, 85 insertions(+), 54 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml b/openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml new file mode 100644 index 0000000000..08a88a993e --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml @@ -0,0 +1,16 @@ + + + + Output format + +## Invalid format setting + +Either the Reformat node inside of the render group is missing or the Reformat node output format knob is not set to `root.format`. + +### How to repair? + +1. Use Repair button. +2. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index 27094b8d74..dbd388c2e6 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -1,43 +1,9 @@ -import nuke import pyblish.api - - -class RepairWriteResolutionDifference(pyblish.api.Action): - - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - - # Get the errored instances - failed = [] - for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(failed, plugin) - - for instance in instances: - reformat = instance[0].dependencies()[0] - if reformat.Class() != "Reformat": - reformat = nuke.nodes.Reformat(inputs=[instance[0].input(0)]) - - xpos = instance[0].xpos() - ypos = instance[0].ypos() - 26 - - dependent_ypos = instance[0].dependencies()[0].ypos() - if (instance[0].ypos() - dependent_ypos) <= 51: - xpos += 110 - - reformat.setXYpos(xpos, ypos) - - instance[0].setInput(0, reformat) - - reformat["resize"].setValue("none") +import openpype.api +import avalon.nuke.lib as anlib +from openpype.pipeline import PublishXmlValidationError +import nuke class ValidateOutputResolution(pyblish.api.InstancePlugin): @@ -52,27 +18,76 @@ class ValidateOutputResolution(pyblish.api.InstancePlugin): families = ["render", "render.local", "render.farm"] label = "Write Resolution" hosts = ["nuke"] - actions = [RepairWriteResolutionDifference] + actions = [openpype.api.RepairAction] + + missing_msg = "Missing Reformat node in render group node" + resolution_msg = "Reformat is set to wrong format" def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishXmlValidationError(self, invalid) - # Skip bounding box check if a reformat node exists. - if instance[0].dependencies()[0].Class() == "Reformat": - return + @classmethod + def get_reformat(cls, instance): + reformat = None + for inode in instance: + if inode.Class() != "Reformat": + continue + reformat = inode - msg = "Bounding box is outside the format." - assert self.check_resolution(instance), msg + return reformat - def check_resolution(self, instance): - node = instance[0] + @classmethod + def get_invalid(cls, instance): + def _check_resolution(instance, reformat): + root_width = instance.data["resolutionWidth"] + root_height = instance.data["resolutionHeight"] - root_width = instance.data["resolutionWidth"] - root_height = instance.data["resolutionHeight"] + write_width = reformat.format().width() + write_height = reformat.format().height() - write_width = node.format().width() - write_height = node.format().height() + if (root_width != write_width) or (root_height != write_height): + return None + else: + return True - if (root_width != write_width) or (root_height != write_height): - return None - else: - return True + # check if reformat is in render node + reformat = cls.get_reformat(instance) + if not reformat: + return cls.missing_msg + + # check if reformat is set to correct root format + correct_format = _check_resolution(instance, reformat) + if not correct_format: + return cls.resolution_msg + + + @classmethod + def repair(cls, instance): + invalid = cls.get_invalid(instance) + grp_node = instance[0] + + if cls.missing_msg == invalid: + # make sure we are inside of the group node + with grp_node: + # find input node and select it + input = None + for inode in instance: + if inode.Class() != "Input": + continue + input = inode + + # add reformat node under it + with anlib.maintained_selection(): + input['selected'].setValue(True) + _rfn = nuke.createNode("Reformat", "name Reformat01") + _rfn["resize"].setValue(0) + _rfn["black_outside"].setValue(1) + + cls.log.info("I am adding reformat node") + + if cls.resolution_msg == invalid: + reformat = cls.get_reformat(instance) + reformat["format"].setValue(nuke.root()["format"].value()) + cls.log.info("I am fixing reformat to root.format") \ No newline at end of file From 01ba3d1c8a21b7ae2dd13daff67b30969997b8b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 21:10:41 +0200 Subject: [PATCH 0155/1030] Nuke: validator ditch avalon dependency --- .../plugins/publish/validate_output_resolution.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index dbd388c2e6..710adde069 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -1,7 +1,7 @@ import pyblish.api import openpype.api -import avalon.nuke.lib as anlib +from openpype.hosts.nuke.api import maintained_selection from openpype.pipeline import PublishXmlValidationError import nuke @@ -62,7 +62,6 @@ class ValidateOutputResolution(pyblish.api.InstancePlugin): if not correct_format: return cls.resolution_msg - @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) @@ -72,15 +71,15 @@ class ValidateOutputResolution(pyblish.api.InstancePlugin): # make sure we are inside of the group node with grp_node: # find input node and select it - input = None + _input = None for inode in instance: if inode.Class() != "Input": continue - input = inode + _input = inode # add reformat node under it - with anlib.maintained_selection(): - input['selected'].setValue(True) + with maintained_selection(): + _input['selected'].setValue(True) _rfn = nuke.createNode("Reformat", "name Reformat01") _rfn["resize"].setValue(0) _rfn["black_outside"].setValue(1) From b9e16e87f9d2b26861b5a33e1bba502ffdfafeba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 13:07:39 +0200 Subject: [PATCH 0156/1030] Nuke: proxy mode validator refactory to new publisher --- .../plugins/publish/help/validate_proxy_mode.xml | 16 ++++++++++++++++ .../nuke/plugins/publish/validate_proxy_mode.py | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml b/openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml new file mode 100644 index 0000000000..6fe5d5d43e --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml @@ -0,0 +1,16 @@ + + + + Proxy mode + +## Invalid proxy mode value + +Nuke is set to use Proxy. This is not supported by publisher. + +### How to repair? + +1. Use Repair button. +2. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py index 9c6ca03ffd..e5f3ae9800 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py +++ b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py @@ -1,5 +1,6 @@ import pyblish import nuke +from openpype.pipeline import PublishXmlValidationError class FixProxyMode(pyblish.api.Action): @@ -7,7 +8,7 @@ class FixProxyMode(pyblish.api.Action): Togger off proxy switch OFF """ - label = "Proxy toggle to OFF" + label = "Repair" icon = "wrench" on = "failed" @@ -30,4 +31,7 @@ class ValidateProxyMode(pyblish.api.ContextPlugin): rootNode = nuke.root() isProxy = rootNode["proxy"].value() - assert not isProxy, "Proxy mode should be toggled OFF" + if not isProxy: + raise PublishXmlValidationError( + self, "Proxy mode should be toggled OFF" + ) From 44655d25156b1d8a1587ca898cc5aaf2a17e898e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 13:08:18 +0200 Subject: [PATCH 0157/1030] Nuke: improving code readability --- .../nuke/plugins/publish/validate_knobs.py | 11 ++-- .../plugins/publish/validate_write_nodes.py | 62 ++++++++++--------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_knobs.py index 6df0afd5ba..573c25f3fe 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_knobs.py +++ b/openpype/hosts/nuke/plugins/publish/validate_knobs.py @@ -31,10 +31,13 @@ class ValidateKnobs(pyblish.api.ContextPlugin): invalid = self.get_invalid(context, compute=True) if invalid: invalid_items = [ - ("Node __{node_name}__ with knob _{label}_ " - "expecting _{expected}_, " - "but is set to _{current}_").format(**i) - for i in invalid] + ( + "Node __{node_name}__ with knob _{label}_ " + "expecting _{expected}_, " + "but is set to _{current}_" + ).format(**i) + for i in invalid + ] raise PublishXmlValidationError( self, "Found knobs with invalid values:\n{}".format(invalid), diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index f8d8393730..320307d09f 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -19,8 +19,8 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): for instance in instances: node = instance[1] correct_data = get_write_node_template_attr(node) - for k, v in correct_data.items(): - node[k].setValue(v) + for key, value in correct_data.items(): + node[key].setValue(value) self.log.info("Node attributes were fixed") @@ -41,40 +41,44 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): def process(self, instance): node = instance[1] - correct_data = get_write_node_template_attr(node) + write_group_node = instance[0] + correct_data = get_write_node_template_attr(write_group_node) check = [] - for k, v in correct_data.items(): - if k is 'file': - padding = len(v.split('#')) - ref_path = get_node_path(v, padding) - n_path = get_node_path(node[k].value(), padding) - isnt = False - for i, p in enumerate(ref_path): - if str(n_path[i]) not in str(p): - if not isnt: - isnt = True - else: - continue - if isnt: - check.append([k, v, node[k].value()]) - else: - if str(node[k].value()) not in str(v): - check.append([k, v, node[k].value()]) + for key, value in correct_data.items(): + if key == 'file': + padding = len(value.split('#')) + ref_path = get_node_path(value, padding) + n_path = get_node_path(node[key].value(), padding) + is_not = False + for i, path in enumerate(ref_path): + if ( + str(n_path[i]) != str(path) + and not is_not + ): + is_not = True + if is_not: + check.append([key, value, node[key].value()]) + + elif str(node[key].value()) != str(value): + check.append([key, value, node[key].value()]) self.log.info(check) + if check: + self._make_error(check) + + def _make_error(self, check): msg = "Write node's knobs values are not correct!\n" + dbg_msg = msg msg_add = "Knob `{0}` Correct: `{1}` Wrong: `{2}` \n" xml_msg = "" - if check: - dbg_msg = msg - for item in check: - _msg_add = msg_add.format(item[0], item[1], item[2]) - dbg_msg += _msg_add - xml_msg += _msg_add + for item in check: + _msg_add = msg_add.format(item[0], item[1], item[2]) + dbg_msg += _msg_add + xml_msg += _msg_add - raise PublishXmlValidationError( - self, dbg_msg, formatting_data={"xml_msg": xml_msg} - ) + raise PublishXmlValidationError( + self, dbg_msg, formatting_data={"xml_msg": xml_msg} + ) From e33c06f95e58db53eb27edbfb85909e6643679dd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 17:26:30 +0200 Subject: [PATCH 0158/1030] Nuke: abstracting function for correct knob type value --- openpype/hosts/nuke/api/__init__.py | 4 ++- openpype/hosts/nuke/api/lib.py | 43 +++++++++++++++++------------ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index b571c4098c..f7288bb287 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -26,7 +26,8 @@ from .pipeline import ( update_container, ) from .lib import ( - maintained_selection + maintained_selection, + convert_knob_value_to_correct_type ) from .utils import ( @@ -58,6 +59,7 @@ __all__ = ( "update_container", "maintained_selection", + "convert_knob_value_to_correct_type", "colorspace_exists_on_node", "get_colorspace_list" diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ba8aa7a8db..457e5d851f 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1528,28 +1528,35 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): if not knob_value: continue - # first convert string types to string - # just to ditch unicode - if isinstance(knob_value, six.text_type): - knob_value = str(knob_value) - - # set correctly knob types - if knob_type == "bool": - knob_value = bool(knob_value) - elif knob_type == "decimal_number": - knob_value = float(knob_value) - elif knob_type == "number": - knob_value = int(knob_value) - elif knob_type == "text": - knob_value = knob_value - elif knob_type == "color_gui": - knob_value = color_gui_to_int(knob_value) - elif knob_type in ["2d_vector", "3d_vector", "color"]: - knob_value = [float(v) for v in knob_value] + knob_value = convert_knob_value_to_correct_type( + knob_type, knob_value) node[knob_name].setValue(knob_value) +def convert_knob_value_to_correct_type(knob_type, knob_value): + # first convert string types to string + # just to ditch unicode + if isinstance(knob_value, six.text_type): + knob_value = str(knob_value) + + # set correctly knob types + if knob_type == "bool": + knob_value = bool(knob_value) + elif knob_type == "decimal_number": + knob_value = float(knob_value) + elif knob_type == "number": + knob_value = int(knob_value) + elif knob_type == "text": + knob_value = knob_value + elif knob_type == "color_gui": + knob_value = color_gui_to_int(knob_value) + elif knob_type in ["2d_vector", "3d_vector", "color"]: + knob_value = [float(v) for v in knob_value] + + return knob_value + + def color_gui_to_int(color_gui): hex_value = ( "0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui) From f8b45e078c74c64a9fc549e35429649db42b667a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 17:28:04 +0200 Subject: [PATCH 0159/1030] Nuke: removing obsolete code fixing family to families as Write is stored there in avalon data on node --- openpype/hosts/nuke/api/lib.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 457e5d851f..5d66cb2b89 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -836,29 +836,6 @@ def check_subsetname_exists(nodes, subset_name): if subset_name in read_avalon_data(n).get("subset", "")), False) - -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, - "nuke_imageio_writes": nuke_imageio_writes - }) - - anatomy_filled = format_anatomy(data) - return anatomy_filled["render"]["path"].replace("\\", "/") - - def format_anatomy(data): ''' Helping function for formatting of anatomy paths @@ -2185,15 +2162,14 @@ 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"] ) + # collecting correct data - correct_data = OrderedDict({ - "file": get_render_path(node) - }) + correct_data = OrderedDict() # adding imageio knob presets for k, v in nuke_imageio_writes.items(): From 1686eaeadad906ddc8e4f20574f4af6c9e697674 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 17:29:07 +0200 Subject: [PATCH 0160/1030] Nuke: refactory write node validator --- .../publish/help/validate_write_nodes.xml | 1 + .../plugins/publish/validate_write_nodes.py | 86 ++++++++++++------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml index c1f59a94f8..cdf85102bc 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml @@ -6,6 +6,7 @@ ## Invalid node's knobs values Following write node knobs needs to be repaired: + {xml_msg} ### How to repair? diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 320307d09f..f0a7f01dfb 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,8 +1,10 @@ import pyblish.api from openpype.api import get_errored_instances_from_context +import openpype.hosts.nuke.api.lib as nlib from openpype.hosts.nuke.api.lib import ( get_write_node_template_attr, - get_node_path + set_node_knobs_from_settings + ) from openpype.pipeline import PublishXmlValidationError @@ -17,10 +19,17 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): instances = get_errored_instances_from_context(context) for instance in instances: - node = instance[1] - correct_data = get_write_node_template_attr(node) - for key, value in correct_data.items(): - node[key].setValue(value) + write_group_node = instance[0] + # get write node from inside of group + write_node = None + for x in instance: + if x.Class() == "Write": + write_node = x + + correct_data = get_write_node_template_attr(write_group_node) + + set_node_knobs_from_settings(write_node, correct_data["knobs"]) + self.log.info("Node attributes were fixed") @@ -39,29 +48,41 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): hosts = ["nuke"] def process(self, instance): - - node = instance[1] write_group_node = instance[0] + + # get write node from inside of group + write_node = None + for x in instance: + if x.Class() == "Write": + write_node = x + + if write_node is None: + return + correct_data = get_write_node_template_attr(write_group_node) - check = [] - for key, value in correct_data.items(): - if key == 'file': - padding = len(value.split('#')) - ref_path = get_node_path(value, padding) - n_path = get_node_path(node[key].value(), padding) - is_not = False - for i, path in enumerate(ref_path): - if ( - str(n_path[i]) != str(path) - and not is_not - ): - is_not = True - if is_not: - check.append([key, value, node[key].value()]) + if correct_data: + check_knobs = correct_data["knobs"] + else: + return - elif str(node[key].value()) != str(value): - check.append([key, value, node[key].value()]) + check = [] + self.log.debug("__ write_node: {}".format( + write_node + )) + + for knob_data in check_knobs: + key = knob_data["name"] + value = knob_data["value"] + self.log.debug("__ key: {} | value: {}".format( + key, value + )) + if ( + str(write_node[key].value()) != str(value) + and key != "file" + and key != "tile_color" + ): + check.append([key, value, write_node[key].value()]) self.log.info(check) @@ -69,15 +90,16 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): self._make_error(check) def _make_error(self, check): - msg = "Write node's knobs values are not correct!\n" - dbg_msg = msg - msg_add = "Knob `{0}` Correct: `{1}` Wrong: `{2}` \n" - xml_msg = "" + # sourcery skip: merge-assign-and-aug-assign, move-assign-in-block + dbg_msg = "Write node's knobs values are not correct!\n" + msg_add = "Knob '{0}' > Correct: `{1}` > Wrong: `{2}`" - for item in check: - _msg_add = msg_add.format(item[0], item[1], item[2]) - dbg_msg += _msg_add - xml_msg += _msg_add + details = [ + msg_add.format(item[0], item[1], item[2]) + for item in check + ] + xml_msg = "
".join(details) + dbg_msg += "\n\t".join(details) raise PublishXmlValidationError( self, dbg_msg, formatting_data={"xml_msg": xml_msg} From e71653fb1672a579dfb96b2843ac55eca4de7985 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 17:29:21 +0200 Subject: [PATCH 0161/1030] Nuke: fixing proxy mode validator --- openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py index e5f3ae9800..dac240ad19 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py +++ b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py @@ -31,7 +31,7 @@ class ValidateProxyMode(pyblish.api.ContextPlugin): rootNode = nuke.root() isProxy = rootNode["proxy"].value() - if not isProxy: + if isProxy: raise PublishXmlValidationError( self, "Proxy mode should be toggled OFF" ) From 9fe4b635174060697a9ea8a9e33f47394a34d9e9 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 0162/1030] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 69a388de1319eb49de84a0f6d846631623fc5a7d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 0163/1030] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a42f889e85..303cd052bb 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, 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 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From 108597f9b1e139f31e6b0f20568866cb2971020a Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 0164/1030] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 199aba87727d7a2417d7f8122dd34f6e4160b467 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 0165/1030] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """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"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + 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 {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: 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 + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """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"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From bd884262b0c001715432f28ec1cae6feeeabfed1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 0166/1030] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From 60cc108251db884a04cef1d2ea29a558a7750b8c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 0167/1030] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From aaa1f13f9d0ae038f70eb2cdc21cba56f92b97dd Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 0168/1030] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From c2aca3422c8c2e29a169f9550e7e1719733f7ec4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 0169/1030] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 95d3686889470a8ad6d677b949a86cab094e47ea Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 0170/1030] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From e29d4e5699e6dace616933317c57fcc9bc43c878 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 0171/1030] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From 28518eeb21f2a9ef56c32c0009ce09aecf871a86 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 0172/1030] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From b65a1d4e79e3fa2ff4ca11392f9ccbce68a19a78 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 0173/1030] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From b095249fb859c9845d00efb8d69bd515867c6e94 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 0174/1030] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From 79c9dc94528ff8f3ae216f106b2225ae790fb044 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 0175/1030] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From aa7e7093df8d72357118bdb34dbe03e4e73d6801 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 0176/1030] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From f50999d0927bf533a74417479e4cdb4a06b32b3d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 0177/1030] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From edb55949df619a81c1828571030634a4b0c49584 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 0178/1030] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 15e51cd6a640aea61eb927b84ce6b48990d206f3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 0179/1030] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index efd22e13c8..2e0e30b74b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, 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 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From c8c36144cb26df5d0024fcd02df265736bbd209f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 0180/1030] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 770b6d3ab2ee9e3bdf460cee4fdba96d67e44fb2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 0181/1030] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """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"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + 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 {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: 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 + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """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"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From a5a3685f2b5b99bbd1f8de78581eb17af0175ed3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 0182/1030] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From f2ae0ffa5950d922fd3cb90ce8bbf30ec64ca0b7 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 0183/1030] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From 41a47bb2bfb9b728f0cad37f5614e5d382b2d9d1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 0184/1030] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From 58814d21e4688fbb13d183fab7ba9010c68b57f8 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 0185/1030] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 6cb037d3d63290752bacc0aa8c2b81cac8e3b370 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 0186/1030] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From aa88ee13c0d3b647dc9c11534ed4a742168b0e1d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 0187/1030] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From d8edf2b1aa9e83861bad1b8aef6da69cc6011de4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 0188/1030] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From bae9eef400e2b1195d8ece023543ca8d89c83b1b Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 0189/1030] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From 349d57a4a8ec86364d64b02798c7579f6c3cb5c2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 0190/1030] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From e2506d569adf78c74ba6452643fec5d1afca0ab2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 0191/1030] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From a6d948aa93e2c84c001a109c50dede9b9c160321 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 0192/1030] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From d6543bf281a418fe768059a58a1a9eb8257ef68f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 0193/1030] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From 4a02dd039de637398aa6c2a1d2c26ba772f720da Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 9 Jun 2022 14:51:48 +0200 Subject: [PATCH 0194/1030] set empty placholder parent at creation --- openpype/hosts/maya/api/lib_template_builder.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 108988a676..20f6f041fb 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,8 +69,7 @@ def create_placeholder(): defaultValue=-1 ) - parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', "", type="string") def create_placeholder_name(args, options): @@ -131,11 +130,11 @@ def update_placeholder(): defaultValue=-1 ) - selected = cmds.ls(selection=True, long=True) + """selected = cmds.ls(selection=True, long=True) selected = selected[0].split('|')[-2] selected = cmds.ls(selected) - parents = cmds.ls(selected, long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + parents = cmds.ls(selected, long=True)""" + cmds.setAttr(placeholder + '.parent', '', type="string") def create_options(args): From a97f5379b16f4141035629aa23d7ca26f16fdced Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:17:30 +0200 Subject: [PATCH 0195/1030] Add documentation --- website/docs/admin_hosts_maya.md | 50 ++++++++++++++++++ .../docs/assets/maya-create_placeholder.png | Bin 0 -> 31543 bytes website/docs/assets/maya-placeholder_new.png | Bin 0 -> 28008 bytes 3 files changed, 50 insertions(+) create mode 100644 website/docs/assets/maya-create_placeholder.png create mode 100644 website/docs/assets/maya-placeholder_new.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 93bf32798f..c55dcc1b36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -120,3 +120,53 @@ raw json. You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** ![Dirmap settings](assets/maya-admin_dirmap_settings.png) + +## Templated Build Workfile + +Building a workfile using a template designed by users. Helping to assert homogeneous subsets hierarchy and imports. Template stored as file easy to define, change and customize for production needs. + + **1. Make a template** + +Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. + +![Dirmap settings](assets/maya-workfile-outliner.png) + +If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** + +![create placeholder](assets/maya-create_placeholder.png) + +- **Configure placeholders** + +Fill in the necessary fields (the optional fields are regex filters) + +![new place holder](assets/maya-placeholder_new.png) + + + - Builder type: Wether the the placeholder should load current asset representations or linked assets representations + + - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + + - Family: Family of the representation to load (main, look, image, etc ...) + + - Loader: Placeholder loader name that will be used to load corresponding representations + + - Order: Priority for current placeholder loader (priority is lowest first, highet last) + +- **Save your template** + + + **2. Configure Template** + +- **Go to Studio settings > Project > Your DCC > Templated Build Settings** +- Add a profile for your task and enter path to your template +![Dirmap settings](assets/settings/template_build_workfile.png) + +**3. Build your workfile** + +- Open maya + +- Build your workfile + +![Dirmap settings](assets/maya-build_workfile_from_template.png) + + diff --git a/website/docs/assets/maya-create_placeholder.png b/website/docs/assets/maya-create_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..3f49fe2e2b801e89e2027ec521dbf700b97f80d2 GIT binary patch literal 31543 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 Pn$XtJKYU;9ROtTzYS3%b literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-placeholder_new.png b/website/docs/assets/maya-placeholder_new.png new file mode 100644 index 0000000000000000000000000000000000000000..106a5275cdb5bc0588c811d9a059ec3b6e072e20 GIT binary patch literal 28008 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 literal 0 HcmV?d00001 From e4a7dd6548eec1c3d85bdf133c4f273432313c12 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:27:00 +0200 Subject: [PATCH 0196/1030] add sreenshot --- .../maya-build_workfile_from_template.png | Bin 0 -> 20676 bytes .../assets/settings/template_build_workfile.png | Bin 0 -> 29814 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-build_workfile_from_template.png create mode 100644 website/docs/assets/settings/template_build_workfile.png diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png new file mode 100644 index 0000000000000000000000000000000000000000..336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8 GIT binary patch literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef87861fe97322a3b23c28ee3644b76ce995e9d GIT binary patch literal 29814 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 literal 0 HcmV?d00001 From 4441a7cc1c07dc62fc04bd7428bd2f7efcdb0fd9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:50:42 +0200 Subject: [PATCH 0197/1030] add screenshot --- website/docs/admin_hosts_maya.md | 5 +++-- .../maya-build_workfile_from_template.png | Bin 20676 -> 29814 bytes .../docs/assets/maya-workfile-outliner.png | Bin 0 -> 4835 bytes .../settings/template_build_workfile.png | Bin 29814 -> 12596 bytes 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 website/docs/assets/maya-workfile-outliner.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index c55dcc1b36..0ba030c26f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -129,7 +129,7 @@ Building a workfile using a template designed by users. Helping to assert homoge Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. -![Dirmap settings](assets/maya-workfile-outliner.png) +![maya outliner](assets/maya-workfile-outliner.png) If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** @@ -159,7 +159,8 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![Dirmap settings](assets/settings/template_build_workfile.png) + +![build template](assets/settings/template_build_workfile.png) **3. Build your workfile** diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png index 336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8..7ef87861fe97322a3b23c28ee3644b76ce995e9d 100644 GIT binary patch literal 29814 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 literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| diff --git a/website/docs/assets/maya-workfile-outliner.png b/website/docs/assets/maya-workfile-outliner.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd1bbd03bce52041ad511dac138479494641361 GIT binary patch literal 4835 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 X`o37wi;`y6#S|Ob%*wRV#3TNnn_AI~ literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png index 7ef87861fe97322a3b23c28ee3644b76ce995e9d..1bea5b01f5f3145e2d1aa5d46545c3739aaf2584 100644 GIT binary patch literal 12596 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 literal 29814 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 From 90754fb0b8d36eb0b34cba6da48f672505470f0e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:51:02 +0200 Subject: [PATCH 0198/1030] modify readme doc --- website/docs/admin_hosts_maya.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 0ba030c26f..0e77f29fc2 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -160,7 +160,7 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![build template](assets/settings/template_build_workfile.png) +![setting build template](assets/settings/template_build_workfile.png) **3. Build your workfile** @@ -168,6 +168,6 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile -![Dirmap settings](assets/maya-build_workfile_from_template.png) +![maya build template](assets/maya-build_workfile_from_template.png) From d3c2dc57d8f39865c3f3900db1be1f911b6436c9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:02:43 +0200 Subject: [PATCH 0199/1030] fix linter --- openpype/hosts/maya/api/lib_template_builder.py | 6 +++++- openpype/lib/build_template_exceptions.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bed4291e3d..e5254b7f87 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,7 +43,11 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') + placeholder_full_name = cmds.ls( + selection[0], + long=True)[0] + '|' + placeholder.replace('|', + '' + ) if selection: cmds.parent(placeholder, selection[0]) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py index d781eff204..7a5075e3dc 100644 --- a/openpype/lib/build_template_exceptions.py +++ b/openpype/lib/build_template_exceptions.py @@ -32,4 +32,4 @@ class TemplateAlreadyImported(Exception): class TemplateLoadingFailed(Exception): """Error raised whend Template loader was unable to load the template""" - pass \ No newline at end of file + pass From bc9c5b183171b9ef03b88062c7536a5effed3ae1 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:22:33 +0200 Subject: [PATCH 0200/1030] fix linter lengh line --- openpype/hosts/maya/api/lib_template_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index e5254b7f87..a30b3868b0 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,11 +43,8 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls( - selection[0], - long=True)[0] + '|' + placeholder.replace('|', - '' - ) + placeholder_full_name = cmds.ls(selection[0], long=True)[ + 0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) From ba1abf8b15e1476b430180bc63b1a3801ff118ef Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 10:35:41 +0200 Subject: [PATCH 0201/1030] add task name field in build templated workfile settings --- openpype/settings/defaults/project_settings/maya.json | 1 + .../schemas/schema_templated_workfile_build.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2e0e30b74b..453706ff88 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -722,6 +722,7 @@ "profiles": [ { "task_types": [], + "tasks": [], "path": "/path/to/your/template" } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 01e74f64b0..a591facf98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -16,6 +16,12 @@ "label": "Task types", "type": "task-types-enum" }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, { "key": "path", "label": "Path to template", From ef7627199eb8297196c9d0a778e222132d742be2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 11:37:08 +0200 Subject: [PATCH 0202/1030] add a task name verification for template loader --- openpype/lib/abstract_template_loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 159d5c8f6c..e296e3207f 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -160,6 +160,8 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue + if prf['tasks'] and task_name not in prf['tasks']: + continue path = prf['path'] break else: # IF no template were found (no break happened) From 712e1c6707a9b0b57bbf1a50f108d692c6f9030b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 14 Jun 2022 16:00:51 +0100 Subject: [PATCH 0203/1030] 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 0204/1030] 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 0205/1030] 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 0206/1030] 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 0207/1030] 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 0208/1030] 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 fe77fe64adfcd2bf2c9eb77d6c5181c0fb20dd5f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 22 Jun 2022 17:10:36 +0200 Subject: [PATCH 0209/1030] add new studios to main page --- website/src/pages/index.js | 27 ++++++++++++++++++- website/static/img/Logo_On_White-HR.png | Bin 0 -> 77588 bytes website/static/img/NoGhost_Logo_black.svg | 31 ++++++++++++++++++++++ website/static/img/agora_studio.png | Bin 0 -> 133985 bytes website/static/img/igg-logo.png | Bin 80331 -> 96336 bytes website/static/img/methodmadness.png | Bin 0 -> 8650 bytes website/static/img/noghost.png | Bin 0 -> 22435 bytes website/static/img/staticvfx.png | Bin 0 -> 12912 bytes 8 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 website/static/img/Logo_On_White-HR.png create mode 100644 website/static/img/NoGhost_Logo_black.svg create mode 100644 website/static/img/agora_studio.png create mode 100644 website/static/img/methodmadness.png create mode 100644 website/static/img/noghost.png create mode 100644 website/static/img/staticvfx.png diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 0886706015..ae7119e928 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -153,7 +153,32 @@ const studios = [ title: "IGG Canada", image: "/img/igg-logo.png", infoLink: "https://www.igg.com/", - } + }, + { + title: "Agora Studio", + image: "/img/agora_studio.png", + infoLink: "https://agora.studio/", + }, + { + title: "Lucan Visuals", + image: "/img/lucan_Logo_On_White-HR.png", + infoLink: "https://www.lucan.tv/", + }, + { + title: "No Ghost", + image: "/img/noghost.png", + infoLink: "https://www.noghost.co.uk/", + }, + { + title: "Static VFX", + image: "/img/staticvfx.png", + infoLink: "http://www.staticvfx.com/", + }, + { + title: "Method n Madness", + image: "/img/methodmadness.png", + infoLink: "https://www.methodnmadness.com/", +} ]; function Service({imageUrl, title, description}) { diff --git a/website/static/img/Logo_On_White-HR.png b/website/static/img/Logo_On_White-HR.png new file mode 100644 index 0000000000000000000000000000000000000000..c86030e1e78092c7ecd1de2cd129198c99680ee4 GIT binary patch literal 77588 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 + + + + + + + + + + + + diff --git a/website/static/img/agora_studio.png b/website/static/img/agora_studio.png new file mode 100644 index 0000000000000000000000000000000000000000..48b07b877504267eb302aa7ebf9e0b49540ed686 GIT binary patch literal 133985 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�Hb3N@C8UbRsXio1UqFP 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%0A%VsM|U#I>^O_ z?=+fqM)|8>fL4qJTYnJ>RrfDc-qMS?jcLsH466TKphUNpdbv?*@%`v=*z+Q|ZlnO) zp6{9*5qv&aXUw4!l0gxYbL-RmXMUD-J79Ic4)O`VmABB9Y<@@nc}{fZYW;ECee z{1Y}4o%Ca-Idl7z#6t}zYa$Y}tmF~(l9dx-XXjJm<10#g?i>EL>Np!VaW%d3wRf{b z%3`QlOXRRbmak$_Q&STYtLkDxbXPj}QdJL5i}m*9#^?FfzdmXXc}_-n$ixM^!LxKQ zTdm;8ioY2b&5(@Va*3*+W3NM=6v=l}p*4kKDd?$!{eNg{IXWwrk z=4q+LIvRsZc&r>APb3;?$4o^nZ*HGWWW0pIY(2+&dSO1LG`Z}aK+X3L$Wm5B=wCX> zupq|Bqo^BoIepWmeZ}m&3_)`6eu=-oy?wjy812gDc4{v1Cos9lefid713Eg}5k*3~ z?hs|~bep;tudlE7VH$1j+A%JCUAuxADsub7y`_;X9An6wygZ&ze6f6UnNTZ*dPQM5 zc6%owp3Crgb%+>w(Ost&f4CSWis`d9jiOtBZNf!Q*|6wD8@F^n)>!oqtOr=gn-~ z#60QvrA-NKdG1wy8u@(+iSg+MFN1@F$v~3I(Kv7KYt~f0k1L|P>RuCn)|-g+ce}Uz3mc6*s&r+@Yf zD0kqnQ4sHniA2CmQ+&qKtSbOx^cWIMayq#o^DzHJomykE{$FHMmo|leM`&ctZ0v^P z>ttg)uAwq?8$b)4L`sSn8 zDQtWMsmz7!h;$h|aM?cFzA&Gi4W} ztXt4V@yVvQL3FoW+B2QoNtI^{1j8nLEG;bZqcc?AuYoZt%Z6C^ALXVg(LX+$9XEASNt5Nr}irwIdPQGz5O zOf-S1S`IdQg!KKp|0|(ZBip6D(A0%rt7B(puPdAuQcx`$+|I*$>R=QX6_vJcVth@v z1NJaiDh!x2doNdamasg%v_1sUt_2{?NE_tPKWk8RHNP)0IWf^k6KlDPdYmqmc_D7@L=AY2#PaAej%KZ$2ka#P5r4}H9XA|i(Xkoq=O02>Fdg7E&0zN(|N9tKJNCP8pf{~4El@=-zlaseEV5fD^gk)W~ zI&(dx)2N*u6R{uC+!l%6>X_xptx-X-F#IkT)`VhO6FLxnNx?lmJ)=?df+098_tJxo zVTfp(;c08Xqj8zjLPu`g)G2|@|9)$-P@U0WdwSS@)dw|^f4T7#d%>qgU4=uHBS|+Y ziy_*8>XVXPN=Q5Al; zR~hrNOM{1{Vj71N|MklvO-TE~uxOF{F#t<1& zQmm+Be8hTvAHXtMaq@RHuvq|3r^i?meI6}hYe{mK2Ff)C@{fQRJq_#LRX&Cc!k2&6 z8*IR@b>n57-u_|7j6k>&p&qyG9sJr7y`iO_(t-luJY1TuH?oVox@!cCvlHEAUg$$V zc6hExDBz`F7%wLSZLAcx#ei$oFgYAggLGOH5b`j?B*{8|d+&Lq_s?A?!NRlLk@zZU zvsWwY^~$5<)m8guZ-uv@o>t!s&egrZRid_h2R;MWYN%@Yu0qtnb(>}Y>gO&uGGQ7e z%v?r<7U+;pEdn`v%q4cT`aPp#%>Nc61cdsC#QU{Etz?Q6`>3Jopikj1ptAK%G{G_? zRfDM{Q>ZoI7Hg9Y7}!4P+13dFfD>{t%FN|E!eLDp;>1;(J@ZPo)6XGDCT?ykH*CA? z$8-~!wrD_v_sxG_uLtCd+5pX_;5I7zt8osQUIvg8gr)V@m6!Kh%U^34;Y89S#fz`b za7RIS;F2q-C`SWswM(pIZ$VavuFZ&Sa|7yPt8b^i;D~S8hVBQ?(O;e z=Z%2{K@OcK4JkN|)my~8(#{5I;R66mdoVg;8U-o&qKExN0%dhXgf@6sI;d$o%+Q+; zt!%1VTP9V*wHBEjbn`3T#h`X$`R^JF26m`pr#~PUXWR?}w7c~LzS$L4fGiv0M6UA4 zIiGa+RrzN%9IRFqb-7VEvBnCUbp%-_si3g=uFc5j4!A{H_jI)~;SKzJ4Iat+2UjUX zSbpob`MaikT1Y=@aBk-YrurO8WZ>b{uGlQ~9#gxYpq%~=ytpGSLkvi)^(hc)J+0Q* z0ik-{BLDnhNO6>(;=J9k)}_GD7tRt~RWZIMPEL!e8X;c~!HoqMO4F?!+uQC|2!8Dy zTMj*Cub(Lz*X#Anh9>_+im+~_j*bpQE%F_}>Hu5LXdLk7D(JDFE8HEnk)U!r?w8!D zdsALsUN$Y32%9zHLhg#8cCfZ>+aE)96?(V7u;m>7pM@b3oF**C3*V^~3Y!S&BhirY z{Lg`)>drt;sf~@Yk#A*;{#cac0IL?1m&=a2#%U~TBNu<+`-#WC%BVK_sYP-fbLiao zV^<0fh9oT&0{QEg_+za#vD3rJ*DVKmmj;0mRCRRnAsOK-{eHz56;>paZ(VX{w+~3S zPn!F?id)sNOydC8Sc}8B`Hl-_s5E$H54Dn)!4G)Ab>r01I~S9de%>L!{EX_A4>d?p3jv#*WL4>Zr7#ck08(kh>f2 zpYgYpE^z)a&SIKk{rYAfa&1+A-!B;f&KGfz|0L&agQ=E8m2UWKNAXY03)k+!?t)ph7?Wd4Oiauz=k`n&*lG23 zFgm8%!`=ONLT+_CCC`-oJng#UkDbnb*AbubDaL&LahhNrWSOyGbX(wI8K8>GjjBh= zhgOhIOZ)QnjQ;t6XgiHTAY?Aun53bfGe9;+k&E3>o5{CbDG_M|CN2S|Yr$cSc`q|c z=0eO+2FR>H%=jmxK{3I;Z1W%dT1x)_E?NyyG}f1#Pakz#5#q;XQk#l^}{BPb~>Su&9Gkw7j+n{g88;A`FQU69wXw>^tW9OJASg?x6`3aOkc&mSXLC-E zGodCT4;t>dv@nXJ;N97T`wb+)hu&$Ytfrg((~K)5G|BU<`Qll|fU~ zAe~4c%rfS6Z7F65Z2NZ~ZTST`l1FB=lEhm|4(n~I7n2VF@%*?ifAa*W>mRQ2yjRe3 znrL)j*@Nh==ctJwIZ`Z-tfL98X~wu~&RsNOuO8QOkF2^QiJFfeTcIqC-~^dUSsHYi zcA@*1*?*&<)?n?Y5=Yd{muHgrTJ z6VJ3d7xvuW?gx4=mUbBjJBHys4KW66Z8Zxf5SF-slCGQ*S8O4eQw)f_GN>$PfL19h~0abWmMWgbRfx+3#pED(hoXD z*)~D|3kC^wZr??tYWwd!j_xwRY<--aoh>0%H3LHyQ_{Z7=)6$8^HP_1+kD;bj;^7h z9}ZJ3fvT;5*nGunGaXsTflMsQUZWm5hFs;6bH4Y*aj$+97z)AEHV<^UU+pxrxtR`l@xg*_rwHFC{Py{o$=mLNrb};rzSUg9={1 zzFj`Zh3pK&XR0+;@lOoY_^g(;yC=h6{^n4h^0&X%;@4aU)f`FgJ@IbxPsVtYULd!* zBqw*PT&k_S8V1;4D+r)*7-C#hMWTzT>8qcv^5lpTeqYN{C7$kyFIn1JeJhuE?Nu9l zkc*Lc&z-M1#kDqTr=hSB7isvd6}gy&2SIaU&*;yA$olWekyh0jQ)QC*Q7FQ4Io@;h z)2B~n?R0QubqTnQ-e{Z;b!Qxl97$JC&#Rn8_U@e0_zeFv2?2Mz% zyq+w-#PK6U8`C&~UyG-hWSIZFHF>0l4mDANbh6HwL#5E}3Zv$&5veT`40F&#&sn3m+OlTkZ{50uB5mnv3iD48kc*|T zz7pvb?@*No`2|HQ%88#}WrKpK6=tZ*m3eRXR}*8{?^C%)gHE=@_-0LWS-uWSTT_NI z(g=jodfzcfWtV`PEw=lu+~YUNb-RV`RH<2EC0?D%r5@xFOFPAK`0E@Bl9Ex5Zt=fi zWV|7yV}pM02>oavJpIXGG@(P1d*a%RD%l5G0{NkM4hC8WsIjd;9saI&B-vc$al?28 zYyEKx)Pw?RLIdpX*Suk-%!w3@!nX@F4>IhYMMcW|!OY8p-EYk3Oj_Q{jV8JnT}4Nj zg&pcG5)$QQ3~`uV-7LGM)1!{o=-#>|@Z;Nu=UROvx4Y}M7z1|X>fCVEY6gC-9m0sB zoxcp%{yk|si-Q0y3^}wU4+E_SWbyyt5;C~|$%S$vSUa+0((h<})#ohj%=R+G$mdXe z-EpKFW;S$MLGKf-?^Zc4?0WrSH7L^B;-$ceezxvDyY|aL<0BX&c zZn+b8wJoR|=voVJAyV(lAD}FFL7Q?lf}}0rfXu{pDmTLx=xbdc-OmMz6Vmpzg>k3u zi+hhwz)5o>OhT&ABww4fr=|bR0vKaQTrL$$yHk-LlAA`u4-}VE0!g5uT}vGc!{>hq z1Ojx5B1uWN6Wia-OYITQZD0CG{<~<>e*wi8@ku zbNB1wr4Yu5gumxG3FqnET#4=?B{II9m*isfP^W^OhT>&MT=&ism;v5W+W#L%=_(M} z1O!N2r!`ZCG)^smR#orj`*WI%eYA0bomQ?wB;TTGu33?WaWUt1jh49R9{k!h%+TOp z@1U$FaDueeN&zoyjh|GVXhEfh;R?qe31Q?en&hD@*T3>Hg9b56D@WRW;Zmz`idcx0@k3A-Nt+*yTi)$t3T^LlaIbeuRLDwRILx#O2d2?Nt`~0&O4UT0VaKXg{!D zv0W}e@)OOTsKHZ2T_dQE5ac2u!=+%!ZkBpPbv_hd#mFr$Y5T`^^^#DNx^|^~-S9hg zagY1B2z0ZQBk+@d@e;=ds6O~%&qqS>PThL_6@T6n^(g)2Uc~2l35IwYM8^~fnG!n` zBNN)+wMV-|PEAb>-=a>9#5W+FJpMu;BN)*45RjdVm+f_BbkFF{)KqJ)nC5uy4vjYt z0E^*j@ZKpESzzz`rji`+dpDYF89PrMiC+UfT|B)BR@%qXV?0zBsWHpPMY0v{c_Ijg+)b0$33Y2s0{3XI}3%Kz57||56Pp1 z2FZjZ>$kCNFBHg++7Uv{%lWP?l>dzcd(SnMBZ)$x+eV&Fp+RJX0(K}H>i>mCMg(Oo zO70laFb=unbVhN(_S}SFeY)=12M!Lhd(yD}3+zl1fYGU&Xnu%f7fYtDl|ap(hbO)M z?@5q@etr`094FuFdZ4sy+_7Ar(rX0Se*3>CJ!9c`NhLGn*nlmp-Lm@WJN2^!V}*w78bzFq!Vt8ZMhzx1Hb=Aqgc3t>_5KV-gvuqZiGv&Z@m z=ghpkIH@qeR{p29$_SIgu8WtP1p?B%fvMwW2uwo^9XfFre zn*-o{Lm}pf%VkqZno`NZ(;h#+WDOfpN1-MtneAWd-CL~T|0CXOWl1j3hq(*@71Y>J zfqEfaPTo%J-h&r(;hb&yVR(ID64zpSS=3(XNcyiJ834EEEQM!&k^?0i1wP?gM88JG z8%g2TKZcrv1+-0|ZgmbdZ!J&PZ7s_+u%F8bg|ZLDzxZ!Q3g8B;P%n`nEN5l7h$Y9w z#=fXWrbd)_)ldE(?+Fkm`37{$#VNT{DUUrCQ#el9=U7fO%yyi(Yi*5&SU++UnW>GP z%UBXS4-4^=xX^PySM4HD6LoN-R(}b>y&&6Ii#ARG@+F~Oixd@1|9;=cqN^0`_&^ZM8Q)!$t)z9OjW(tn>+MS0S*q@_(9Heke+ zG2w+vWKWNd!h=3vX>d%}AD{no%s~pdgndg=j(1U_yitlXg0;H9> zf?;U;W_*HHEz~ZDV_cMaT-gL>mZ!Tdr~>r3kpV17kH2BvqzawLh^RM?aoLH$dtQW$ zqgyZTNm6n8OWxTGd^zd!t4r>U&{vqruk)8iiNG|+aKP^NZAd1bEPDFtwR?$>hK@yC z)@iQ*pPcek6oFdREGXBMA|VAGof2)VPGMfp{gQUw%9R7LzBeZqgLap;$4+&$dMfg$ zwR=Uo@btKy=VA=W&eTjbg4C`RDGv3%2K@WnTQ+gHkE&WZY^sG&?1UD&Pk35+de8mg zO3RKEN?)%OUoMQgK0jJF%4CdiPoBxEvX@4s@E|)tQ`mr5yB*mw*)o+hI8$fQJn0ng zl6TjRzw=tV+*Fcf?{394bepSuas{N|?pSNH;?DiMpOx7yx46A0znduV#bsn}KWUjnp~6;LWQ?Bp zSylLa&zChzww6{(H{ofCHU8l@Rw!|Z6w$J@L3YXR;Z$w3+Af3&IprFwYaQXUkhis^ zcUrwMI+qGJ*9zt)2!?g5Y9*ZsyGtMvr;3CeU4=Ti^Qi>sP9XsOYFH7b98D zeYW4~d95$P^Je!ByKkVO&1kLPE;L8B3xCI+b1y$~`g-HXkJxVUj3!xBEp*oAUR?^= zQl~1-^{AfYjs$O)y1Ir^g&dd*E8We=M@St)l~lkIV6iI6!GGD-sZ&cXy8G zgevAvz}5qHV;NIR;V6?DZT}ziO##!X9fHwnh0@I@qM#*dCCLdxBb9kGJz#anQT@_8 zf8>G2TLJ-54EfEVqwLnV#F1rzbY+C+Q>f0L=SLacikTXLvT_(c+@;+Im1d};lSw23 z9>P~PALCb0G9-iM0gKdgy=3l**8OwMOa0U{4&VLDHy=l+cug9ame+22nuR#$$h@CS zg6D8T9i|3~Z<`I(mLin?S= zczn;3DQ6mfuz3PI@?G(s+OpjC#W#0y1zXRBrFs4ce|ub%x(``6aKGdyQ@~ooWMv+7 zkQaacyl)5@JTx+&Bb}TTc}5|%jErcR)V!(V)-cJtyH@8pJYz)Szy{j=1Rz!~$9MRC zuRX0eelXv*Y>?6|&=hQSE)@*>#aCq&p5&r{k{)6B{qng2k9Esj`s`!#@bmxPQ_kLm z;>M-jPtBImA1voDj|fJEbokA?EHy7(PS;XOm#qzebU%M)qc>^E5~9d6K!dUJ@4~3z zimS#b#9-WQHblnFy3x97Xy7M8nHFSN$zD0c$mrI-fUmr&jZEC~u0Nt6*_jpTGY;7P zlcn7CL%O>{}P&&QbSWuytF_1r%?xmbRlCo20^c1hu6#!>el&OAI8q{ZgK z=jnJ3C}Z`M9BxiuobJwCg5Q}bDUVb|uU{Dol|gd{IxNPqNJ^V@%_(n1J7~9Fsa-XuZm)1XCyu?YrSGt(2}i%iOVz@ z$JspKJ6Y}Ty8_%C)vGC35dbwNoxYU_ITAml3DLNX*$|~=zDXg3yPymwLi@g{DW98s za#RM4I=ZrzUT>GXo+OLR&d%0Qix+T+!W+v%1kDAwxW@qR)EemNRs3oUJtBIXF+>!L zHsnTXjucyVs3u>}JTD>f{7&vIDV``9_c6sA^N%0Kq6@S*@;fg2-_RNYe8^7z__=bd zB^`8Xf{PMV)d)hZn(d!H`GWDb$@sP9o$bxo#Emg&C|3f=*=*S(`-OS~IT#n}*pPZN=W=+g4?HJCf|^}%MrNNQ=bqwEtOa*fQ-frC*q%}A#cC?)qF zpZOHx1DAdc)@=`V7#bPvz-+2fFN}f$I$dT&zm(pc!+(#*$La0*-Osg=ma>hH=<Kz%w_Pj7o;E3_PySuY}I+1}<44v=4k}YZsd>0el)%vl> z*S%`k-??gdo>lc(FPU#(rbRN=a!9-Wx|d#q^U# zAdNl#jaK?}F9sQ7r**gS+ZFI=iP>8!rS=_I)-s zHr5!bd@9hQOpcHDx~ms++>B>Hjueqrc4PTn#$dmOVi>-24`d(D{S(jG5SR&z4(@In0RqYf*y`0zAcd2Rs_aoWyX`dLkPUjMdPnVA~A zzkNqk$4cvI{zZlx%s4^S>g@~2Zlup!*NcyAMH1V-7sbx{xw~5eTrAqnHJFlI4(8Z*SaIP>ANbQJ{jVzYnV%$B)!JtGC{P^v~Z6j+RLkXl# zetUa+*+IUYD}5{K)DP#R&@1< zz$z+51{02YgWI>+WdcKgexeY*0%hiK=Uxv{rxQT!a_QAmEv{CMz$&NK`qn68w`|r{;GWPb-N}IAvkJJyRF0S$+rISLMUODh@IXW>8_Q zWPW#=z<~H1%uGGhck(_*lcVmtIi3_c6a5%-_Cvwflu~&GSq}{oqE~mfNXopw%WtC6 zi0_(MkXjLV3@RbRS|cn?QZ91ol&aQ9y7is#KDW-UPwnlf*j6vY$G0u06c3IKBK+x3 z96r(4)6-PZk8vAI=iRnytstdrwg?Lg-sJ8u&{8%%*<@}oVa-5=H}?Gk{b3%&)$J@fY8TbqK06~LE?ODdVp{F z?%0gkm^$Y4I4haAJ+_xI!#V*b`6RyD{*HH*-|qNu@7MIm$jIa2K)$r*=KZX2l!~oi ztw%Mw-@yQg-JpLykrNkrD1vhhmsR=UEKI(k1tMFsfXlmnt=^Y~vat!{*r;2#7e=~Zlcb1I}+4{e)@arlPj%_y-lg;RYlcRZ1XjY~;k zqoJW`o}Cr?H`+GgMB3vf<~aQ8$1pX`6=-mh0t1+tZV|09HNiX~NNV8*X+??^TEC0|4+ zAq*6lJHrZu=wUGBZDZrUL&4OxgXe=p9&tQSc=iM?U3{MaiYT$@IYcc+gLro3n-*W= z5ooBE&Xv)uJt7j>5Asou9kkW_wzTej-~l({7-Ex3@z59@;_^6c-O}vZDI&M>`UlMs zSs59RD^aA^MQmtR7TVRr!_3*a=u4CK#?*PiJeSb-$9XqC zhm1sbkd!m52OwpXrss1-n==Q#Eulf?zUitowX!78ninCtevOw3rni=N87sWbf2`p#L71NjHg=E5G-vCWFRob`1GPg;BCBpNd8 z@>taQ-?~0$UThR(SloJ(acQ=2}(LZoJ$r8_^vULh^32Pmf;RR$EfO_Pa1$=BrSZOVtyK^Xz8-Yetjk|B| zu-YFm2=r;m|Ju_t<$0S2nVbjRsDPj}!9KW2nJef9pfWy$zpBgDDhcE3=#qR-|juN4n4z898oL008f@9oK_CP&TU^p%6SCVxC z2PdZr>Y42Q<)uX>)(gsk6-UoT;$MF(SPI9#eyfT??!R!M`%NHBQ`!yAP;I?P6C`&@ z+6j9`X@3RcO?jP7;FLvlIjWsHPdm1IMCHjlzs_iyrDiX0ywKLW?>6$;dVa@U5=)LDKM@%k*l_r4>* zm+Gmo5_2eqMFp>!P2BK86AJ7$n{XdzV3g^^Bi+-F##SRwy?&?lc|XDokwUXTk8nb0 zBZLrMj*Svp8eU@$=z8^IRz6UO-dqp(Nbd+Wv!L513sUDui__EVnT0;9_6$r+sq=gl zrmTJ0m^Z#;pV7_UD`;gij^?U=qiq-(tlllX6-8?4T+QsJAVc7}+}9r@vxxfSdzUd^ z3d)~pVlZl|71|rDM6s5W)YZwAu z9Le?5bPLDJCzE}`l^xz4_M_KS-j=*%oW|+gDqL2bQBtxZ(bm=$yFciy1;P{oqfEZn z1=5J-dTid_-Y{E3^{K5b*$QrYndbe?ymM(Aplmvrf>lys!nrPg#ygz(A#7O(o+d6P z7KlMnwH95^-1I1IULC+hD0WRIijg(I{D*n zGb)4lGyD;=zoTuxmUG2Wysfi{8c+Snz-`11oqa6%!9>TPr+;iA-1hW-2Gf*PJI3KBXwAk+WDhCf)3T75=(&{=~_VvaC@z0Dy zCp-?>-~MD&{(!uTxbp6>kFetC6h#C*_M1FD`a`$IdnHp}Uw_n=<1%_}YP%M5Qv#R8 z>OMadT39H~dMNl*E*Qknk*N~diEg}G7!Fd-66Rk{JvMy=l&K^+W+RRwp3k~@ADx~W zMM9Trcj6&pum@l;H<7W%{uRtBYf>5#tg4-ln8TP=#5G(ZqJN!1Ao&_wkE-ekKN5Sy z?B?SzH8jLk^mR7g-gCf_T$A|N$HPH+2-&_%BeUE_{nS%wK^P$>*A@R-X~=sfH{Zd| zZd-ZWaSc~DI{IlW7Jpw$B(`Cs#)oq0FycDJ6_!Z1kVnVkdBUjq7yc*qB#WFf>mffS zF2=eBu_tWhZ}S=Fw?W6epK|8l;J9!u-r(a868n}DLYhb36>md_x^G4C!?ZBZiO|z0 zhhCnmS|f?pZT-5+*J#Su$W}fYf|WoVMHB23zMCWRm{qE^w4#d`SjckAizdJh)&Y6)51o#iEt# z#6`!?&(Cq}*hP`pvn`cWj+uQ<6ud`K@SgknNN_`-&u9Vj1f2?A4e`T~W+M1$`La`n zb>4HF5$zZXL87ucqVk6N02{fiD=fJwZEcgypX?lT4^7}FL`P1_I?~*sv)9%4?uWT% zgiwo<0NhjnsNG=%Bh`57Om@aEIkRjLce1`HJ8E8P#WEL#`3s-{>E~nhz=D7 zjL@pN4EUL^sXOibKRX-Ho)u_6%8S8BjgR<{4NpH>Q_3WRGNP zk>lEeDa;}=W!)qW87iJq7&m%#@yiX=vn%R?tQ%DqP856~ua-5`oKX|q-U#i;0P;TF zS|oPv+{4h&Q0GX0r4-9CqZ(Dr7wF7jD6g9r2TSdI0^q~OppmD-(%uA40-zXWBZEGC z`0(*2apJ#OfZ)!KQoGnHphgnn zwTKs-+rBw^h_5hEpfl^riPXv+yUdD&msY&u)LLO&*N`5s`C~o#_#CG2+&}mbmxypaD+-7I!~?`QjSSe7A%dg>c_y(c=E!HRQXn zoa13Q#G$h*u}Gwj1V#10t94;*GAcXk=1+Nu|3jpunwwhD0;*d56_+q>4NhE7o;)#u zDJ+omMp+^$|1==Zon2>V$$g|ND+seWT`fgovcAHn9J08cfH{^L&x zNBaUP?ukTp{-e0FHfa}s?K^#pxt*=Oy^^|C^a(@{q&@hL=oi>mpyfbjyeBQur>EY& z!5g@Y-6ddN6qz?Kf#nB-W|MRlI^NXP0yQY`1yqx+uboMf0NUerR zmFJX8`@z84Fq*Fyv(oM;J0$XGAt+e^bJNnff(u5MPq}w)dPN|Q#)P-O?UK|Pme3z?GcwSgY9kW! z&hl_g`qjY`np;}L6%}WDKy6$_TtXBXv8!Ni9_FAvLNQlzqfG5$#Dbn>@=8=#nysz1 zwO;E&EdJ-SXqN8o?jQ5>&5#vb>wXl?5;ig2JuDPBRBAVkvVDp9YjeHxMzC6EF$o~?@MSJMc zQ(EbM>~+EfgI}p<8#~+VS{{cGKYqyUITPDz+LvcYQKQm{hP|YV@ftLOo`(B6r-8L| zDaQ^?olQ>n}EEiWpru_3|-G?1#H*eN}MeFMkETQ{^T8WS6N1Ip>&PMDPF*k+( z9$DDAO{(v8tby|f_73@(oK+~!P|_Hc1NLKGVQ2Q2L{19X=6x+a+*-uOQ)<^Bsi7Ma ztn#X0!ED+zK>)rqXR6n$6;OKHql)MFq0Mdo*Tl*N=q-GfJY1RAKw7%k{U&(T%`k*W zq-Wx5)FJ~tJ@q;$)T#nF`aL%o0yTh#9WTy&W;!wHhQwsDgks)NPV=Z?+~cT1z_FdK zu;)0D9NvGU3+#^Lu(~lN1u24l*Q~d1Y^*fdgr1Q<@aDn+DvfS7f1#S1H9s94cK_Np zw*=2)$uzP*?c`w?_eoTgnXBuYJC~JtJ1&+#$Kde?C{ic#EK6t(mDus9mL;P;6%6L= zWP(kemPqZ|L`SL%4BxOY&~_YAS=;Y{e|-&`m`6BKc0=$)Y%5=C2Y+JQU3+`RbzdIf zr?|vKh4GAm!sEr^qoTo8Q?p%J{~FBy5Qu0pwhyP7?4D!AvF=aE;A1{;g84u_d6ggj z6l36cOqJR}*C58GGS!sW5b>KJLcpM&UGVUb5jcIC{o%tijhlg4M93}zfY)Ub5)z~j z@p0_n^n@9mHM$>JCqNO1;;=LL=ljb}`xq&YmLQ_oPo1zNQ!6yxp#fab-!!@Vltfqfw}9Yk*x>MNb8s zV-_GPb8~ZRAe;>i4RyB9#GD~ljc(7M?#(mYB!TCeH?@y8V;@=RvC&n5LqY)nl05i< z1gmLiXoz0A6`Pn(;8izN2oJ~`Nf!ES6nU-Lj!W3@SSOo%XLIqicU@A)c*)N8+_g*Oe^g#Mex$K%@iFd}T-){s37yWE z_(J8I_rZvzb;ZYzA46`%t|XJa+Z>(#Xh285&RJKy`#7V$7K8n%5mxukTQ--guuNWux?Sb<_BjAol6F zD7VdGTw|BO9MojGrX~OR@QuFGU7fqGe(A0Kix68k%PdoGk=RS;;vX?nhovQnNrF*c z-LT>l6A5k(mwAxwq*!9E*IyT)40A|$=%C-{+V&MXUN08tl~2!i$9A$gB4BuEXp2;t zS7&eli2#cDs1YgX`y$iuvlouF=arh29e1a;Rm?518{__HF)}D4=0;C zT3lNCabaQM9a+g+cd6s1XJ*bO{o)VBb7EHfLikWbYTQRZ+{b(33|2=~-W=eQrdgvw zo1fu~@HfU*oZ^XM@-jS&Og2zcQ#(-q0x?2AMvcg(+6SeY1=&DrGLb*u7V2EvaLK8X z43j-!X^$Q;_q^!+9CkLi8U#+(%|pPeS5C611y_g5SRGsFO_lf>*fHfCm*QUMmn10G z>&6Y)Jbjc|(8P;@1>xb*K0w92b9!_9Tr?Ven_y-> zgraP3y-=(CnQ!ooRE3}RZnV`AB zf;K292moC2+bfZ}9ubGPLZ;k0iIRT+U1KT3zWgI9Idb1)xe#(_^UK)yH+Gx;=qW>( z%=i*6ANnI$US1yLE)!KFqMG3X{m|I$kgA<^UPS7?g~YtPyn*tqu#Xcvo8*8?f~=Kq z$*Xm$5bYn=MG(I9B(_dH{culALIPye)oYrXqWjDUCb4{?FD^~jj!*7y3QGgxFoROF zZpr9OLXs{~rA6Up>6)EJH!}k*k}(8%U?1DN z3FqKO@M#D)@1WjG-~3M>J{h6Z)z`Or9AJkcvQZKk`N9BJmFlDQWbUPjM*Uk zTM^|>Ti;GAA56^fQkUhbg)fv!g{A#4>chM+AC!g?9QX}IDT<3b4Hfj2+Sw1_nv#B= zfOO=Nd~);H*qGC$iHr<^9KB-N+}vDxzCu$DdXK{?c#2alW*gAekJ7A>0YVA!Ay-=|<=EW_B9ZqHZICE@+YFI8{)M}>=xLjUxPn4B z;0LNM5TJN^+FA=@iJjAr8nv~x>6058US41JIF7J z>8pz#C>A+QHh0L#$QY;3Y^Xnil;g{>)}kANtYE^b&%-u)Yz;=lgQw}k=!2=x#eZ^A z2#sQA&L)O^{i?@s_;Am}57ML;ZGH4!WI007xu;DBe+Q}HNBv=t&k8yw8ALF@SyY7#IMu;E_hxRL4!9McobhB&rFZw})%FNnrK}z1VpEo`i4obw1 zaFX{^kjfC7n^OjA+a6o?zr*MRm~xohX#7V)H?ju4e{Y!F*p@wGevD>#=mO`#HV}lC zj~^KT;eb7tQ&OsC+01AjR_T8$q%t$Le4Ffwr0A*MXE7BKkD3y#EHA&f_-onx>G_@d z%Utek+7yH;oljD}o7awTL*k06;oOGF>haJ59Nx#;kkgR>BJf8{(xE|Fam&%HeUCG_ZM(zj$QkSD>FLi%9VMX_u_7bALo!Kz zTN&CTN9W$3B;RDhg@Q-@)mdR*J?Ir*6SlK;DJwIxeED~X^M1Y2%<5>1j9YKs92ZH$ zGB7a-Jvb#pVfAaVSpWP~(=A%N8xVtm>CbNk_AV0)UL!Osjy!Ste#4{j*|od#3&xL7 zYYG5?A>?-4&p*y~d|Krd6%}rK@H?5r zW0@s8qBv(2Rn?W(uFP1;g$%P@IlknV0}%(zmP|%EFm?L~dPpY~OY6#b_C2;OnMq`m z1LqjP>=9b~dooL@ezjP3XFyB9BcinQ;K;>DfOw#t{rz0fmVEQvA<8ySI|~kS+k#ek zdL9@#u%a0yCo&y-r|{`k%N2;w=&c)k`TSVB=|wS%i(IVT@_$w8 zX8JM@xVZy_toBs{I=rw8`Mc8rytom{#VW`JK6D+RE=nIsG24Y4@}rJbe>U^G;Gp1$71j-gm@BFLveTKfkU-3*U0f0!b=dCLGDr=lwWX!IAHs7Zr8>U zD2}ip>WEJ%GgW?*2ISa1GLrn{$&*`4oeZJ3A5RMmy4e-P_16WiFan!JUQ?dSxPb_n zOG=7DPzFHc%y>5U7F)OcI&K5AkF_2_(=ca2IQ*cF5hiK8(~kYRe|OffurLdwz^OC( z($~6?hLD(@olQCLx~(`p*+{Yx;4`(96>~Q?XpTGiA`{{cvzrEGMvz0qiQq$;k*ZWd zVU&9&mYCeFFONEdR(}OHMo-R|X8@YSy0nZrn2`q-cM4clQepx0rOZ8%g~ku_$E z`8EIZXJwsCRQtF$bh>_D>C)Wmd;R*g)XgS4`i0#xzNB%8W-hz9FhF(y6gAqxLe6+! z(s+P1AM4koK;-4daIkEn|5*#j9aUFXRfWEjOJEX=;{RH0<9gD}@ajto*ak>W7T&ZK zDma=>c4u~Wc08U*sK7pe_bLnP*C@cr5Yq5Nav=zjU8~8lm+&B0=Dj#wszUFU-N>AG z`WIKHO48A*7JZH6{Zw`me97Jsr|9D@FU*3)RQjdPPY8k!YKyLyQtt_rG(-3(2rp)Q z#~QZhx2-N+8i;}T#%RCZlB8i(IGtkj@85yI35^jV$U#6LQp|9W2X?R6kX#Pc*A__h zy3@t9ED(J|dGXYf$Ekn^5Tr$pSHuelmFim|;IA8~Z?W*}uH7QE36rS&H0%Vq{E&x_ zKnoBJT7rxp=kUT= zvdH51v4JV%B6%M7e0Dw6V4nX{ef`?2Mpb@6ItqX0;~ch>8tS4pH`~&5@;A6!&N|RE;Ma!%9w^ zkaXZ2eC{g1m%K9ndjIKz1Ao}?)^!y)o@JK!=10;V~A6)G_+aE0CPuAZH z-2lgkoRh<&^ju}}9Pi85g{k6LmHLeT@9~!9S*` zPL`0cI1f{Uic6d>d(1r_12-m?*7W{8&5;KT(j8Y8jdbr$A^~*sC$9?$sZgON^C&<# zCyVpiy$aN8{6&Z753ZBT(!|=L)b|C<=8?gr91fmuLtxXF@@s2{vcPItE5{ZsM)-3n)^IN=peEN!6w_FDX--;I8O{ct5JCzfx?1nQLKouJB zJF3u!2dj>jA(EnyRRTo&ZOus6#*plNur&p+hJQb7(hX85v;+qmPP2E0CyH@AZd4q5 zSL7w4Sqj)W?(z98UB1i5qvU4QPa*AgC|%>cq2%1V#x~D`PdELTaEFUOHZ$fW51sso zyvItK*B@rez}c+ ze3{6hWiIMPemhQ&PrnyqG@rqF4+cSf^`DfH1=~UiIs@f$7D-Xmd-wK6UOn>rW;s~Y z{R)g5B!O;8y~6I7-@ku%Bl@bBI9VcArWG`hd^~;}O`f4N^D(Yl!LbRku}P7@_I7ip znweN+94mCnWXw^pEw^v!N(-|+bgM0X*!JK&--0W(ECVii=eNC5yrFexJ8c+mQZRRk z+9c6*bLmw<1N|_im3MjL?w0R8Yrk)<5K8edw9rUAJXR+JF<5j=%nML?NNP7L7`u(J zr4Xq|e0{At%5mi*-mnSa(a@Zs7QF5kTi5jCVTd{jQA zbm^W;%)!V_{NFNqpmZn{;UZJwk`z_aEF~%;^}oo3xWl^Uozusp{v^#bU^K+o;X18KPYPKkW27?T6%y?eDPdDI@^O&R8T!~ zLM&|*UEz5)f()LBWnu01L{W<6%>cC=h|BjO%i1(8vq9G#Jjsg=6=VJ!oDbDHH{sT} zv!1awOB~^*p->6cuw@y1TlN@o9?-sfB_M$7jaAYHoO+cPDJe2%rG}+c18T>8A+Q^P ziG4>Jc1xf6sMLS{jWX7yR(x9d6%`8sL>$oCC>%ZfAl)mze71sWQNO}D|882>)Eic_ zY(9!woO5$u*`*OL5Xz7tl$kI$cN;s;(qb!AD({56fs9)g&l{g~z}TA3kQz;gXOQIu zTbrtiSiw4_q3XQrHB{ppKDY9_lT+J+*HRF|aA@~^2aGjP%iOz=f4on2da;J4R3=Nw zy+IO&Eu02ah197OkQv(N8u{;Zb+-2fRq_#kzJ4S$4VetBt*^32lZvrE(1HMr2PFt zrK~Z?#~^D8^B71VmfO^C;vSXNOFz88i1qN}07!Q1`-ytQdYH)?W%>@&qc5mQczaG^W1&5xx0|MnYz zWOL=SS0zqtzg(UM-y#!dKc6py?5O(vBK@j4w3?Rt=SP)B?m|{(NqywTUPkV?^8L)r z=5p&=aOY5urQ~({afUjhz^E_lk20XVP`C)Gw?(G_^LQ1M$xeTa#Pf!E~dV|d7= zt$VDUOisG^M>ZPgHvt~G&J8|fU!p!gr*@qUjb}M=?X-jEL7%UlhzsTOky-8OTYN#& z`4=I9SP7+4hvEuAi0WYsp#d;h@L9><--`;Bo>3e$$*CjWy5kSfea~m@=hxgb=4)R` zVdfUaW|Anqk@mskla}t95PjZfB5^92=Qx4^GeNYM3uID!>R>wn`d4#do<-qo6eLIsB3 zXa#`9f8`~V5?gcaQ|2~^7u5LSh-+WS!3TYiK3A-e;54^a2-dl^8q zxZ~e#AwD#Mnf2u{?eRPk=8hbMeSbl!mH%+yl(A!O=z0tUI6UN}We5U7Lqh@R5zY)X zp7UeV;?trdun)T3ltlJb`cM+SQozjC`DReSzqv$gEic;^mvdqGnTjU$=SKTBrd`t~WT?x57tSrf zEULqC1s)0Z+;B`|-yCz_ZEh;phIe#C)SeljCMhAW$@?k&)6R47cNSC|Efj7l-X*GH z>V{{99v`}oTwB18Gl^Yb6gZGr?(Xj1sAK+BDvCv;V)h9&1P@T`GqHB;ZVI&=gPqdg zjIAe9=n+ZUSH{Dk`8Z^li9`pU>B0f6rdgrb*c%uW(;i7NW4e@atjsBsf?$GIf#kQ+ zxzZR)QWz3bCsWjI4SLd!4!NoaUiK+t3(d`q&BIya%4mAh)RZ~Z3vZP6zgmC~BJdUV zO!N1@8N?;i;=6>!1{1AklT_zPny;uKU!uY@C?HN$7_2ZT^#*UYA2ws;y!qM&7b2VM zI`FUj&Thl~_ep&ILK=jDi_(KtQItv^hgR3uD{G156(JK+#vVgjU8-ao0^*RtMRTkL zN1$}<$brONLmJNrmN6Ib0?VXOg87sZLgoLfzbRXWd1v*JCL=k%O)eoZv&0kj9nzZW z!RsVI7f`svfV)jW$cAC&4{Nn(n+=4IbISU{XZ-R}?0`ts{ zoA*_(lKEF+@ZeN*g?YO@8=(ssq_oq-SuI^&Utjk+bDp=~+Bkl#xxVvOY^+HH;Gr{t zp$0TdCI~~sY?SLtp8s7**N=A$Afw#(%<20E6k$JNXwO!7}&37BWLmCp{C`eBCoUm-abnQDWo&5ewC%mlSIbt1dlBpWG= z$tRNMjjtpm_H2MoAi^1P4Tf8C{44FddgQ~gmSqNH^9ZgDa|#YGlaHwLC^@F>u?W3D zTO<$k55&ZK2o)@Ei{e=2D9QutnDM1oF@IEX@)h(>x=^HPcNP*E;w&^SUlbBCi${Rb zg|d~^jScVg@t!%VMI|JM;?B1kUn$jaXJ#dDT7(W+zEtftar0>>6v6qws;U~wo88JH z)IV|<-Dz~VOtvXAC?(8<%WkZSMOQ()AtwQ%P9^gePD`3EnG=p(5aAZo?FUFXga2S= zo`cBY|JoRf%Nrx%;k`gtHcYMjF272>xD3!mZRO)pDCx4m@7?Q+r6eh`D|uGC4Yh~A z{oeY?Dw9mfYt}@XqxLZXdwx*FkuP7+;40-)O?5tvpj!E3F-Ee@gmf=eoR7P^tgY!6 zfzhz#l|?R$o59{gl>GlnC)0X6_K=q0Z(nPoEj_6Wj?Kt1Q!JA7lZ4=z2`6Bi6qq&t zJV(pZx)^XB;M7SE9CMncrzLVPWr#77ULs*M0v|e?Y6zdK4p7`#s-ZcQ3#Bh7;IKZ` zl-rWcA3xjy77~g%jvu=kxXVl6#HjTS3|v~Q;j-lLBRl&+X7K>6P(g1i<#Exj0x(tz#VPYRVoN0 z8dM}s#0xzhF}qOGp25#s!Gy@4S}({quUQjE=;oCGS+niiw~vd5M<<^!vA}j5na6k> z#SQ1=@O6v_UH=`BNO4z<5(QRJhjcUEOQ?VjXjarbRkMtd3BFYU^K`Ur8~3z*2-&KW zDd_g`^BZ>LYBkR+`CVUL+?NRDPG~7mX6D#Rd(SFnQwrI8;X8YNKAOnsHrdll(s#3t z)i0c?uC7KWeSro*9eGL$>#0+x2$lCQ$)u1g{i$ap*qNFp&v~k!bcOYc2e{i}N1JA$Pl`=mV^c%6K3n_&ZzYhcH#55;+L_1xj96WtOyz>~*vHzh-A8h&q@C#yr84I`t^*7%r7eZX z7HYOR1EqenY3?qeDdB~+-ywn(&QE+%90x5D+=%QD`yjGOOGWUFht(XW7wB4=uR44n zQLzg6O9S(-u`Jqvp)KDbc$ay~=ldCOv#hESw9xXfSl|OR4q;!WuI~P;1MBsW36a5F z2)3rregI(Pb8E07E@sq^tcPy137Wy?pjb~Mw~KSVCc}~lj60k%lr|wa7Pa8O=w(Dh zwD^yxffn)67?DEBn`b}-ta5Kb_v7z(3V$A)P3v?v`6;AO;$Z;yVFX_yDBvuLCuGMR$spp z?wq-9xcJ-20xZXK=n_KniG+?7W2EuT&icR`VVgZJL`jj>j-hTJe$r(SzqA4@9yyez zQ8gyrD`H1XPo0v3t)swgV-C*G%#6-PZ?&4(8Dh{nFDO!kt`*&LmzM&pphsE;439|E z#aUlLkNAScy>;XuM1A!@SyqA;f|81)lyiS&b0S0HHwrG!)jauFlZ z;z4?UcAw^1%jcptaPbMGM!o&}Zr zB`jk{1SyD-9gv`n0bdB|17)U-;*2I%?Lj&_qHFa~YxsMr(;@Stw{KI4Mx8`}!lTY^ z0F%G;^XpSh7)38ds2CE>%E>`1@aQNNU*0{TiL)iYzm8|Ju@z^7Zr>848dInWOdXaP=jWS zt6=}tJ(zJH1FdPwxaj9k+TA!sO-LNfWKGm3qIN{j{A)bMBj`O_ajz4UmKC0s@VN2* z2v(988KHU}W1q)E-RO2;*u5zI?BB@Td2=W&_WG^<*Y(DRfuCnprhh1BeDdx^)^R*7 z`_n#MYlO)PVsNDZ2xLc5I$B={9?&Jrc-CQ29ohayIe5CCcCddPxevOlQgvOS^fj#E zlKIz>snt8h9Z+BnK1VTYttP!jAnGKIU=De%!2v3AQuGwkPapOP@*47C)S^O`f@bXo z7>1kn38k6oso8|#ElOOzcy36lt@!>$w9h{tw8-$EUcfT?>w}dbYug+tJR#3Sa`Jd+ zUwB#EW84e*Lxt1^-q3P(p3TlI@ud@l^rP8Rs4s+3p?5wk9_l!w|tIjKb}QqL3e%Qn@6WGRjLEzX!>%}gSN^Pi>yrJc9b*Vr8D zY2dJ4dZGSCeAm>tby!Dw$Ha5@5e=xxXz|=suo2sbh`HC!De~=JSNlcbX0;Dd=6)0h z+!pZ|lxRS9Y5_ToI+IV(0rSSQJ+S+=#0=-&_7X1*4UcP4r>fAEOA2p!jXa3FPw=op!o>Gek*uv zf~4G8F-oQVB|uv5%%*WQ=6KRe{`sM>bGHh*uiHUwk}XW2A;+QC=1^{zeMrc`HwUwZ zJEWO`&#fXqQ#kXld{4J3RvhO2oxh*HEQTDUwRQ;Le?H|$;HCVuN9(ZqUN|}AI6Iwd zkHX;}s3-t{hw_@|&{<{nGY)!>rQ(i^`SQM6h7LY)k4Q82iQ{@_Qf-JQ+KAu32gN|A z8jYY2AOdrM8-Hn{0Bb{a$OU+?E1c1Jfk}wC05t4jT2&zE&i8{^I*XR)&$zC!jDR3(Og29mz?24U9nhWQL13FMKygd zx_r>Xux-|kshG{1TGFsHO>APk8Bke$YQBDBnDD*IS+m^f`vN%99LQ`!peuBDZ2!6a za^wQl4jmkB(+XlBjbi%;6*q1H~ef?rQ(@I^tnUtf^kx_PK#Sy1^Q{o z23>4yMuKe3?N~K|;*g6p-3z|Ft7=q)oNx4`aU^W}N1EvonnP)QeSHhcNh{m$JfFVO zjbFe_dh6;Pq#{^6NCdx_=tMU7zDxx~Xb}Dm?#-AmM*;#|ZZH2<10pMs*<(T($%#;9 zgLX~>e%wDc)*Pim)u69)rNoKJNOG#a{FW6YW`9^%kR#>P&oKmHMA;EO8K!EOLPfDA z?B6sTyBm~T0Pj^tjrU@T;896TK;O>(C&$(o$^m@SU>E|Y_T&E?I6wcCI^nJZ=Kj!2 z$7E1u-B$%QO)bzFVbnU$A|#Z2Qqmn-dXFK)AWw-Vh98iCi3GAuMI2NIlL>S&k(S>q zwer~dLyw(}D_83`clHLBnX;354@dlpLsi zhOw#X3LmLGixTChg)GlJM6`el{QJ>D#e=SylN;yET{*QMPWh_q?$5wLVL`b5((VXq zcWbXYtbVNsGjD``-Ug^ElReLVYsj~Qq|Ahtu|o@}+vNE^*5|^{Q_$C|liRh=3cdgO z?c47hz~a#V`4oLWcrD1ldNTM->?gG7=q5(*L*SrMz?NbFY#9ub2c)$&a;o=_6m(^B zYCqekDc8R`pct9cE=ic%s;AVF#Ywk}wBr|>=@;Y}giA_G|5{*ByW6^CN#j~H*Us%% z5H0=)$+W;$$cJp7uz!L1m1YLX(kR?T>Aqo5yZQo&3S&nYgE^=(J9U@RY=OqwH z!=pzxKpvN`e5DKg^`dQ8l7*(us8?Z_yzUh6rjS<|<(j^m@~zT=-gHe#LXZMHgDIcZ z<&KQQbyNj(uU@|nGcy0$5x~5-?7^@aH+Gq_p!Fyn>VrC(QTCF~Wu!r!UVtb5T>}Lv z7j|ZBuWNXx&BErZe@=LK*T!~zwVU~x>9pGcU0sSq35i&Wfz&&%%^YrVC3;egx8jqI zI*Wb7U;Z?0C&$YjYm%VPU+cX5USIKBTTS`PtK%&%V)#mk$BLo_evdK;!qpb!6qzMY zpU(P!t=|Z4*lxS0pI@8r?mHCRF!hkQvNXLnGFD%Wj%WD&P{DY_E#b1l0J12y7=1`| zNNT1fwc(JHTToEuphMajZdBWHz|0qWMrmcqwXh~1yyR+&p5~|npadlOJH!O7hUij~O z109ur922|-xtP^g??5y6Id+9Ia&j$kj}X3;S3gvAe*plt`Tawcplao~G&G*9*~vBC z)c!VzGxn3MQKl!1)(g>R^ohR*h0Qw1$fQ&5eWo!pAPsEQD{FLyH|$NWfyh+8lV`EgPB8TNPk zVXjhq-}ht0H`-9JAEPv@Bs_Bs8!S2`Gc!6fGowgJYTE3%*RWcC?d5M3nV5%fDy7#T zM~M!hKV+oQK)`*bJ(yczs4}La`%zzCKg_mgSOn`uvE4CxE zs0vyKaf}C$hZvOzOx`4VuZUsh2u$X^lHt&Jk^S9DSrvarXSRFMyq3i4i7jog5J-PNqhgknlbtJ9*MZ zt%sG596$a$8oGgVPxkt}oRw~{XLxlK3e`LVPxZvjvLGbYI08bt4S7{dtT#GnSDg?zYcMaf?!`+^vD*}YP_*6>gHMgrNQlarB(#Efw2 zZhX-@IF)ACqJYpp^-&`7wbR22ba>p%%E(C*GOjILWYAO-q+Dgefg@uK=M_T{r zri%R9->GGh0kfj+WZZMQ?jL1hCm_RoQe_gx9d0JU^xuG=q^8JQMcR#NTqm zg)KopQVd>IYFm?e;NU3KL~j3n!vv;!$K~nQAnVMM!t$4!Zca{EnD6;Za4;X0iVLE= zfAzuu^g_*_XzlN7zCUtLC$q#Kc0sAgDJV^TvhA(dZwpVHw_$OSyW!UJB3Yx%2RS+O z_(Te9&<7)%QzUhS-&L4Z(RBg3thek^bwdZ2 z_w9bphbfiPzK|HI!3`C_A`A?^vZt66Pap*U!8$6O)*uWznS+5E{8fFsBY?AJe1`4I zc3$^Om?BOP>*HOACs1}39T-JoDCU+W^{2Cpo!Vsp_+=%&4^*{$~<8?;+G&ybZrGu$dDqL$e0>lKGV6H~SUqqj7_ zxhNLycDkj*7LX#Dp%2xj_47GK{FMZ(uIC|+5953|{HV~ivWex3y3eZi^w{jP5Ib^@WlAIk&8;m%0yX*Mck!8Gu%FCjRo+SV zP4zEDc5W8YTTwTCu=e^O+L-3>?P;3G<)POY7z%reQ|>hL!O7O~fl{$#I|Lks3?Cog zF#L@o(Lx(~APQk9C{h0OsJs3kUX3H!tY^d!JO%}#ttBpp=uU#iFiOpUpM~l4G=K?m z2T3??|1oyfCDNyk{=SJ8H&*H7mMMe{G4l=EZYsVvelYKnn8WhF-l$jUs1$^kc+}Bv z!q7-CRU|njv2gL3#~_esmF(eE8_@<^$7cg`=pGHZ-BZ_5Uwn|%pXRRZ%__jei;YK_ zbfPZ{LW6mF5sCc*&J%444L{jaVd-q!H|MvPU38tVwt#-o$+A@Ct*rM$a?LSf_zsbW z)7qzdU|Fs4vv0;}we@~`D8k=YC?8=7D_|-vs<#aq7?U2=p6sb@ zZEdw7sn8rkZ%J9WYVjJL`o%YcCEel4LdcY=h*&3h!YVnbLuY!b`$`KE?9fYa3f4*ZY!(AUmk%&I5Omfu>wT(CZdg-U5U&$L5%QjNjEZAh{0R6^-#co%d%pNG9i)6nqX9!iEU)G=>7^RgVZ z05yi&_Bg*g-W9DJv?^guSo`01kEAP1p@M%?3GSpYeFH;Be*wWc@afvhJ<hw>5| zwoyS*aXo~1;>wz1_`0JK6C-22SVv{_taLs!jdV9RlUWnA>e*BAclS3`fLb*SfK+`- zwGoWF%5xLS+V?VJjoN&4-0Cavl7B4y|lS*uM1$JkuLg9uUqMD2ejO!}k(5j~V5)feMC8FN%n9~KZ_MxmL2<=a zaPU$ncVEL+pA0(HW2@T9_6qc5H>j{1uZT^}ul(WST!_;4mR3K6j-Nj3Ll(0@otj}C zL-Q57MHURlmiws+=<(~|u6eQvo|{rwQ#_2A7R(=~M}(^2W`$SZTy|wsZ7O1ADCMbY z#BZ0r_^hd}8t&j&l>H7@tH4(S5qh#{$6HQq)#JV5g;Tf1vIdu}P&+`|@E-xz>&yWpkLAMx8jUL@* z`T{3*`BKW4-OV;llcsBtuHPyrP{iw*Ei3{Tm()@IqW<68(qEBKqZaM?8DU8Qp!oqJ zKc|G_wkd9TwL|4K)?x=Lbi`(eB0E!1jWBpjUELbb#C~&1eIZaYtz_o6&tvz8BG}cP zg>c({cssxk1ZnP0skWTrXZTV^Y--&gS6Xc+TPg^493d=kpRyY&YR;)iH6Tv;`RI2- z4@Dy$#7PP(d-iRbz%+xJ@FkI7VI7b6Bi-$(AA2VZ5^qIZb`!QF=eVg4dr zDp!C{ zqTe=pY9e)5YNir)@_Ew5M;GeG_V0b?S~+CT;Er+613Uzb<8>#e55DY3*Wvx!-FHIG zdsvBr+LfqRQ;HmlYIwSX%nJgTt-zZ~`+N z$xjpamlm??=pb!N_teOr9$BDj4p8<)jQ-zH! z=noot(h8h25Lqf=MpkKK?jS68)Vkc2TCHTCjH=KV$VoDw(C{phXobc2dPonELJ z@ClqYxl_O6xu*L0Arfh^LbOgpRBfCprKM%hsz($~ATA{d!QFv8P69Z8PA>cV&wHrBBZ2hlgJvLF$$yU^VN{&A z^D}J-8#I2CSxN z&OGY9Y)S$j{QtI~sf4RM!LZ#r=5ZR7oYy*()!*?<+Id-7*?iIf(UJorxm#Rh^zQBJ z2D>2^3b!5j;cDzBN+c@MSl;TJm`X(MHMoy)vQpYsu;E_l|1)iHDGy7QqHCsHD~b~y zb5ZhA$f%S5w{ZdidS~Qz%NiALn~)v>DG{odSpw@W6we9JhM=b09zi@>A;GZ$3`~T) zm~XbIZl7h?zopIx_w;Kj0g=MwMM9t$Zeet@z7D=f`A;fydBMfBEFG~B|x z;H*Il-@`Cp8T*fSAjq(^vbs71KgCHv^njE5W%f1kb{bp+J-E9dE8O}Lo7w<9xUNv5 z^ZjzdXyOkIm5|NnFjg8pJyHT7#H_Nad$pacmB5{X?E#PtkBUHX&t?2)OVyt$6a`;d zoawM;aQ`<^?~K)?iott;RbyiX001D2LLR;V9#JY$-tkQ*YpKbj7KhXD1YMX?0pHPj zf3B6Q^+d~E-JZ|={TG8ku{g_H@CrwcELSQRSHmx$+4tCfy)XPn5iJ4KSvbprK3+xj5kv|pK!anlJ0jQ)E*;iQk!Mv2QG5x! z5Z%Ri6c4%W>7QXo|J(=0wgYqXPq^UG6(E`uu-Aa0`l;b50yoe|pP?nyj41f?v9uA9 z^~wutvsc#7!7W@Ua%w-}mJ9Vy-QDEK;xHh-csLNAVZ~{wK437BSY93`ght0GAyPxq zZIAimdA%)}HK@j1O4Tp)^h*_a_H@hT+BJPx!r%AngHfq4s`&z*W{JfP}PM z7Db{2U-nf>MjK_Z&*()-cZ8Km#NDguWUZ-`o|(oV6hNq0+a++LTdzjDPefl0ez+>p z-eQT+B+82&YduY&aoerB1M}IAL9!?12T+roB~B{s(@VQIss|3mX=9sBdEqV;sI@zr znj>3tV7s(;<~cPD^9bk$jt_`o?ka=di+*}@-Bq2(M@9}}EZ~ZkGwlCw;R7P| zpJCzh>LMpyvFFxd1wmR(2-18fPKbT*#-U~tQht7Zb$sMt1ca#_J(l2|V8sB6$Z?&ah`z+A=O?i;Asc>NHtvc+s)#WV7mj6LA>xdA zgCG@Xx!_)pL=8b7{GQAVf^A|f6Az* zU{+wq5AK^SDRM1swJYob;3NsJtIL@B9qw2bDt~X^HcUKBRM)x>nu8$Ita<>J$SLi< zG`V5Vut#uma_$X)-Ww^u=S_g{YQNrz3z`Vw5_}2jpv%4A+BrDr{dMv#b=qb#5ZoX* zQ}8OtO+9J?Vb08)g2|?^J0LVwq;5t@0D_jEezk^d^R)=_=dHwa)APR4fw@X%nzM&w zG*Po01^!InqQeKhCCq@J(|h08Fx7xXJDZFN4(##V7rRn%H>-CL#HMz4Yj*EVRR*mP z`Du1a=V*^5vA^!DKLTE#SZU5$m!hhqv;pow)+1Y(0|HpkSsUjnOB7gj`6h9FraY)H z{O^O~#SyLN%E$ryTwERvT(WWQ4Ewo77MZn zR6_u}Z~!rqsW~FPU|#_IyO3FOrY_Wq8dMC<0~rR$6_i!AF0_;}qXpOVJsy~=Ns8Fb z`*-ltIS3(KRgaZxP3{>%_I~L4%iK`FPR~TJu-$iHyZ@RRxM;)u&RdEUj8pZ16&WKZ z1jY!WUI3Om=V+^yfYV!FCM`1~v!5!unqcNP(j5YC&mNSwfcr5X^?LsOfDbDnkL082 zvj`6$3581B3IAC{ivi5(L4}{q5{qYIz(jHuJJo#fO}fagC+J+3Fj~iqka7XIa6S!^ z5uRQlBp(sBp=HDv4DMv$-%l`iq69hqFX0*Q;; zG0Fe-61A2f2nuf(j{mfeDH0?KDq6x_LL%7IPoSRB03LD4K$-iAV~-dT$4Jkw#ge&$ zegb@g-!IyV!K+CE?;n)WJ)4~Xg5Aj?8qc5SR{gZH2vQ%UT(zzd^NfjjtEpIQ z^?<_;zs7&StsAtsE%Ex759hIOW?6V z4?r!IjDjF0kXNxm6vgf*??VL00&|#o>~Fqdq5eV>uZ=3j3mh3jahvS<(Gx?T-4(N@ zxb36ism2x=b+e4fVkbEJ^MC}&Ea`vaJ_vT(dBPoLpHbgK2tRlv-z1@^uZn1s6*eJd zyR@q-W851pqI1IQIsPpMSxk|ioP)bt<&2#u_(WPy7(|B30@g|XyD>a-R@h9VOLEB%PC|6 zUuJwzD7V9RjAGs1$Y`NkwGo6a1Sx-A(;y4DtiNt=aey*a-zw%Y4^za$0clBpxTH2@-a9wAtkj6ZgS6?djJ>{z zAaHxu3a1#h5=Vu=NRFA!~Rx6C*P>q+KXU&%zQsnPzRu2mRHtQh!5-R zt;eVg!{JphU&jdeVN+ccTLX6158Y%3jy7{qpzLVE%_OQ3dE^A}LdI~7*oZHafCs9G z|H8{n*NZuv11uu&bB*PcWEWQ6_0H2sQhU-Rw>P)^=P&`XT`T{Y=Y)$&y0`XTqQ+Tw z6l_BuJ7>^oO`GJ%;Wyq>UF>>GbqWp#UeP;P9Ky0K^#!um)k~8hnuj_n51007B3q6Z z)rfF{pZA#%;nQ0BEM->nlJv{#*DYl=vmo)4St;$0?_@n%6I7rr$c5WTkbk0V>)CAC zQA=1N?SUx=9l~Oe#z1vH&J(HzW*xqK z_w7chC`pgGUTjc;4A9oHBa8Rt#iriU9;>>P!x8aPnuo1$^5LkQk_za=qGe{uc;9z# z>nTLY?%f2$Pq>_b<0Y)fBD+e2X@qvFckK@+R%PR}H4M0>ewinNka1!`F3Q?}bnd7e z{P4Y(aktjsR{cNC<0D5JU$AOW9w^FIy=2zw8*4KJ!>2z1ye9Jl(9h&E-ZUbI3!yEtj>D=;j8o08l{Fjf4<3!-R3LSzh;^eQzmp-9Fwxsrb zl7>H5zq~|&gcTwobUqZy)fTD#cfS~CDJEjS&!`xDMDYzKa(rTe0{J6he@d+HN9Xg& zWTt=K0ZljueB?@t)p?Y5<=`vtOB0tboO=1K?qrV`IIxon=81k3h30%3|FPr0HszH5eJJ>X{Y5_3gL5@09P+BuakV`{@Pc{ z9ESLrWiI{wE(jzE?k-VhE(kAsd~|;K?I1QaYAwYcn;KhEDsfs3Lpo;vaTaXHyAf%B z3B*8@^Zz~&*=p+8Sn5$Ywk+p=5^@P~_x6F?QnvqSo(dO^lgCmx`=G+nJp znFKh(g`mBoc0oh&K2p$n4KkC!&c3+hGIrX1>hV^B44&!#qv^Wiss6wJ!>*?U~c zY$-BB(xoVytVlu`+1m}Ngt|o~qq6tT=1QVuXRnL0#}y*P@7zA$-@iTdzVG+zb;k3o z=eeg?Djk@Sz5;?Gi(wVRh}d(P9l>5J5j2m3q>t-Uubd?zeAo-zL4EoNJJvWRE92Nt z=B3E_<*LQpya!OuX@JHLWzYKb%7+`sPY!C7wg>`{{|EV`q~9164s-M<+?OOzb$K$S zL`mQIa42r`8`lOBW99KQ+K(-F!O-M0*K;(~-lFm=3Po*;eJ=6fF0fR|yW|d7=JPX0 z8(8*|BQSh5h_Ei7M_2e3SNz0_Ilq?jl@9PI>V#pr4f?gBCW34rRAI&=D#ZFrd$q}i zR0i($oFi!lim4-8GRe({pMvXYp>vH7B23-Bf*x@&lCg;3&(16RIp`>N#6{Hhsh_SV zzbX;SJixi&uKV8KA3JLX zbff4qmQe~;7~D;}n`|d8Rqu2b4CdytR4>mlhdUUjZd(AV2vRpsNRva>wj`=4TCW^; z{&CH?G2^^+Y*9R!P(XeH86JvJPk8L_fp$2S7z^J5Q6p0QjcqkwT>Q~vA&OxY zmAH}rHt;i7k2!m!kr#Q9E)Ly__Wm*07HnJ3VcaZIoW5aP;7xdX@_{R%>JDchVQZ%_ zchW$JBRY5BD}q_Y-^b1OT@ z=oypoONQ-_XwM7wZ;^8&==mTRB${fnVXwJqPM>}((9>}wc|&!#Cyud}aq69BE~6Kg zi_cIc8t<6L#tFKuDObe{kn01#ZN^bp?sP25vOzKb~Y~OdnPFz96j;;!uuXJ&Ub??TxD+; zV~jpKiBf4BcU2(aWAbhyh%Z^(B;HT3u5(iomwj@1_~Rymbo*Z^2~oxAT6pkJqpOxp zkd}PWjFYunc}tNzZv`21Qe0p#PJx88Y^skHzngfH-UH1DvU|T?Hja7o&sLkcSZRkT ze2eL3yhdRi=YMjVaa|#wrvaOc#z+oKSDfc`H`5>RkS5&+O{{)_Lyzos<{ zxa@hR?;6Yl>v-{-p83f(m6zv0SX!a+S&b-wrE60^YWhuk@ISa-ag1tUT^lGLPeUhVG7@9zB5 za(rh#=T|e5DF%qhp%oVfTul0F@fE2b5J}h%&H+zd3arWoI3Qzh9M+_$zrS*wX@cUS zwBIR87`4c{gCI!QUlvhhz8Y#>xBcf}yD{~OT?|~g&e7lKrIs7Ac2`G5+|36n)VWa; zdKj5O;UG_Vdkc(J|E4_q(D@20B}5qAm>DG^^cvy;(}%ARBz=EakUVvKUC>~~#^E#LV3MEM7 zzaSk;%)>9D^DI%-J=JFYku00Df$vdIh%&hWqeVXX6)q6SSDiphc=LZW^vqpwlPtz2 zv^Y=$%ar&XL>R%$#X-jL-kE2QdVbDdOU%?^RXxk%-;hfuI^VHv%-$CEv?FJ+s({~R z<^HiuglszQEQ!>?Qf_7ZhOy{7?9-0iOb}mpEGey`@FX&iT@l-Jx}mveSS;r!_#%?M zWXrAL^E=Ng{Khnxu3%&OX*EZwwEZ}9>Py{`S;dGb#=L;z?5N?B>)YIjl#i0!2xU2- zVr`gDyf1jKWa50>FXb#~`MlmTIesmTyl}Blf*wNzXIzKgF2HGQIUIQ-Zq2gPX%`uV4@d!gsJI zY;-!;?cLT>t~?96);8LF3-#xFUX zgRfe!wE(s>vBc#?mP&Ikak1Ec(>1PV2paO{=u&nraNv`4{Mt*Duqp(!&Q znhiVC$EGiwKUkVODSNb7=t2g>Q(;**;^`3rI=Gw9Pd$Bf;=P3#q7`@V->+&?Ld>E}Fe1D^5+-Sw;BYYtmrwGpOjccXL z0+mk}S96W9-duZ}xZ~oi2Sy4ClEJ0viybZq_)0Nk7+{%ZD0_1?5FLX4*iYZ>7-=hg zf*h|xIJ$KJoiy#h_=x_IPT`}OVuCgDB6x~b%Bj@@ z%uPcsE7Odu(}=KP({Q>q^FW1Py`2)cYVO7Vne+3je;ugQ-Atcf%`Dl$=_v41Bh(zA zT$O(eF{ooGz=n-EsclqW@CbBcB=lzSx=(J6-2Dl@XC#QxP2Fk~9e9g#JjK(F^k90pA{r zocJi0l_>bb!lhCC5#uMGwv91wiYkxl+%2SDpUU=_VQ#?b_+TatQaLpR`}_<%PZFM| zEvrAkH||<#|60_H4kY^~x_%kv2uBwcA36$LJ9Cy3)$znRq^Zeb&)b-~Gn;IH@!E(C z$udUGw$$dJjvP@$5V*~RLFkVmn&sa>v)7Vl^hIIUmO%|1T8A}cQDoB@;|Ny&brfWA zZ~U4P+Ac~7(j$+(hKL5=XyJGSSLNw98|YwuDwaby0Man*gTg5t&~0lvxjRtF!6r05 za~OX6C$6&=7D-sZ?5ci2=yB)Sopj2YY?ee#QfgSg8BF#51+txU?#xC?a~K{U{^@E+ z5^z*byTJC{ZU#PBZhc(3{oZ$P#x7-P=z>ALWfG8T?~QuTSQ+mObA83s5BpxFTY=DI z{+{`b^$sw)+(rf##+x1yGF?p5ux@8`-8@12Q^ypj#$_iRL!X;xG0vng*62{b?~44m zQTFcmG&!AyfsW#D=tn7DQEc-p8GvMbL+F{OnG#wAg?f!%A28w@ua3PCRyOJV1sB=p z4eh>*5&b6-JC9%ra`q193Vb_Y-pQxX4@xcRwRu4z-YedZv&uB`7`dpjwPis0fX@%i zK(~YWXV7TQoSF*{`nHi^CH!>Ofk#FU5Bmxc^dEivuTtpsZ5E;V&mC~c9F866;vzh{ zJ!&rYZz7T!&h;O5c)#V$%axV?Zf%~hF}-I>cbVzYz%7jCprt26^yj{9D@-&P3En$e zDf#>e+oz{U#j^8kA1*mpF?#SEG+6|G^F|o~4jj-Gy9>_zG+plFt1^S?VGIUs&s|eo zD+3|d#qLjB)|v-ILEZ{qk&)i`%d z@5;)Sm8lV!>wq@am8aCtka%TWDNXLghXyFDg>!R)i*;B+42b=QO+!(9a$y5D^*8fN*EE=KD;J z2ss96Lc6gsr+3lCCTG8d>)B#pvwgL+?s0fJEcp3kHot?$eHL3GJ)J3MF^NLsqFp=BZ6s;9Ck=^i$CAFx{?|<~Y~i22C?IwmUk(vIs#PnS`?6`B6jfYw z7VYwUjQ0qrNG}5ni*nl&mTUiYlW!B6v2}W`nDUw@c+kk{UBSGx_Ut%w9jxj@dHZc_ z-zSCJ2n8CG&j>=QC;Ftwk%o0AzRf}7agmD*?oW9O#;4t^jyH&sT#hZ0|G74GHOXca zI8Zn+{HW#(cPxhPU$q?Pq9=Wx7g!DHnQ>{P%AFoQhX>v;TA4dAbFXh$qPiq_+*K`tDTy7u~$nxwEMEyRCH#Lk?NXAg-Icfi2 zdZy+H`Y#;5yt{P8{S|M)g}Mv1x31a z>2@>uPcB*MGNfr>B#{pWPg4x~k0?ADQ9t%oTy2mPa=b?&_MbmusUY@okZLMf3sM*y zAH>M)jQ7%fUZWKQ^t$YS<$1Cy#A^s}h~-zb+dW1R5}$1bcwm>kWbz~_kPS<1%_QtW zF76D@%l@)!_CvhBWBYTDZVWC+y|V{X4}<{r*P$^8*kFH|f-g*d!9y}eg1hWSW#HTX&H?*71p5-db79bGAmcldI5I`Q_| zH&94!yx?rv>k3YUm_4s?fh%Zb_Rsy34^c6!!&zUW9W9k||4*AKTGInZPRCv+2vc8#%sa4eZ z77|>vew3aX@71s*BtRhN)+otdWaTEo0~RQDf1c7W*coU!ka1ao9;fNlz0jbSX=l++ zT_l3}W(YZUM$~Ecv)PT;$O9c<4{1il2bY4MN@hsFT-%cWAOtqb*s6{!s8Wb@zy|*g z_z?im5a+LH70`!p7xiv%o~xY0>cXxwGB8pK9@0;m8vawt9fAA8r*72t3ak z)`Zecyn}V!h5Ip;TD_~#{VHcgom0J*v-p-GNM3y4-_sv}k9o`G?}csR+K|C)6nYY( zVjI^6S9TS|*c|77;yyqhnXiL+XPzR$rMiP9G7E1+xgTireFp*XQs=*zz1 zQ`=$Iy4$v437sIe=@-%Un?v zMHswW%}un%hfp^tiRce9BD&q6R1xN=@GWf4N0-N>|EFP~*0pcxPBpg*GV%(kAX6;! zY;yy5pwas`w(R@x4QTN=tRqE!2WkK0 zIbNcehYCRv6P`M9&eWJ?h;H0_+Jyb7KCkW1<8X7LYr8VGf8 zj`76~a$3nVJ$KFyGkt2m6cC+0SIcPN*_w(&1pTf`2Rw&(8*4@b-396+!VoTgT7aIQ z!>shRnog`B4-bz)VrHQ|K1K#7>ww-Y+b#4PWjato$1&Y2sGu9LAU*lE+zSW#rsyI4 zhw?S4?eC9~H;2Zx&4A)>Lim9I9N~!J0lV%6H0f0p2^}|MPK=y%Zq?SXAt+vo3D#!A zPBm(a$<#8ISi|sE2&N3o!pD}9`IYYHqVGUnO1S+M?+g0j;>*weYeFYScz>edLHON- zp%oF#8_PK&Lz=yd!yB|4;|=Jb-HvxK|Il7qURd@P=fFL=P-P{yaA#21`Hq&?2Hq_r zy@DxtUUTAL9F83t9MlKTU7h-6wrK!4it6(|&QE$G*Yf>9CiNc0pFr$5L@6)6&!@Ae zk5bzzu@Yd%isc-8EdFC*G~(T~6(Kw3Qb0ejlHZu1Qhh{vY({SQSmBa1UeVi#e>R}q zVz0O&p|aW~!WUFa5mO5ly%NE;nFYGA+k^VH!Th3TIso7N;>8=0k`4->>OTPtyj?{sr{E`-p)C;RjWDPceqt zPO>EIHIjX=Kk+%JlWW6@q}b*O>z{4LtaF{qmGxF%K}$a&jh(W)qi?}Wpk%<{PA*MN zSehfc{z`9TVfgp&;W!SG$l>q%F;XJ*zU1lgA;Lbim0hdBpZ@V{nbjg?j!x@ZxrE|C z>-ypG{EXp~2Aa5#0AQJ&9IAAi6fJE29#fGZaspmX;Wh@Eat@)c=^|DSIR_&xf^<7n zQAW??rkH$uG}}@`7INJX0ePf(PEyX1bM4{+dDjW)F4g9>fWg|Y`k}jh(0-WDZO_$e zeFQp5NIhP(YrLO}RSj_m$1!YJ{Y?f8Q{I97`bA@_J2P0f{p_nX{=a$cLyXz5E@{Je za>rdaURlTQ@5rks zMlPksZ_eQsRT~JQ&o>S2^dD!@+3{^cXf^8Q-+sO3^D6^^!a$7|bLZ^e%_LlcZs|~{ zW$av7GyZ%0R{h1^ySXWpWFg<*f2@KuZgA5BUc~;i6{i@H{^FLZr9w`Be}C8Ak@AJq zha&EGp;_;S>)0ub+sn%JC~?K>qF2Vs9Q74dR95~vJdcrs?d5BV&7Lqb(RC72{%huDtmIRYq9Y?AyP&JN zwYHp-fnG0M<;uO#n*c*4`!FW@YxaX{?RZMXKq0sNj&whaKkM_)|2pel{#$26<}toO z=jSl*I@d#g%HEDX*UH9T=L&04*{A&%!F5B6$2fCQX09v24FIMG$L2#_K0gla77{!a zJGf6CX2UaG88|+qFz~rLw`Qci#BH(x-14u1s~zPQG8q!vb|N3ld(Xhf!uS+(zOJw$ z*5mzC0Cc`^SD8G}5cKs_fUwARfH7wqBElX}1QIXd!NZ6U@V}SD*Eqg(o~+8mN7uQU zu_q7EX5}wn_6elm-+D>7~+fkSNp3rGb$VWb>tf+N$%jX+OS%;!mrP<_S@VY$=ll zP>8cCs3!B=i66go02H!ml^7&s?3kv8f~WRRsXg$+CqGE{)v0OoVBCz2zN_$BlXhOH z5;{T~a~ZR)9VsvY@L%}CYXX>N3Z_CNT5eJ%@pYYrwJ)|m=@21Pc&}uI<(&AtQ0)zx ziIGnjclDh*bX=R5S*}?MIBXmksT@A-q}%@Y=-|C`!1Yqe^EpXDZ{<;#_SGNL*Sgc% zN<5gHGz@w((?67d`XSF=eT-J1j_){i?2M0)ZU0rt-$HF#Ex(y%*^k`~(D!^(Kj{1S zU})G$F&T5~=;;lsz%`(%FpHyRy8TAJBPDrlH>4r8OFp9eak3Uuu1TH`(nV!rZ7_Q5 zTfb$4mLNM8s|G=KUWw+sdWON5ZE|C5YO^cB+FrbP7DqPBAU{S(G{DVHeKo}eL?2gK zq;~8%*WK+|D!s?&?z_wva5cy1SFBnx<^B$g97$7bbI5z1xy~2?gAJP<7gu3x@wGKd z*#x;OGURl&e13Mz%oc1+aZa+_A+qR&7XkN>COE2BZ!#9ChW-&`+5~ErL#d0clrah_nxUX}GZmz1AyUqEB zDEOdMj4CWD^0X((f(C!Sv}ri%!z}a4g{NcS<+$>S&t5aWpu}@tJzl59JDio!ufSUK z^1r7F3J5(<|M?@=6eo9ej3^Ny+5LPYP~LUyRpsc4(uT4ck5aV2i_9F+ck$$n6v|Sh zr@pC9TtWRE?${W_%iBeDhx5@C%%qS94FZdh%J~6aW#!CjzRc5@3phC@(|X)@&0e{! zj5+$MVM8v>&D$(oLhYxN0%)_XiwtGAox`gWzR+Q_tU~_$R5|Awf~bC-H+*ZpS%e4& ze>uS+N)r{leOT}^=E;*P*Q+!;?8@f{r2hTDN0DN5GOl(Cp0DBlHD*_%s7URAb|-E~ zc=Kv(1V{48jm9T$s_Q>?Sa$e4RUL0*5DpTkQu1L`$u^R#c4fwIgk=?2)eazUw#-eP z?D!e-mR%_V&Hh@-8nY|1tN*F8I9y+)AcD1HyI}dA0Ht~?UEdU$UD)%yQ;WFzpbVqZ4 zA==5d>WksAxhF9(Tg#93d}{wLD0eaa@Q>B^NCC$GHL3w-;s*Y-$Xxfws@HkX`P22I z@o;_TYsXcer2f&!8C+@*L=ngd7}(;v)qM4ign&R_^Ei?(nqLF=_#0&`8uNGxgLhwM z){S=!9Mt5!~GiJ9kmKZ@p z`!-05C>R}@)dG{h=LS*4ns!()zEBcAU%t=S|7G_BiXPNJ4HYB?H(*p?QpPMD z=0W%ZAK}enZfOmWZ0r%#4ZL&;y~J{Sl~V*(lcZVY18Apuy!6Jm^IyMyT@=C0___04 zRPNwXY=jB-CDNOy07YgT0|;O|}-XKUws>iBktUOBM{ zW~Il|ioAaPz$MgJ<2lz^)XJC^rF3w5@m8Upk2WkYRg(1&2&mbDvh2Q4w_e}(`d9bo z3whDDNZ`(FFA2Y`=R@uy%YhYJkh5ISF<6^lSg-Dtp6i{_A~1h^se#%ieNY*%nb%nw z8ye*^_ZX8C-&89OSML7p90SQXsmMDZ`81&3RlXQ@7#3Xzo6u)R-IKITF>vQxcf-^J z-hInUy%JSl-o6W;pEab*dZJ2%pzTeLC#g#m&qdUDRzqRg0Rjk1eiF354eqm zoDxT9@`q1Z>gx154bwg_3**0MwM-!Z9nfrK)--?W^9k%bHckGeEgf@4@#E8}~A zQ0GEEl#=i2@>0hbv@NFhJM+^D`|0GIx0IhpJCeEKYq8O7dbS+{RLb?Dn5-#Uu+mXl zwp-67yEirhG*NqDZmn=gp6K#eCF-5(mC?%UDA}X(p8aP|@K(dmW-@`p^>ZhL}Bg2VO$ zW(%;@?{fQd)t2s4Yb&YV1r!k?^tg5Mh<627k|x<;hSP2a$*jQf1!DUvsCL%ye?azx zNzd0VMoD9`99Yb70opcRdK~JBnJJrbZVy%eC_9?}7RK*gq`Q6d<|kKMb;t6VQ0f$J zrH29^$&2;Qo)}k#_A=W92c{E3$zAp7CVA#4lfk$?s1pgB^MTxa(DXX`=n}j-zdL4; zzk>Px&6#{Tlb6tFOcp`5 zigcUYsZ#vg6t)UJ4;_9rh82Bgd>17Zuk%7jj42eQ_@?^0`{~MKmVNU#w~?nyH1jjT zh{Q!OH*mwxOI|S?WKbNY3lrqy=T{N;VfH^}&E(01{NVF6J;U7?%tTtm`ARxJxvd__v)Hd#S-dYvIJBIO!I&$AOPw?l_6^F4fCgj;(kAQ&PwsxC?U$wd! z26WkiVp;0us7a%O4Hq)q;Z!jWLj(>*4Mi z;RJVegy2%*dE3=xAispy%t{~T5slq~m#(MsDYb=_z0nqrV0NXB{oGkHGP3e}PZM=` z9IUrxz4f-Mt#<~VH_S0EHr68INh|2@?<)jwIyfUQxPADAxVLeS@fh4#s>ATOe@f+s za%+~EeuhB_D!))Jg6Jt6F$YU}JGS5Xja?f2;>sjUZz->jnsN(sYXN0n@;A4`D>%78*S!8jVvy-$5NO-gz#Qw4Ryo}GopT9EY>m{fU#EaE@ zI6Ch7`MuIJe4nmNR_xNW-to|!0k^k=n@JfPs?@Q^ji*N+BonudPzw;To zrbV5wEj-;nS&rhq>O;PCQ$~p7R|Bw01RPo-p=_a1iI}O0s+^|M5@ujvc!o~N=%HD{ z3X)c`Cl0Ao8F5c)Z@t|stXa%`JtS<061I%bT;Q~y>uxrdWRJ!xn)44aDj)ZGO7?Rv zUT=0z9mmo3(iPK-FXdvh;-Am^Q^#o&v<|4}P(H%l6h)RgtpZa_v5_XG>_hZHj>|&G z+?Zr2EzP^8Fn=7>METBiX$lWBNPbbe)2=LG5?%)SHxspNlb2uPUBFNL{sAxk6dub1j|GaWZs&>v)d=Fskol%N zpv7&<Ix z7g!h??0YPOFji+ccw_%`AMz%pNzvd6)xeidvI%-+c^_t{F#dEBK=k4Pamk^!1kxa} z-C{G2H(9%ByziL?Mc^Pj824F-{X^z*^0nAA?dR|geyx+vWk<}cxRDbKV|UIn0qj+B zmj>*Tqy9tM|Jai+S%=-mdgN_D{quDqgz4c&&%;rSD+ivNRovIw=uF#Vh zoMUg}p?{{O$4~SH1RrYhyat~LjZrK6Mum)IzS!9UI=sT0+kboYT z6H)TLb0?oi>C-(IIi6OCJLZ(T)*dBmdFJ&y8%f3woiyTOw}o+$Ibbd8SK~5K2ee5} z7pB}5_tRgMIIb)l5lSOG_9e_$i!**4pV;h+;DybdU2*&uc+PLzgXiJ+1X8rfFh^D~ zB)l7M|1K?n^#rTZC!J&QI%^aSkXKM`396gXFH6$R%9~SbVLT;N2gkTQeNh%RVVmdm zkML^jm@G>tzFDH`z>%ww?c;$S#C`chZ&=9@czTclV38Jr^@g20KWA-=>2<2Q2l&NZ7JdLF@9{p6EqHerXmz4tMIBzIr0jNx&(`e(+F}}a z>CLaC>-R;r+C?3^{~wqL7K?B{1{&Yt9X?BBBbdU$q_QYIlsgaN-%DN%dFJ{lX+_>X zgGPV3zvDj-)|o#R_%+(xFwQfE_udX)x(WBxzfm`&zMBL(a8Gm_EgVq^4*$>4Ik`%? zDI$ueq{ogI;8sh?*Ph177WI;AZ-ajO4qLbG13g$^d1vGX7 zKzr>}A)+(*Qe>9=6rqON{{)94$ggx+kn|DM#Yc8vPnCR`bS?kwmd^LpV1Pi(N12QH z{Q!RVhLKH3MQJ(b^|Wf(~kPtt|emxB&W{Z)x~k( z+4De0XhR%}Uddev7E$ri>)7_j{>16Zo&(w<)%dwTSWv=72u`B$i7R}z;>7!KS})+V zcF%HP1w|r?nRciSp0Fi=Y--Z(E4cT-kNB4~UkbiM)%NeGZm6r{0yNJ=5PW~rmhvjZ z%3bNw>nmTY9qW?|CRG`Dl%i%|qG@1+oa4TCDTSXf@c~fAg=jCI2nJ~A&0n*-m^WIY z$@XP)(+hOwN)*Y7N6(rT4a&d)@VHA5>*0E2B{Slu$Rscp6gD+rQ_`I*bo;))Y-683 zR++uH=`VhQ#Kdziw08X9UdkNPr_T3({u6>I4X;vGq9Q>I7@7HF`7^tQXr$-I0$t?0 z#wMu8E;}w~&_ZwoS%RRK-FN25-J18X8S(cXb}Aa+$`OS|Vmb0J=DpCEznPy&d|A*+ zaUF-Yd6=x3bxXc@)}4dsLkP5^UN{XZt&BQR1Y!#)qU!VEMKicx!Lm z!}5JTY8gy4K?g)D{7w3zX}A%^&P90RTRBFFWVt2Hw_TkQPeFMbR*biu+UD^3cm&7- zVKKviO(UGqp_tFB5M~V!14LBGn>iz&Os8EtTdQTE#OsNf-<{8lteh3ziO)Rp72idc z3d=kB zpCBbfFk7=~_^kjJuz;Y_ z;xuf)#rn@ZGr>}~p`BQQ7824YY+3vw{a>2O(YUVySH|1rq+npTa+}`C)zIyG0sAB* z<8_vgNhZ(|FWPzm03nh~MsvCD6mIpfReXIFuvVqQWA>q9EY>g|cLZI^<4+ z%=HgDjvKGR=pnS%ZNE!IGB=DnS2XZwD6o>mO)@^a-?@-i?mBsp8c_-)PX7^qX6w&Z z!7q@Ckhz~{h*>0aN(@*u{=4jf7xM1^>wub)5{^)g2-EivtPPNh6DK#dGwpg^dc>@I zxUJ&I4jUw!H<&+OU!js!9b`z<=Jw+!+5r6Mg6Bq6i@z^K;$g`OI>*!u@`4kD_SoIw zH0(PtH0_yJ-o%oHBF8wCqV;4jtczPU+enuW6gy;u&^tf`l=G}?6Cz(qpAG3ts3~56 z^h?4N^7V88*Jn?X*D+MtdU<_kYU?GLX(dFK{iR;=KfN-TUJs{49C~0>RDqN0g%Y+F z#2D|r{OW}Z$c$F^4!51L5ZRNY1fRH$f`QuNg#1L81c|z<5H{mJZE<^k{ zq#i)B5jDx3-mJKEO5)~gG^RDfNe8v6txY^el?~2#4;Ztct+0y>r09v>-J3TfApf-; zev6mN2T|z?l)Jmo7FP1KLIHG#AS+FN{1WO$D2y3a7)jQjxYDPtq&#T2xTOfmOrKhQ zrN@uO$Wo4IRNaGpm!!>hU{PD-Ei)dtGbD0oYc3&RVV3pM&5hPZV=XJTP3AR;&lv zB>iQE+!a!ECCZ;9k417?n>DR18P8n2!q!Z`%Ff;g^le=m%IzC` zsdj3R*0^A*zbvg989*9TeKGeZq2rBws^A}`3`f4h?F2T$c9;0-Mu&12|3cOI)>`Z& zE_1=r-mzz)f5}@i9;-B$9m!24iySBd3)x-RVxG$v9a;+hDSB3~63sq&dfrRNIUfBe z29uZnG0cZVMdff53Oi(L6iY1s0PKL}SA09xhD5}5#6D_)g-m{($_hamo-MF%;uMY` z#+c-7Ijm_eGeE{PHgX_HfNYs6#1Hl~%F60@d32L2Xw2!bV0y{TK@p|{m9NI}+rgRw zz7`>0@*^5MuxY2^@$oX1cSe%e{`myiGlVO6DS5YvAlKy+DtM3XUqX1+BrkcQBVhdn z??HwJ``ZHg6caQ-Zp%U{cs*OP^7(v0G=}bnq!I4%+B2`S2^k@0x+BBxj((^JOU84; zfBQ(!z;La=B$?-JH9c|^yADUl)XHe+&Ve|i>6o9!ArawDfLYX3;L&YhCRc3<0mK95 z5;P8nm_xeR0NwwFeUyX(s3j30kdlzDo7w$7{;cI`vt%iDkiKp70#kA1PSySjGL$fA6NuRYGbk5;Blr15;^D(Vv(#=Cg z0aOqytg1Jm1FsKiusSw$gBDh$sAr*+i>X`1oFpv0bqty!nJ>;iiNgKQOxx+=en8&V zm3$Hk7j%GDa!m3{?6Y6#;F%DEcP8roD1kh1DPbL0{J0~S5BRFMYjXoH7MP?NtZ(UU zDAEnl><^OEon7fN7BEQ|@?SoP1Ck18cFV*}&7I)-#X3&Ee*f(=#gNP5+gSlUxRj-< zrvjbVKnYzpr@tYAJmjtl+L-O=dd#1BewiX+dsikLqC9(`k$y>*3S|enxxaYD7|*au zBrgC|c|N7f>ZBi2`Um}6-rq;F+i*7)nI?;ln2d{-py=@#FRcR$(I14fkjWA)DohM)*`x2JEePGW2V}vzDx($OB z>9dQH2%{Glps%SdM%77^a@n_pBdCZAL+0h~kc7_bR}RYkgT|+Y`I2*5pc2~teN9J) z2}V*CGQz0NOhX}-Vn9CwZu_6z?ogz>n)@>{Mtx&}ubdT4U z*S_APtUJt|^P+3&RbIp&?~|ZVlkjoX4fNnK8`^r`@jk)?74-HrY)IEnT9p7R}>O8vnN1Y+S?S^iNaEcNKMxpII6#D$?o{ElAkIn*94Fh)oTeylcOtlM%CFPrdxf zS&}FQ0x^ytaLHD{x%p)nkk+ZuH7Uj`5_t$;0fHwBDt(hIIpSAbvCfQt3^d%Gny5x_ ze&igZfqFPJ!290sMVC7AuA@Q(iO=CT1!I%6Ef|<=NeO}u_6&dtz!<1CJ=>leAd=jZ zoj58eF!^muAYH&n_N5HPlGlTOP%(fF=*Gjr zrELX_`?4hKkSnuRG48yE#nGwk=i7 zvW6tLykz8zkHiO_y*sjbf9QtEt7Lrm3d$$DunKA+qbt8BfB1(8`EbZs=NOfWnq@sl z#F76-G**TD-Q5s@sG(QoKQKI4g?j<%Et26=PSg`mn%&-lQc0zM$|R)L6EZCdEKmar zMp!}e5MO|I#~<_GqCCgwS3FMr?Gt0q8fXoY_?!8o_=fVJ!c7T>5*6Bx_%rwu&xzH8}}D#+S)0aCEegQMeAc1q54= z!74chIZx-#!M2EZ<32pOvqn@6)H^m%8*8K(D+Z4HhQ&mB;TP*lcb=+*&Ac_$DjQZV zztQ~QZic}^k5_}~cYDcrS_FvPM)HNMdyK{0ew({nOOqa8G(uH;-Z~@*l3oe2>;Ka> zTTW}gDduF4N4-LR6v>PV$sZkHgF5tjS0WDil%f*~M_DXo1CJVRNRsJOA0UQKoIKg% z_MCMYbZ;ACyIdOKpoV_uVBqwr1^?DN#F9_RLuJimSqq2l*veS;4J-h^SK;%gq_9RC zBl#fkh5HHta18SD6V#^IP7Px!oYxG7%T5UFL0v5+&3#Tnhv|0v=v?01}) zYlbwki<+l|?=h#cgyh2^<*xIE<+mlw`IMK!w8_>G6Ux97xzpyK083)RSM)&rjDhPy zQ#b;M4-O?jfRf>@uCni_6T(3Y4xhmfZZ~W6fmz@PYaX--tvAJ7>J`~)7CDdGeeIjh z5dp@Rwl($mM=1X@*00=k+dD>DbEoTiZA+gju1F96d{gcGml9G~~xX}HX zC=o{s;3gy4K(f$7F((Wu2)0v#-Vh!>QNQzdOBx0s`+6e6-38J|6Tx49mhv}QJiI}O z$M3<)ih$rxX>Y2-S9}1^)y&wx^A!}NTggcixTRgpag0)cy6%WyA24&KiO!unLb?U+ zBq33Z?zHnY+~W>C0=dU?hn9>TyT)Urqix&R|LdO;n%Eg#5cfwG1`dn> z?er$rS?!9x>S@UwK!m4Q$F%QsY@LNmS)Bkw{L#hNWCC)8eQg?|a6{Tk{)`Q~Mxp-h#fH0{AU1eds;-saB% z-lyqjeaJHiVCV2QH_d&J-DSQJxoQ6*h+xwm4SKSrutSxzXU{@gk4X&wZ5CzavqQ1n z=Pn$@SfEWGg2X~ar(4-4C@AQd&qHfcCWUY4jl-}waIUL$c?JdSk#PwKWEw2=V;U{Q zXxtfzYGIRafb2KqhjOn*xx1)Z4V*0fkYA8=d842TjL_3HS!B}^FB8J?ysbc29i8O( zPWgoLkigrr5YR(ho+jv_Bq`m4fkpu;B?eyxwUbR~m1&1) z>Vn~fot|w-M?@J@jn?BYll)cm*cXsesTrrikL%V1_AN6-`>*8`unJd!OJfKXqzEb( zCwC>M-&vftsYK~TFzsI>AfCLKGAHxjVi$nN^#8u(E&k}XFe^ct;EflIiHWKAgPeKW@fxkp%y>s|VF~|W?{x+!aA%Ct zB%vR5(JJH9N>D+Wu29BN{8w%F2Ot9hq<}`xLekiKf{{B)fD?iOV{YZ;&+N*`#ln;5fkDG4gma8B{5>K8yF+&Y#d=Hx=52d&)#}m_f zq4PB7phEb9sx&!g=lULY5~+6#K)q{Mb%uuPA&$Bv$C61jhlCok}aPb(&T(n2~ zyq(!6GG;1ap)X~A>Yp=z2dRTlP&z3e0hywJ`c=Q%GDb9h((Y%7weWjgkCLdt?Rojm zke$L6S3eo_@GwYg~n(=XG*O$oiSMoZ5_3rBap~l z98&H6!zB>Iurk(T zFa}pnQMilF84Bu=IHs*p^hz=L`JBa3lH$->KAZc~N)*Uw0Ps~WZbsK%zqaOxy_ME8 z)O}m-fyi7BVcoj%J`g@KAsRtP#Mg^2POhie${56V3YYpK2SGzx6ffKoClbCAx2p>~G$r@yHp&LDrHCE*T*PEIibeq`@5RE@3-gwL;Vj$8&+fkAtX&wSq&HU^%s8 zrB>zID&jtM<;s=uLD$Mj)czzO(EXZi3dnd4UHi^k6V+yYB}-~Y9L&;UUXpOj-)HLm zr1mSB*T*Mt=$uPBRjTGJq}>m}_%5XUw62#8B##5{S8z$V%Mz&wMZ7Ve+j(L>JG;(5 zFy5~)lHOh$F3W;;E4||p#GfX4-eSZI_{CEYV9dO$;5tKijbB4H)_=8XD&(aaWO>UE zjagf{_m&YjG)BDSUPUR?i}!UBGuNma)O@%Kp6Fy~Bxdrd1iX-Lw`;w&&Op{hsfyTl z022Iru%6Y%I?`rvZLf*VU9`~=FiBLjY|T8W%i&zS&d))t_~*{_pQbrR$6mI>BQ`&I zu`I9|5Lmow?%SLKPJZN0J1hqhub3Z7sXkKdP2~Cl6kM^co|l z4tW5@6b2IE-eq0`Qj{zdM*<9;|ISBKFCu`+;=Lj@|5d6;nRXvo)j}DR|Gh$`)X=Jc zj0UA2eK$fCX!SyJdu~h5)qG+CHf-xgfwO1Mc-x?+8dWtfCc<>76p)e z@f?2}sWZQ9Q-|6yB>2KYiPVa3he2vj!WRT=kCLpU@u~%!MI>{|eJxvt%nb;|B}wO? ziEj~(SXyrh5b~(G+09oBeTj%RZ8wp>D*$~W>}+W^Gc){+x|8;C-> z=KK$GK=gkBsfNyneV2iWrXnSue^-MI!r55&NeTjgB;Us?SbTY+XD{8#BnM-3o; z4hd6rz`E_VbVJndlt!qqJ%YD>EDU)F6I+|Ko}Hh-Gwb@}@*=~(%5e8<_(j(!=EAPh zWqE87bjI`><``v@vX)E7_uxuXVb90vwBg1VtUW}9-WKs-M0oRYMwVyr z&r5hNI4Eaxw7{osR5olC*Z>iiDa816kfgqBVFeihxhqZ;7B|f~lwt!kX_F|GnRf0H zrr{kJihaS*F(pl}c~0YPYmm|S;0$n89(+))5!v zKsnkzTcKBL}qR;4|^W+ zg$66_Oz~U*R%2(DsxCMzQCNoRr=Q^9qLa~EL_Wc;dcMX~*K3f$-?jcCoe5Hau8xNb zsS1078tAJ0V>|WL%)%E?1ezllMI;9&I;ZMr*j=yBGCJXC>eU#2yQe^2WjqY;rAk)E z?x#c?YkS6~o?~Qp`}RZW_S+Q}9N`sL#Rx3I5o^c^x*+eF&oOJ12X|nwsBqPWO~ZY$ z*|tU|f`o%)8UdI8$T%Ln;0FBVXMW!MvxUJaOZpuBKla}IAL=*iAD@LG`;sNQX{=dW z>^oryHQDzlvdc2|WvFaL##Y%%D2X9N)~UQx*|P6UDnghDWB*>${ki{#?+^FmQT^bd z*Xw#+*SXH}JkN7ZRW+u<0AO+>-UHqZz|aI#KrLvT;l2>9Tdn6P)Jb-JMrP&XXEngY z8_&-7383s|p&ld`86^JA#H3ET&mg14K z{gdoIE}lg=Kh6Rg#}ciaek6Q;c2NZa8fc1=Qh{4_1b8ZHt8-7^oYX=_1tUyrt=_-G}3kPTIV6 z6gRuNp|LZoEo%s~{B`nnG1~L=*v`PaAYRJ%eE;SpAMm|5W}-KHt2{MV?22LDUXQit zcvA||w}4Yth7mQpO33+&DJ8NMCXjcDswyKvU!!R$-C$PS9QVI3&iDhqcqtn+?we#9 zr*mY8mI@N!(%ESzt$nPnr$151BFsoklmO%fBBvx!%#r_G(Tv{=*W$r2 zBsH4f45F?4HEc4{IbTxruX87g$r5M&$5wwTEFx^3DYrCh8X=HE&8eQ}dAL8>Y zRKzte!kVuxSesM>Ac|Q&5@i74e|79DsqSlp%xCcSA6(rnuuHKutxW+j>Bk0z9g) z)`!rx?Mx+n`7BM(pn5ADn1ld44OA#(*O+FcTD!Arl49^|$ zkl%t!_Svl%Lfz@6-G%%Nt^JAqBjel~8DLbK7g7x*vZeC6L7*s+YJ9jZLhb@$aSS3c z{BZaGd(GSJNgr0YFpKDc3`ny?fUw}ZP}1GIt;)8`E^{<2|FJxs2bZCl^&uEcHWAaR zZcBY8$ZwYb5{UG~9CE8d3D7zRPf=Y8eCdq_K>c?oF5-$oI3@52X@p?~?*|-ifO(x3 zE<4JER)L0ReV}-w0$4JdS$4gL5dV}yqc)%kUXDlu6#B{l=J;)KUmFo$dX9>5gBJMy zW7SS}^xs%Wo_h3gCxC+sXuRrcwNw_0_3B1uWbuD4g`=RZT${Ek2exhS7zvH|ojtEiF!*2%E(d67 zQ#c2p?ADjDbYtEZ)>(2RyEMbg9-s;pyL3$lrJH}1r1jCs!Gt)~)2wH7)$(nU+yQIp z7!mCk89B-MUek4FZRQ+6wD)m&ci3hDueCcq^0(u%AE1nsONr6|%j8&Zd8vy`&d7ZN zj%*q@ANzXr#Wb^EhEy8#Ot?LM?eSAbxnf?f)n;AkSO1U>&mQ1oR8=bjeW*ra2PRb;CENH-32h1vD8Z;95(+!+RK(+88a=I^K)=h`{9y|l~m>wt74LW(S zy;4Wl0#3YeddM&bK!d5i1qDQfJa^e*!7xH*;$`YmXVzSa2{o3N=>Dgyg+LX>%1V}d zkPm=38{@`HX#9l+YN0*O&27FB08|gl!Bq7-wdX40Leyg*U{K+eles62=)Z}iCyt&1 zX5>_0zbP1B;^Nyl#go_l(}h-uM?G)v!)MxWVBAB?jCj9h%bAP+i>`!H4=r?8TngQ_ z3WgYw!FZ+n#EYQ&voadbi1P;PlLv`Q=^Q}Xl=ygYN>HY%ISOP^yIp}0v{MDlgZ>-B zHSBTu+=3sDJbm#&=CNjK$*sG2HaXDu zz3yp&l%3lVCIV8`a51>anN)CK4$u-om^AySe90%?sI#x^-9Vc@uTZOz?cgb|?@xu$ z_S`~NStxW{ALsj+n^eymvLC+{QFP4C&3Y2us6Q@u&Fgaz$y|W0iVM`y3R0H~odtwB zT5^Arp(V|X+_yFNyqbDc(yx;VVrWmmaRmCn?s55T(o^fD5U}JS*qZ-&zJM>Ue;)(N z0%y4YZ$~~~ug68>UztT8EWVvv8eF;)*1lQD{iA*!tQroaeVpzh+*- zT8vt6<)m!SJB?*kY7N4O2S7$$#0^nf<-d=T(yXA%O- zFscKqSEX}|IXYgS4@@T+8(jn=HXOQI668TjPJqIMEw6myF(={syaq9@`HwZx_W%G0 zKqh|6@hO0XcEwNOT$n^#IViAL7Rx}1jC8Gt;4$_=K1f&1ukuge#8_Y;$;~MG8@6^& z0IgOvaq$}C2R@Oo`D-rV_^F(()Kc}cmV@&^QYDH(`*+rH##fA@j%LplWVY`4Uu$Zq z@VR0e^CdG~`gYbDL#zY+HCeLqcv-cOdt(iv6H28gzYg57!&~PSdgJTwfr&p3phL@B z{w4;>5!-vC+VLks!-B#yvJ6wQue5@A8U3UmfVhOdi8dUt3hxt-K4pUF+`%?xSxIdS zZ4B-uHI?S%O!FeChTqm+2z+Cb9(^NBn_n-S)Bo2?iyk?qDaBVpA@Z{;&l3O&M71r> zBv#xyU97MQuX0Ykh740B>vTHxG2Ck!ri060KuHMRy7R}6>sqR+5%OeR5_n>VsV7s# zu(J7>Vgag1`Rcc6P^-md30zn;ATB)Q717(mup@5uv5};OGfWNw*19vtph@WPk~#2K zsNcM70+t7iA80H;K$SyitLklXqUh~5+xW}HE8KU_sIE7Q6FUMQkG%7gCfii2_ydE6VYU@HmJDn{v*{xr)>e+{q31(n+?lXnQ5+0}*tkq}kQ5s~4W1H#_kT zPB8uPjiGQ9Y=j~y{XW0)8l)P?5A@(Xl0}V~<2)>`$x7lX{DoZ=#a?QU_g(!uT z6;bztUga-!GW-~bqr%|sIthq5`HtNJbi(?*gRB$kU@s(W=NvJi?Nz)gIhAaIwZSo}B zdwW-5Yz3N8X>8_0T}$#RY7{L*T( z(~|%?S8Wj1_3b#Ci*TjzZN1|`nCd?42x9@qw7B`6Zf(UN$VV0 zKIr~C;d7fUVdd`ZcN%uIz2QF_JwYoftKG5^Y6w2+9BCGuO)Q`~X!IAeI5Hp5b0?O) zSXgFwObDO8VDl}!`q;S=`C*H&2jVziXjs5_%{KxrM)Y=r*Wk;?*wlGE3 zbovwi=8o@?y{ne&`qot!5Yn6rNqr{_^#_h46&t8IXS;Vi(Xy`awr}t2_qj(_d48Wk zMNZqNnQ_+)7v;dGcc9#yL1_95_tV%^?wkzD%D?^N?#aQXE59X?(|^tNlIw6~rxZp6 zJ|f)s6+SXg`+dG>J@)(Xd)P<&Zs}l;{^7&eB2OWn)!#zBeFd9EJ7af0&WrV6|=Hqg}C@p5Pg7}v4G9Kh$Fo|DcwpG~ab3N{Wq_Q7q zqSqTq%F=0@@O1|LiL_Ip%xI^xo_zufJ*O(v2ubm>hg$S#KCaWDcR-#E|K*KG?_(+C zgFloU8|U2)To@SPAY5f#89Nr0qwA;0>>wVbg%+yxU=0p+jR%yrQzVUk6>1E{3XX^;p?BHQefbe1R2e z16>5(kuY%dnVEvX7l;y=Vo#2bt|va&yW!^Mwg^1LXTUWxcjXGiJ6%MD4i%^dHxFW) zjmqQcYB+51ChlT2U|-%zS48_isaT{|o%IqI&JbER_(+q$SbPG07XjpO&cws_qr>rr zU`iS<$p3z@=JI8;g@HVO(Xuqiam+4DZ=`xR>}?V&Y)eHe464$RU|Yw*Y_DF{ILe3F z@1_{E<8>2Ez}p8orR+f?RyKYuph9ZY!ptm&E}Xw62&fCs_yD+m<9DQ24S+|$u;sfI z@4Mc{4*G+aC{erOu|nfm9f6JR2Qx;55lTC|Us3lTdN-)~=Dc&j75+O0F>8=x2(SZ% z#ixH!@fIrzdAqUWForm{I{kJt8!WKeq%7!E%@YEhq*p1BFntC2agmyO9uSi_;Hs!e z3m5^u01hq8x^hGrmh`RdSt-6=Ix)%BIgm3=0##v&0A?Fs%0cZ`oR90Rw659u6OEq^B60fu&9KlHzKt5841Rx=c?x8iM zc&5NR{cYp(*4|U#jI)SYVDZH?)pQF=`Mr%`9mOIXpB;Bh9@j!y5CQMNbN}DNl~*yl zm#V)KsfFO;u0zWsmC9_g!c-wrhE!ILET16Sy0BgzG=6x_vroi|)&j~u2I^?q=ID^j z`QSpV5)ttL>z#vF1Cf(}bXZqeyFg@?7yqF7nBi~yA>;H+j@Rw+cOU7xqL-_rm~7F5 zuNttclS?D-d~hB9O8IoK^;g!F+B5T)^fj>kwJhT$m zb9Kaf>pqcPry(Or(#zPY^49XNs|w+oH4Vbrws^xF>PE;`#+L=zIT14PjcKt=uHf;- zkalC=WoMcLnLsVXZL!c7UHmN$wSx7-p+FRYU{NP5t|N{fykLuUXt0mr3_NK~Zz}!5 z;vEU7oVEiT{`@i%Rn{jsETr>--&}Y-3ZQ^Or_Drdn4?k8VH7vnoW$XJWsc?Y;Me6V zEnQ~BbpK;EbPHI$#tDrRRB>z}3d2XhWZAo5_qa=L)JNg)lcV2VYruk~A2Di!RfD!9 zrSHLm_bsQoMk;Mw=%#fn=?T>1V{E0jP00C!pBzp!n~_kYm)Yv|hkk1yy1?CmwgH-; zIn`Ji82i#Ue~sxhjf_qP;&1TY8!K2%TPc74ZtB9-=6ro?lov zj<*T_g1-8P-?KO-721bm9nZ4Qe`?OwQKIz)Bj;<bxL;ITa$$ zE-FJ`Ik!c^nsUcG3oR_^JD@5jl(Ax~7t1*EMmT`!f;f2YnV~}aJ39?H!;x|IlZMA! zwJzZ|>n0qXz@0fEkP?-Q$6gImXnLfLLW>1KCD3g@HC!tk`zCU?-Sz~&SyftF#cLyG zjVw>d)6FtPYuDDQH(De~dFfb5=R86rjJdPf@=3Qe+>hm@+#nr8z)?mB1l|C`a-!y* zt%Whh?j32B7u=P>e;4Mgs~CFuEtTG1WfDH}(ux({0c*{FcpQczey0BVfCJWla8tn*{Yr`a_WBLjBos*`|ZQ6j4O|~Jb0O?Ce?`qGXwT|v;kk* z4vIQvhLo_IPssA{4qeM_0s2;kaOOa@8hV1u;H!~q3KZQ9Fg@`@0I*3C76F2X^nr{K zmw9c+eU;{HtE}Uac{OVxV=RNOhOa4vGuG7ccb!JpQFREwMRTY0FH=NG-X!_tsJje- zLZHm$be!SPoi(879shua0IQVH&|8f%y6#G8^;gbY*0!jvA9jO?|i{;Do$N|VW zitPYZ^DHe1JQx4(N-B3-y|li&D&rB_HcT6_|Lgez4ED;tVcDJNnZi6R{bwRdiTn|y znN1ZGWW4gET)V-*Wvw%jrFP&G=DnUtKtTFW8y5Z8>)-p>$sfV{MA>lsxToQtC ze(L<6J(=D}pa=SG)q$Fc7pMm_w4!FcxBhKGtEMLS4RFA z5BgrCdQU6eL@{FRp%&Zyd;7SgM{TF-Twp@$JG#boBhIi7tbm@;mglu9ve~r(Ez^~qvWK3R1IdKoWPU~1pgIy{jmmXAK&vcT9l%F<68%hm+2xax=7i}<_KLd}mg z?@|>+A$1R{;^11adt3WD=g(r#wp3ngggETSu;HaYDGO)?S zccY!-1=I+Z_Q&}mpT|`4g;dn)7|{AGuI@+SU$|@>6`|1yFZldt^le^_EGu?4e*=q* z;Hb%Vazdk+an={f^{(U(;Q{jKvU4lW;Lc-Q+ID*bis)oq?R`ESDDv1ES{@nm31z^; z+Q2}Hx%xRmc4-12v7{=x%_sm>kY0O+;sDJBmL0D6$DWT{J>|zhQam%&#q(Va5=T2x zw-CUgI+2_CbJGlns5vBaUd2ww@nMwUo&0v;1R+yonX`%6(?!@Wy8P-8qo-=xCC zcesa+1;HY!RLI(Y9093;@SavGR)%LlS$||0fz~14cZ-#0l1FPyqy%XWM94^2q1tOT z@#91Xy6J)&6MaX5-$&QV_`5qT(#2K|{Mj<^!5QU!J%gZdd+V(Z)MX zMZO3nj`h6zL!~7Y z5|!iS;VvdpF^uECF4$s(52XWXYq68XQlt^$^Ogu5r^|G)>?-ZOgyRdO^^_li=#~P8 z1I60yrSggc#lyKUK*yEsoD4o~;S#0A6wTAbNm0t|{C0Q`<@Jkd3GJj=$9qmED0M;l z-NrmNYl%Tv+So;ov3rVcn{QA2MkTaOjdQWJ=e;m)=BIeHvmd%1EvipHSS!pPti(0K zjW|09it~m$iNq1R+?koK5B!KU+-9-}S$Frik}V(J$nlP}b*Ng83x$#+v2Ba?xR)8w zV?*EazZXrsxX7nFBdR`EGe5afOCPSEd!tzDTa+xFoER)~rhZ?1dM#tYKSRQ&VG?Wt zjsPvDYX_}rmBMyaTBOU)BVO6GwtT53t%p>N1*(GL{6k`l4kG$| z+22#7cA4%sVY`HP{Cs;K{`g)p|#!O$8X-jVSIu@9Sf z+}Z~}Z`|5<-6KJePwNXs6gqeP$9p7R7P5M6uJB;=l-sihLB333az8@baqzK+E=6fG91GI+eOh;#%?(YDc51Fo%xzl!us z0t^CluUz{VYp#u%EK99K^~(O)&$)3b(-pXTd`RfhtbG+$kPH=}@&LF0P#8WFx{}~| zoKAFdB3kNIU}`2VVRY=?&y3y93r8v>E}{*NwKQ8C*TRIzNRMpO5)Rk8e0nrws14G@ z(schD+FC7(!o=h*5*Q%exwRf2E1YcDr{U&QS2IU9croz}V(E>03h}5ed{5OqwP~Fa zZn6YzVGHg3tC>+76FqZw{wR_w^VdK`elw$7dZu%6k)vdOCO7t5G|6rLoc7^` zd?ak~aJI+i#wVNc;Gi^w%bljMM<1$p(@ay$YQ~_-W8MuNv|50`x%Vir#zm^EjCjLf zr2lWf*{SvJQQM^5-5o8Sa4s@ygaRd&&~o~lEwW-IuxPj44%W)s1R88^W9?`CU&lPiAKl+q0MdznOyfSQT#Jk}F zF)wobY57Q~*GWP?z7%h@50r1|8d^&5A;yGg0bPez!(t1XKKtHy(CER0)vr`rg^=h= zMt&Y6Ko7>#qzh7IoEVd5mb(KsKxR@z)(3v?s&yzFZ)U~J>?~bIPDNP|25<)ZRqmVj z^X+8I>K}$%R9)}a-7F*Gv+?;Sr5$o!jJlYhJP)+U#Xw`50S)ctlHn_lF?kL z{n*T!ZReBG>&2+Xi@U|b`C;q>t}i%tMv@@%@FOlt9S~H2iA>pCs70YQ7h!sM!^w zRm(fed>EfInMW;<|rBknzxSb z#%Z}SOEW1amII+HszCxXj7Mw!asxA@5d#GZxUl+B?UfJ4v zWdWjmXzoVW?Bt$xa_(*(`ETYKK7q~T{U?A;1UvZjMEtY~-a{f0K&N0Nin~-TUD3;HD?VzeIYuL8|PhwGCE0VE+(?{M1f(*?rM~jY3m-u2`z< zydh244p+USNxiD}O9{FQPcSBoDLaGc-!Zx0bq!#tx~^OAd22KGq)Ej%8z8vteLs2# zUh;IYSvQxOvX7>ddL`6w2MpwMQerKqn4X&^#}kWQ#ydzGY*0;_3;w(-#)OV@$3JR? zG(2eX8+4u_;gpJb+x|L}z|$rbW3+_OcnI}7&SFfuV3jy+@q`Xf_IG)>7YUPT4D7sW z_g;8MC=XO`+>t74_n%~~@PzlfAKT4yxw%s?ZdY|J8Xw#oXeKakOJ8A*j-5@2dK1Y# zkQ?G+btgDw=TXkla@nZibTLF5L>PM$yClH)V9(KFTnf+|<;`r}pb|kjnQ2%vBHLmm z|HP`{4?8k)Ib{v*WJc=5| zU(2&OZii5uN}3mu^M*kW5<6OaXcIGF&Ws15L>T7$383SFn52DFQ{ z5kD)jb_VrRjy1&u2Xo(#gkO8?%ul?mh!S<{(qQL&8al?Vg3EH#=qTU7r@e?SVRL|K zT|_BhC&zZs)C#Hi-6*~op1!^^@FE$&A=OxqL+cb&;W;2XPg-|SXq_U1NYy}@)tSad zjwtK!IE|Cs9N!7E0BJK{-g#$?_fV#8c_+ps39gOq;f)r%6|9o<&THS%veNADqt5bO zVOZYcJMq=23WKyy*hB#?w{yG5ZLLJ_H}=MsSJw$v*ql$FEb8im4$qe)#Kl>8Y^ErM z#ioz2av>F$ZQ{>>;J&{vOsecVu94jdj*vZ3qF1o*!q}==cD?&O6qL-fq6}{eT-;x2 z{l2ki%PfOV{K>REXXe>#%~P_OeBGJ3eU}Kl3vhw#H1&+}!!7`=@-i;VE}}h98|eEp zVX!q{gNO@LHl#V{asm{a?PX)M9U@rjHTVxW+|E5UZs_Mg!Vh^DO*MPM%I&c*8wIQ?=V0TAar z%-PE-<9*M<&wQ0uG}-`FSpKH5Xg-Kk*~QL163upTh}z7+_(I%x51lpiJLn2wJqv&r z$=Bl#P|V4#hnwoj#5vu2=H#$u>$G2MW+PJLbqVj4{~(y(X{QWcUUa(A4uNYGt~y*!3Ed zO-ZsHN$$yKQn5P^bZ2|XGmfQ+d0Z=Ty*Bwpwq`u#pz29m?(d71$+>CTtkdvm%lVzz zthD6*PeETIjx`|B`Qa8?5MN|S%3_Uv4)RDMooxC$HfV}duVO!&26G8QEQ@@>I#Pt{Ke99{8lDTM#R7T zi$=L(Nh9ANMxRhCbWH-`Y@K_KIc+)(C$mu zj=03O{@+wea1RSsOrl4$`hx@~{}-E@W7V0KN;dvMZP;`!X3r$|hPp@Kw6x3paf-{} zyWmC6%@u7&!kBPDL4iQ$){w@0jO_wn>CL39QNE^2RU(PQ78#slOK4Zd#J2KBNUd|H z^B+FO*z3HyXkJ!#;eASHOqb0=+8x2DTdZU$H>cs0FaF2Qh1f=0~P9LV4_VbzPhz#tjD@JXiY~%JnQYI*RBatrBH_ z=ln77D-+w%C4#pklrLG8i2aS@SjSzX{~Bs$;&ty~uNCT@n;d&Gp)6bIF|FOM1cz)* zT78Lw!SykD*YH_)2V7U3%+%mIe4;oTQc_dMTB_)H%%VYHCux4jVLNG>j#ZXD1nA0D;zbwOWANNW)2 zjuIS{-5I-9#GQ>Kgif2Y@afr|b9VcMmr(1|hOLc=-VT^D=la;ASn)8n zmBCwXL+vwWt5GSp)1o9Lp59`q&0Qfq7@TT3%ijiLwy)+#Bb-bk&A8=HAijApNJ+Mjp$L7)#Bl&hyLfpUa>xIN95y{pjXM z1U%4ag6@Lk#_}B=_Rm5W7cAtFFV(tz*QfmSv+iiUES@lYR-4r%i9vO_@Q`mC~KNl|H%*6_By>aFO-pEk}}FkFQxY-z}#m+&6T!fOJInLe;K2 zC>+~APZlHfCvPp9rl7Wvxe$Wrwv&JUH_=!awm0^?4q?=P|L6 zJ!ptb+~n-+?flPR)c4DAl(<#TCNA&i@ln218KHkZm2^4<>uRJ>*IOvfKAtb4r0cnP ze=OQf%VvPoD}a>T%FG^wpBB(iO{d2SJCh?eQ+n^1C%R#L^!*uY%78MxJ$|nR{~}s; zqe$SiwDWP{hG(gq1pbLy73@fk++uLqSAU?F3@k`#_8MI^vU)e5>`*80&P|J5HzXU@ zGqbpAV`ODZwe!>6p@$Q@b$Y1v)(6I=;WrnIY>GaA{)`y|{p{O@a>vYEy8*|=*U83Y zx%y}41uX5kmN5R)_9k)X*Y(bmoqX8`uuqVF5jkAi4i^xZpT zuHgfuH8|19F4qr4vr&)i#sJm=bVKfq3B!{6KeeF&&{@O!H?3LY1#80^NVbDuV#TiG zw4E41Nj>j{%9+P^S|udd-nE%KWZh4t?ezKj*<&BwiP*nZCNvQLsozMx)Hc5+S8+ac zsWgW0mnnk3jV>jAX`Sue5Yig92%Nr0^}e%HJDQLBW>@m#ci_TL6Cr`7N3G1nV)(Jn zJ?zaf0pzWfp4JVQ!;zQU_SWxr?Dc4b(?MsYI{%sByQ7za(9%w^>)RS6ffD_`OND5f z1kXV+J=zO=_o@84E3UHSILTS4_leBu3paPB3!RT!u_l9T6eP;Sh2hkw9$2;;^(|A+HAGr3bidx(?LSgQ`fSgom@S+4!eisvgcZ_nHL?z&9DImRylR3ixgrJV zm6o8=j03cRIF?je3Av||By?rtnxL7bSXdCtF3lCtQS=gL-NvFe(}5WI)RRvFpN5kA z5dJbdSF{)@XWgE4+>>MAax4EPMjYg5FGf|wBysF)8)5p|99G)DXWPZDkMaQc?p)^d zkIGD9ra;I`AzG$82$C|+1#*-|f0S##fz@dtSg`3IX9%=YqckmsSQe%OEKo!%@HC zbLeNVn4x|%uFvZZMg3vTfJ+xpKR2>%ZUoa0419Y5+`^yfsI9(>#0Vl(EVjQco>6Ki z_OhS%c|M2uN}N1=icw=rhLj9S$d-gMQ0P1V^hGwt{=~t_THP5m@h7^3c2k~hqGGOh z>fDYPrKKhMZtsi%e_1hIwWtzNy*B&ig&2UBy|H{FgG^}X8i+_B7Tsf)eQLAt?w*|( zYqhY6FFuU#0N|G6{gi0(%79d-LDG`?)+&WKJ(M~$zAf_whPDVn-RK*_-&+Hd&r|gF=-uU zV}rpgR7E$9r&^h~NTbPYB?yV^MszOE!P%DFEW#ueTNBTbCw=;Mb^Q!Nt_xDe0) z)XO!si6G!eYI$vdwWd3p{K(u;pRVa#`Klv6@*y{IvZJqkzqf?{|9KqA>O&McX$+iI0(Y!uQ zCdw~Z<(9e2wF`jc(HUV76!yr9D0`v_!Wp%Pbcu`2wsq&42v)uGOA9o=9_!8R4MOvY z15kmk7Hty}yf#G)VE0oDSn*t0#DbYsg(_>>u%q}FY;vI3L$g!hKl93zBtsJ~sAAU! z&6g^eznyVUjAksiM|Qs`tnpkGsnE#MJEQYnb@pJU!Pa~B9vPKb5ycP9S<>hVL4F*l z4P($)4F58fU1(IGpEm362W@Bc{r&NR`f-s9?Cku7$wSY$>|bC0fa)3?a|CVe-r~W* zzT z+k_R=_dFN9bKU$%viU^IBV)J8D}%Rwe~u(Eb~e0e&}vF)x_hJnNU81DjD`3^0bl)c zNq%Q$$a?IdZOLGGwwkr^pokh>)5Epi)hafp0`G_8Lp-~Cn=1y3IatB84tX>t76P|N7p>%b`kx9E*YeFWUzL4L)J9_uEo#s>Wz5CVX2Z7Q^N~z4jeas1`IF4*nI3 z9CZZQ^179+$Vcar#L#bBPk&8Ip;`BzWfh=lEie|s#7)y`*pXIPKeDUhON9WF1D*Ii z!k~NP#>pw2c4mqu=>zfEht`yQ;eA6A+`x3Dt>Nwwj2%)$BgDVZ5v(i0#($!lQN!~b zS)`_c7%bf>{`XE#wcSNJ4!`kk28=lJT!+J>5n>QfWAJ-@p=M2@FKX4^ zAcDWn%x%Gcz-N%R(29)*kbilI793ne7gY7rx~FL$F9R5z4la+9zCG#3-<;Fb7f?E` z@qQ?|^K2!c?Wp%5Is8*Y5RpvbyyMm+RW1_8lU&T}=6Y=*l%a2ZWzQDS@B7!O5~pLb zq(HJVxZ|0bkE4sP!sT)SaVxqwZ~ zu2OxlPU~N0>*{uD02_G`x5(52PliO<*-cb=lfGXy(QjM;nHpVzW5w)74@bk+j|kj| zY`)~Eb%f~8-d`Y&`H_k(e`w*0z(%p`N0MApszrCk_aa0J%^g=*rN{*VoIdY-q^$3p z&oln3S_uXkR65~JvgcZyOiN%BeH0qj zqXrOw?f^Cy9ZMLPA3V);aDgmZBDnF%_lJj_QVu3Eo3bwpp9Lt&5gmD-IQClR3}!-& zAzgfOqx#9AcdabZD0ytvfTS`Rm(eRKjg!A5HIoNo@&8!DBn5R^wQf{r^hqfFU4JrO zFxdX3-!$*CKbL|*!`4$?8ta{t%tUJ`0xaxYW~XKSYb^Uutmyc&XkWcdX|JuzZO+;c zUcj_)+XVfqI*GrYY)B6L_nipS-nQ%XB@s6Nma3x%d+M8I1q}CB?nxxyF7iqFL3U5M z-hjQwz9TRO+{ND4Ju5K*tEJ<7Axifh@u;nduU2KogY@3L(;Dg9b-H03@8&w-?aHZ} zftydZ0mF|&`F)&zk7oNVZbZE3+0XsuhxZ}LW;6ZlvN{AY=YHg8m=VVaF)3rum(+^c zxs2q!x-b{YuJ~+aTr`T=hOJ}YT~gUJMltg%U;)gW&2!sPagY+KpUJ-u40FlY3+ceK zH8RDA)e}qCk@T&vh{0>LT3N@NktHienLBfiNc!l=Hs>(s7F3SNZP8la|K$R75Sp@$ z>!k+0zD1G@N#>+1gS2nRbmU7Mt2{-ccN&%|#u4y5e7*z#fYiHp7-N6QFyO($ji#^dK)%|+)Q1j)isg$634@@gz8=gIt^5}EpqVA?VRQd8Ns~-X+ zw9euxGfJGexhdcn8{X5}6Tf9KR{ge9vujC8j58{-^4DHn)*n$kDzp~6dCuu#B^7CvYovGQErsmIWMYgoQ6lU%Fk(5#k zJw-X>oM8n(yE*O2Uo5^_!wF7qh?Al7+?p8QRu;V0?O2e%B{kVd^bOF9*!LMF3>c%zZXCJmQiZ<-I6$|&LH$xNF&GM z-kg(?r!#L`2XM6YxxiBVa z$^4Y>lH3aWC`JCj9q#82$x7~xSA5%tHA+&q?qv_Y=nK-kJubOq&DLbad@$YG9CcZ& zWTPd&D_n9Wrs{$z$Yl-T>LlO%*)!WP`EvG0j=Br9-Sm&$hv4`zSYBziVGK(P_HFhH z5orwVL1@ve(#i0be3VV%)=5)?7i_q+G|4#bR#Y#;u5k_z$7jvewMKt2d{{uQy0RI_ z(;s)Fxw(tm2QOIE^EFBI!OhDLS5%0NlD7_}(1Yz%_u@#EStaG>#BjQLN$$=;2UIJQj}c1QaRzvlW)}5I zC1wvoi4z8wUliVdBL$Ss&m=2iosJggMi8>H=~KbvN<5)rP1(l54P=gKQ03z^OLBO z`E{T=KYky;dYQ|QvA!ac%CUL#pOIE2e8(mvN)W3l{1>u>nNkAl-$k(hm~lUAnEXdBOco_q(mkryPoU1An$Jho6czy~bs-r=aeii43PajN0uNhU!IbR>TZ8 zG~LcnEasga5(?7JCRaf61reL662SeoWB2@@Sy`*tqC?cJqF>jQ$Qq=5T(Oyypv(MI zuXvE>EbENek(?yJW`R{{j|mD2a{0{2%8_$}^|H*(z?-p`;Gy^NrJ{j|%u9BtQ>!d) zZ8c+22SD3(+N(daUBojCSS`CXt1sLbeL3RY51ZBPag`g?{eF%JQ0fmcf@qAzKN(iR zHQC*49GthF|JeUlu}){Uh;lZym;N^W@7OzsjUqAG06Nc~-z<}ZTQB5%c~+uFBQ_ON z6~=*kX2D){eJA&AhlD$ESLcg5j48D)W%&-0-v&}ldz9ifa>A-)Ya6;76?552^3Bs{ zS^(=@g!v=~U(M`H=Yi^0B+zElQtiGhi(K` zqSrXXye?}Xa)>R_oSHCLX#{bNrM;6yEq2f;vmf{dwYRLUC;rkUbtI3@ZzX7BRj=eI z7}mb|(Kd)u`tf*IHHtUkUGK!O6sV(@75At)L}gl9omc$%j93LAk*J9-ghinz0j{NE zX(eP#r**A~jS^(5@P+ z$Zjixxs$~n{A|ikb5CS|VT;4wdUv-RdV;1D%%Hsipx)IW1+6EBUc5i2x&hQ^N8Nn_v!fWp~Ph7SGvqFc`=XZ zLW4h#s@p~*54H8`ZwrHdIjd~*VVmv-`5zJ`N8by~pF5BbQ6+-eh@;tq+U$NWT`tx} z++yS3qGPLinn8PnD*JoI^}_?d^p!^(w*)0Oikg@yG=A0zX6EK=AfPFRI-d|Q-{&6* zPEo}lI}^s%{mzkOyaHvB_$_NTpK7oO3--lgK_U%1N-$ii>`o_rah8;leR|Yig!+LgzYuZp8nxQuk{QlcmkhXp;NQxN}co8hK5-hb;ej zTxEWF$KXb6%SuUa=iOeG`3n?Y_8(FhuTUg8#$bO908Io-sj|fQJkGlKG)t&LnY{TyEJTShN{jvcK@8}QCx6u>?7E#&@`;m^AbGAu7p};z za8p@9{i5a8tU+z?!dS}nvZ0iIvzB!6YUW0^5qC`Ep)(+1d|gkJyh+RM${%7HuME-d z34G=C1~%tvoIvd4+xzdNdY6_?2__fb4FzdxZII(wjAkPNs z0x8hnN5j}~o&8r@|Fb#s>;Rd1BU|_q%}eb(Or#sl*)Oa}>4GUI9zT%;qM0ivdwkr9 zKX2tQ%m*5z0sy(kyh3Q*zp3e5&p<(x;G^NSZ~ux6c-PGAte&0U;v1eAv`V#_-9UVVIW91NVb`l9@_0FWkO%Q3D?U{{=>?y96ZO(sLg%!>9pAp7Oh6p*UeS!> zT`iVa;2gUf2l#6{K#;Lze$D55?U-WYgTlBxu$s#`469)+r%rw#H{&sn^-v5krH~)R@fA#p5*rVxe;e!ftEIM$A2>(4y-YrrC8fATHO9vBiZ<}W6{~~f;5;+u3yBaX8Wp24! z??gL|r<2gqx7>gA5{Or*J=`pAIghmE9VR?&HAtFSH7Nl zX3{Fr^{aK#%|Zzp2LZkK4Es4MU&~BoV)jtAnUro~-LKzTuk`_IQwgYt2X59={s3gk6t ziYG0Xqa(FPq}-(#jj9*kn{t8}x%3EsCEfA~9bWtYL~avu1^tck8&*L3gNOO3#RT;% z>q(i`kc0?K$d5Mv(^J9Z8$Yl;Hd?LHEBe-uz$=FxxO;!!GTCK3pV=)qqlCxKy(W9R zN^j(|LmD106P@)~oK=TgYB84VDF@gaL>=&|r4Xv=vv{P=V@*{oxpn$1Odq9qAGOUC zcJ4RM?fHwnABC)OVX$vB)V}=0sUGoVkGfsZ9^S^chu8h={pfm| z;DPgi+Ps^T4G4JJ8MO2rNrZ?!5@bSf>L&u0%ovB4jIr{mgx-}apY54>7%&`M`?>Pj zTNpQ#$4Sam$B=}%DPTs!kR(F7_oGfrqbpw&F5t27Ti3X?WyAM=b@tm!a8f+D@ZS~W zh43J1vU1#0Mnfw4hW!;(4I?L|9RK?@H37aMf9K93%e5+saQZgdq5_Igj;~RJz|u11v;XEL?mR z`RPF?#V%2<9Q3ehPx-&j$Z@E&b|ipDKi!&suN44u*wIkE*_!LsaPIiLF+tL z+nLkhBmJb-3@P=QU|_Ro2f3B@!_DR??{EI=tF$#F9lT%%f@z7aYvZ1#@V4!zKYJaFS((YjM{B{2ItdgB$0g*AFO+$FS>F$axb+krGt_fS|`)1QiC^7eTC zOua@o!N0BylI-XR=H=)M={sRX%O*CN@OF4tJGRM6F*>GJ-M;13Rg&c@Iya42!)pyG zqxoRCyT)+&KbUX&hy!tgP_#h1oU;Wn=e?fPh{H6++PUmB!R-dS6Oz}OBY4N3+kk2F z5;sX;h~c=Vbw2Q=BM+cqx_4{*lW%zj{n#;s(|zcQ!_A;@Ze}$vZ*}HQU~c~UJ{;}; zjo19X;rn_PvT)MC{}e+-0ZP&$L&h3I2YIc(NC=F>Y7L6%w>D$W2P*l6n2Do0f5l@= zNX5E22C@yMXRm;)JTbytxCvxORJZ+zRF7-dq_6eemmA4Hc7tuH2~fd8UWwCup~0cf zZzn%%IqsP1#)W1%F?sf$!6A!CaNcA!s%xdNGu$4^r=_PMSwrPi4c|cx0b?nV(q7>G^>Nxc1vr3|7XB-x*`7p?JXB$epVF0^ zw0@!Ic8|Sq??-JMKgu{+ZRPm@w^Y4u-)bLC4itqCX_+Dr=&x}3RFLoj*8$n26oJg9JfEV$ zya0Q)LyfG`!MneT&ZvkPvO^IrSm^s|ObN=?Grtg}tWBm{QagtI1Tj}l= z7P3kg5e?(M&EvET6cwD?a|wlKieqe@VdGo8h9nK;{LS~n67BMGS5g;3R_2g~`J0gw z4BQ&qtZ6io|4#}y??RlFSDU>?Rg!tC3o1$NXEJz{!8cY4g)F$5Blm)XFwPot=Z?f^YVG8CEj1zfdw6(5f+vJ z*hJ(YkC4^OJ}XA#P+D-l(x+n=@`DP!;FheiM}fI=xZT#zKL}`X&Gnl#Da3c0edN|z z`q0VM;XJ?1QYh@SeSCjHW_IL2<3Eq}WbQf7uN z-HwNdvF?;$OkL4bx9x(3jyT>WJ8hVBF@Ke8x+5S5wnCiOJbIeH&5hL+gMeCUVT%<1 z3sSx#`ju_PhyJ5^wHRwf#DJ-DK%`C zT#xQ6)uk>YPgN<^s;D=X%UA)+s$CT3v)@mcruQW-VKYSGeT}Ymeu-3seTrFXU-&jh zhqJ_gJO(?jw@1dp+6z`?bx}nQ8@?tcV<+eJv8XzW%~V=mgv7tbPj>i>Q~d`v?#dB2 zs}kk(GgQyJO#1YAH*>On7`Dt>*EO>Kllb!BNLW(W9qXx0@({C(RKda>2Gza?XuKoi zQyc=G^i#*yNG!2zkcOv%q+t~Uz3@UY5ruSb<4Zq)}f1F;aJ$Eq5_p% z56YAPtERXttL8H8i-#!9_3k+32(D!K9i0~Q1!b~R8;tk6Et0QOaJ$cWFO-s;c5mqI zdkg9f1w!csW+Z>R5jR|bp&I-PxCX|GnSf$>oah`|fZ=y_iff+O*?r!7*yc;E zSe}y07fm;~8$xre;{QaEp!Q}Ft>S6X&zIlg&hi(j5MSCi(iXe6`{yyoxVyZ*<^rtw z4eNcffE-Pn&*qZFMDNnACy9)MZlW30<5nQg_QGuJ@noAUYLS|dOfckho)I_+ez13^C2QJWw9!0yfuoyvWo?pqwZ|AXWJf*3cch9}!YmT)?Y_9N zd5O)Dy0R3;H+&RsC{8n`-+JpoF|W##_Pcjfdiy(yCzTONOFwbY0mNeB&+=Ga4Rz!8 zx~Xo=YR)PL`_l%d?8A+wdr={_Bb)m&^rr%-jhfjZ`eBJ9fg!c$S*MZ&C43hN$M;GF zptfV|Jgciv#pk773dx3yQ0>?JjqTbjkwEa2&uPqOWw8d(73{3TIU7sckS26QmKQi> zXGTxup|_wdEoT?jkbNi|t#tG_Mbf9%4$Nb+&-(7jZaA6xv&A@o`GMQ4?W9TwDQ`5r z>Im=6svuOCob`Marwe#DfqInBTurnv=nn9m$CpsFCzf1sYiWWPy6b=uMYbcA^B2cq zxtv0{gW@A%AUi<_pVF*oQMH0#GvVk3y{nvsw`NShE7X+Gnn8SoCOTZ-I&!~HCcgri z*^nI!4(@=4zSPi~VmXAuz+58o(22}QV2ov4s6#A+qOJQ9n-9YdF#soHNPz^(vgzroa+v*~K^qwMO(g zxlez^x-b%CG0g#t)g~XZHSSwJ%yTupaOGv7js7Jsh8T{xQICt_iI3`+T(~pIIC;BJ zve!8;-}J%7#}7Vj5SjE9qt~hM45AV4ogb4+$LvGl@mk4geiWK=pzkJse?Z^9awEw z*5g{0SSOgP<(9Fav{x=jPH%dhbv5a<gNEtyzsfr2?i@=Yxw-V#t`ME z$PxpFPFZIOZ!f=Q%>d*pes52J2=-;IWIT$QO658mxX!muH*nL z#%T649=oz}l+R3n-vjxk=%Y^8D))PYZDShW+YK2>Hsqm9{TARe<_mHUx=zA6PKc@FBG1ESSaT9TjdneYjncTq}9Uh|n10#8C8f1-P+JY4>N3QrYlx&VRRL}*z z(jV~c}pMGhpsCTm-y^eV(+8rtYit$0=9L-eQW&NI2y)95uiZ@V4Z)rVTU@R^fb&U>ATdoOV>^K@36!0r>SRGQd^@2nX)866E$sXqifb}|tS03PmwnbzC0J)h0s=+?TMfOz z&2W8Q&5*^J|GAmFSg*IDILo;rj)Q*eQ;USC9nDb82^%gs%-uwm8yt7C@T9*oyUna| zV;5(>vr9y-?IEAn=6=%%_~Z9ZWiCLu4jfeg%IHw?qa0?Bv=*k_#EQnbo?Qo?MZksr z*p@Xks^j_?E&Bgy0hoXw%>(X$VLf3$paunFKHC9?ugk0E-xf5-h)tjz z)+!dakflDzrpK|+msVev3t>|DRbg#pI>EJ`vje8M{Xn$)3-F@X-a*{F3uvq8k$9Y zYJ%Ta(_@`XgtGYi&8*BsD)V7Y+TrJ~RtHuXc8*w3^?#-nqoHEYYZrQhI>XKz<<>+H z?=}=(aX0w|up(v~UL7};VX^VJ@2H#KWw}Map_ac884+NVT>Q0vrk}HM`aS&|!-w&V z5=@yzX|H&1`_YUrk`1^qHd8dFlGKms2yQ)imcsFAyWiWj#a%tfoodm^fKnrq?)${w z(7CQrdrf7XXN3z0Ye*1%iwSk;jq10lU`GU{I7WbG z=ZhC2Me@!UhQ~?o^*+)xLIcw-?f&l3x0CoI2g429Zz2bwCiEnE;r%|dI%Ey`kM^eC z5$Tx{UgQP@-qO3QP}noTYn9ei@SNJY(`=o7CCs|ESg(ayOHt!NtVG z$f&MOAchJZGAWO4^3fI{Qz8?-9kcimi}f%5^`}dy*Bg{8?D#4acC&$DE6)e&^Ax_C zJVDY>nC9Mnc&ne#ZN;=$lB1w|`{>T=5 z6uJmCHz^|B&Dz}9ar%-oq&c1J@j>Pl=- z?T*5bI~MEwNbgz!UuBrG+Me;@(|wnD%2pO2&V49zy76E9zF=W5`e;cS+M!`d=`Hlz zfj(VDj3k4esP!vIRF7+>NAyNm5^4$;kSx8cl41@#rYH?Z#k8SH((P>8R!HNW!dfi5 zqIen>J8gXJD>D+L`9`w19=ZF;gUe_}l_e}*ypTF%H)iZ_AK6^s;zBnbSRjOm+7;Pz^$#n{9to^U-v=Zjhtc#(H?m zpo;XBhN0O5>#t|?aLzzJo`-!JMn)B4F^lgy%SV}qL#glT@aVJ_3dAU5u~@f>WaHA* zzHR7vu15Qj(V|?gM7S*%J=r=i_D6mJuJG_`1r{p>?kR-b_|EOA{ruhm8a^z_jD!)!-txc8^|v{uyCg&41C&C8%AjeZc&{AOJ_!^#=YMMeAt3Sgkb!A?-ge}5%u44 zZr#L8{%#{8A7JR*OT(<6ONiNfaP^;}5qLu=y$ZFu_98=>rc zZ89XYxDWP4vktG&8=qB!gPHTrSAHY$dEe33w+)n{H?Ayhf{sbFgteRw8S{XqpyCo= zNLZK#{Lygk`{ZB1@cygCOr+yxVA5#<lcqqQ675X@7T}Z3gHSu1FYl!ot~*W zmLiOE#5K;eq26mNqoNYucJq+T(_*F90a^tziLQM`wkb&=Qi@Wyz?>h*#`p(m`kE7( zrxRmumsC_;dqkoBQw;vs3e&!!wa1s7tUonfZoC!fNLTS@v*(u&XtWijC#1roaOyCZ zCTe8$7M!Kxth>xU|IxGI2*tv`8nM%_cc8yj_lW+`Be#Tto?Cd>qQbyya2x2&oy?^T ze{+z@q=i_k_K^;hlD21OQMOu4WPB73c%NoWx9Xnyde!O?`pv#fkWuU{n30j2NX()K z`dPkoN@kzWC4wsE-a=jd%1qryVd=uOcDOJ3A#b(~^EVV({@VpezPbZF@i=!mC%&^S zTz;!@P?rI%M@dKgUi1we`RO%)N#|%Zy2#3%r_@hha}Vu2`P8X>{<`7y8@q`|O85w<2?N!k zlp&KM4J-NS-Q>8Wk>&kIqE# z0qBqte>dpRt>Zwv@`!>)kYqj4?fuAlA6NchLebH2usk6Me;dwnCN;a-z=(_rpj+tA ztsjp?y^iG-j{+6USTdY+b1`mWmg-c{llsqZ`8Ahg8e1xYFx~{UF<(Rk;HT}6C0^9- zOkxzAtoql!I-uJAD}e1B*>ffT*Vrl$(~HG==fM!SImBww=wxzk%vloUeQyVfd@!!nsj+sjOnJr@0PrhL37@Lm#aU``KA`-%oRxJZoED(k45k7eU5u7lTyMd8Wh9n zsp-T;V1$o(>M#+~J7tN!$X}M5s4J(rk-P2tgRcG_+aF(j(Zqj&-&4mN9m%l-ig<#n zD8kHW46DRuqYK;$^vF8u7tPyaXnrC+n%#kc<+FCB>`9V|tYm5)BJOurscXm0L*~Vs z&mX@N${(;>G}}Cko9N?XNYVi%PcN9W$-dK@jL_#8+w5niGkT|;I$96SBxN41B&$Q% zi3GutwZc`txiAAXw4fr&Zf9(%s^6U1jXhmWtGsFYwkU@ z)+RePNqat3AOCYpu+W3?)1jIqEnOVRAXk)PgTx`(D@D{`k&`VuCRksl2Av$V06IPVB(F&)_x{ zj#-q$C!(?QOLwi6Xu&9yN{m4J@VD_$Khnpf;Ta_~$CS2T-y{^fkAzcuLiLby??2a2 zR|s2Ffv5?=WY+YW`g4kE*^VG^SsCS7fqSeFR|o%LI~bSAem{;IgoCuuM)M)g`~|$& zdJ+CsmqXc1=j-b%G@V4M=L0V%`_4=L@r^Sz$fceg#80@lc}o-YFb^h5Z&u}~`5pxO z0H{%pbv9F`(Mfq9e$zLW0g=7F&3!l8WJI`8QpYf-HM{k?(E|V@IEI?nC3mOkY!I&M zIw47K@@+z|>v5%x=>L(!2>CqTDhv2!z8Kb@$JQdP+lOVVRYaL(${M+|ME3vXykxm) z3|)9=eX{hfbS8F!sUPw_?Eq%(#jaz`L@-iOtL5HGe;7%LCYF`9p{N^jcN$)q>wt z0utWqi?RolN@Um6^dz+HF>Uhe7yGz?YUeb8t4>Wj!0R0M_AOvcRAU zSs*cP5P?{R&}DIwvdFH`J*CoF^Odvi<)uy!-tt%SWaw|v+2WM@Yx&5dpfCiZLuh?v z+VYFS8+2Z49tBwYT&)4u5G0fSY36o+=n*TP8Cf741LOY*LMM>_?nbC#gYhw+UfzhL z?MG=ntG}&j`Z_PaJ;pNUiSbX*ZWsI+jz(+dJcl|zsY7)&D`l;yHu-fPeJKBGG@r=u zJy1EQBKbRt?3EAD`{heCaSUzLIuRUGF1ad=m-&>=r!~b~B3pLt0mt35{MqYg!gfo} z9dXJoOhf*ynMOD|lI`?P;g3ZtbPG43lo9|I^zLeW{U6H^t5c;yG76=HEdJIToQ0B!j_Kjof(HFFhi1%hkhJGbuP3kdzTT2oC!_XW z33HJwCs6`y;c6(YJ-u7tg5|ZZIJv9u)9ee=2EdqTAQ6JdNAjCP2Pv${b5rzh{eAUQ zJ$Powwn#o>l2bWyrQ4d8u3MS5zV=F^i>yHzMgBvervU9P)qBKXj=8m*`?tDdU=HRoj&!|$B7I_4;I67N185S%El;>1~Hf$uZi#!$!U;> zIvkn)you>hXhlE4l$3y+7fw===%G{V zlN;4^Z4GXF@AE$gV^W;RgFgKyjxlJNKZB}#}laTr*d`|Mkp|# zR){W93IRH9+QpMUp}Pz;#`HG{ZJ;PH*A7pg(YIBdOVE{o8yTf`mJiV}?R z{4p5QADd5S($j{~o$7cFXcd3ukago#{&LZC4~2rYo+JsI^DE}l zQoatL^qN8SR9kdcQ79oBAKuFg5JN!I_06yPc5UKolS@e3BUG%4y%ki-rP(<h|bU-MN~D4kp(34Y5KQ(pFbZg zOamDd{6Y_XR<|whhuc>0ZwRlCL^3B$v>%Jweev^CPAJRVc=NLX-P1N3)H|o0QP`$b zc{LU#RiRz*f}KBnFx-_;&M!wYOTJ8Yq02)`7mGIQeVA$@c%hAfy|F=}(w7_Z<`T2Y zWH;uh|MKjI;yr+RGvj%CCsN!X*Yn)- zk`gz6z5_spM(u!y8)b2&oZ#^lJmqqLlw5XW5r>T37%mm(*LfpR{;rJ@3-J2Zfkp^; zJiv4~@h@4IYLu$xvK?0w*HR%>qz#$1jl-S9|I&ul_^FZ6?44)8 zGUwwoZ-nQ>m>)Kh7_|&iW+a)^VT*~Zb~<)CID80hthUi@Emc9C47tNyWpr{w@18$< zkk1CM({y3l0eN_?F> z&HG&eKY?UsR{L3#&Qm(}Z$(1NDcUvqUb>mn%c#3&|JqUvCY($LUA6v_Ux;* z2Cp1@_n*VwNR=4J@`DT%doroIlq~8KTbqOu+6~Cb*UlrkbTdsc-hnwMBg%YcMk-U)8NcVGPq$=`y*F7#B9ur>xPZoU1*t35;RC+2-%#T!O_?!!6cs zce$v_>-5=PJjZvRz9=aQid$j%Hup4C(Th6QJ(|(2G{@brKFE4Y#ZNBp0gC$1 z3&-;#4~QuFcBzF(gSBGK;q@Zp_4>#xtB7aCBi-xzT!@hhPKMh)3_>2yHRNRc#+F{S zKd_wenK>=!MAaWvVVxxi{GH8G0dBRI4(U9N^{<@*pan1M!eKrn0LAs4nfSbq$_& zzS^xK^#HX!+qkhd$ZmmCt%2aWq=15K9x2m{n}Kfz{EmgG+0#O(d$e>C zT_;w89r=L^_4cjyv>5G1Rs3tWn}id1&R5nNG-T7acBLgTYxT@?W%GbE{7PVurc?86 z9$h|jIVtT(Gu?S&fI7IzpW@cxD$4w}o)Sa*Pu##hPaLOL#Zz`Yx_SdTfCFw`5#y>k zYYiUFdsZ#l44+0VdO!A27tu`9qE<8Sy{^j9<{_|W>fidBpy(1w3!O+>I9i0`rO}#B zpzHCkmZ{S)%g6x{eG}a-%rZ2rS6XvQ{*%MzUr0;pYd)l-DhWuM&}`b2!h1woY-p{j z%EXj}c3)Oe(0ayB%j4N5zrndEjhyMGz0RsuYn`(#wGzVUBknW(T2U*M7(kff9lwl=<-5Q1hMd>^KjI*l!1y zcEBq#bjvrSOk94OQ@ej1FFP{Epj_w+5r1%Mo4sI>Dc+cH%grBBO`ks^nxJhE(}E0c~*+k3*j7#PlE zcg7xBo0^K z^xOrI(^zxe70=xn_cd30qcR%V!G|@X_~>LsIcIbKy!E{cYP?mJ>5ZE=zc`Cb1KV^c z%r?AJl3(~YFix$W=D8rX&#n$nL>vU5@FpDtFPU&^xLv2L%rJp6{f(j7ojtYe_o|1V)N3Y?^>wRemwDoK^n zPdCQpQLCv&evkbo72vb0#ro8t{%5H^+X(%IG2#L<^n0?j@h^Qm#V!Ne%(5ItLTx+r zSQh?akMLwm?m!DX5S`ggrAK7PK7Hi#6re3}WhWrz{rJCbp@;#cgAjfYtu>l!+kb-| zAIt`fnH^W({Cf6u|8!vj5^?F2-)>KdIA8nHk>pGahb1u{VMn;~EcZR1S247&yiqTN zuLPw$BQh!XiZRM(pUh6;iCh_ynIVU${Q@VC8g!Dw_D+y8Lx>{2IXC+r{zWp^K35dvy?k*#Hb?L@$U2C;h>JvhC>NCWynUY*El_T(}-ZjeoC7g2u z#0DnB4hM)4A`&WzOrit{RhGd`RtHI0a;HNCoRKcuKz% zhR!8{E+a?;0;fUV*aVf9RD{P=E^x zN)We;d*Gh3@Dl{XJ20z>r|cy1ILZ7ouVrA%;?DCOeOkS*M0B#^FWIRRIG`RNa8POO zN(=B)3l`dto?kmuv#XXIiyZugD}YD?yx8_IIt#R3>Ga53|64)+?jJRqgwj|OKdalN zhR7b}d?|*AJv&}`#HK=4kQ1)Mg68zx=EE6QkhI^gb>A-Bq03PmX>zKIIW_AEVGwBn zDPQvMw$$;Ao#pkcKIof!RRS?~1=buaDuDElyj-N@5W+r0zSMwRJ@8idRHQs)1FSon zkhc^!zyq^q`D{v(pe$vF4-e}}b}0GDw^)9sqr8e&#v-dA;VR*3d#asb;pj13ius}= z3>hHazmu5A*nNjc?L9Ze&4OOdoaLz(w|oCpHqJli;yG@Ik=V+*)ETlzgL#w%?yG

Xvjbn*0Cb+k75P=)yB3b@IT)3GCpyFr~0)E9hoxt^9AKEw&Ry~zvx9xK9DQ@#^O8wc9XXEWJ9*u z10Il}vo>`m9Rj294d7f(SJ}b1L*FsC+`z!2^pymDMFi;4QAtXVUP-wWi6a8brFg+G zFF<}=xh`;fje^WP-}H_;O0|=^{EKz)W)ko^1oY-of;&tb1eELyR5`VgnHll+EA;JG zqD**b7~==IVb4#6K%>9V!;QK6g4vPTFASJ-^*_Q}$minCWH4I(8~6kD6+&!&6+i?l zB{~DDpf2JFP$dhuqBB>q$zEHDE<6B4j=w4yDx&jXUhnb|u|SXX`)9xj+!V}N2oYWm z#NASK_;`_Y=PLle?w{lf;JLodZV|~AK0q*W{jZBLZ|e~P@~x%tfh=x%-df%lHt@b! zTHnQD-Rw?MMJhZD;4gJ)g|rZ*z@p?}Hge7XjKa0j`2zAVc-eyVfqZOl(YJ(u0Cv|x zXh7HD6qDvmbZE&xGV${BG!aPnM?docGMR0wtv%S0wC1V{Nt|hYn`8wDTKv8j?Z!lv zw=R_-c|(|kcj6BWJAymIX#w6e>IX{(-2sdRRiekKt`@dYjh00D?spgF)sD2$5C)>c z9U}s$7_pIH0C;#$O$FRWs0s>&#Z!xqUwBtjP*(=0<@AJG=C=?JfU9(~03TGql){A} zZuPwpeio$gWV0_CKQH&^>Hf2#0S3qVR4;LiVUxg-?3U}^Hcj{glJu5XPzIE@)Dj$9 z>X#Lkn?PB;19QX|FdIAukd{Nf4sf6!>Jr_`YM?MZ214u!G0A`+=P=NND1LeRMSLQa z8c_EKCIgIGrp3g zA-3frdM|VH;l;))qd!db3iArT5DK7EP+ytjsz0U!2MWvuiAlfvcg;^2f@CR01Pu8; z2B`yz39Ae`pE_6TTKBsFagx0)zZDrjc`J{8{)=n5UNozKa?7 z*xSV+|F%7R-~)plsDb`p0Kn(ofG7MB*NZk_UO<9OvBzq>Bx}LE6+j3>diK!vk<0ma z5IF!^&;^bKb=#odziH>i_fl}qS37Y-BCoJ~;|5tlQ$0Q@r2-7IYRygOb<62+2o?XA z5-Ool7jU`>N%5tpDtm_OH@}-VpF7xY-Yl%1=;u!31Nxw8L~vW!b;5qgfsSHxvoPq! zqq8{sJVWZGHU3Hcf*X^RHztXb5KjQU1#^Z_^Sf3DD&c00&svyl5CL4t5;E4_?BTsY zCZ~QdM_*7xbOpigYLv?xfc>t6ny*Raj>d@IQ(4MC_m+q322QGN|2UYFI#R`C2{9Xs zDjlf|2i2 zexkS&Z}M57c?>J;{aMXXNm?Z|XGbKPfof|g^xyKq%f6M!GxN|qVtjG{3J}wWpDuem z23lqkzu~@$AMAL05Us5dPu?kL%D}#qN!_Yw`&@D1!Sc^dem@i*s-5nx)TnkT4d&g& z+86Y~b<3r04cVa{4n%k;0&kkKMLj(4-Y;t6?`nYy33UaSo>=lnslU*A4Om8sxFSy$l%iR5!H_u$>Z)kNs+Lqy#+Z!MRQjug)4V(*t3ne>! z;-v1Ju=f4}*L#T%h>G9_$C(sBVergeCSOFgel-V*fqlSRBAdbMAMe`j!7(V2Re=jd zMFbnXas!j2#-{(rR7xmTuPa7vMbGnojt?;-jo3@JIh1FajWelrF?4AE_CkK02vD17 ztG)izOPsb3XUwt-mUR4BR91DPMQ1AWZ9*`gTr+DEbU?U+77bzd!;uD{hmpWcULi~3 z;;TLpi#Hc`%yERHRl;CQ%M+^@d`Jbn=zB_^z*Q-A%!BQdv*?9VvqAV#EvV&G>k>gq z8N9@@B5Rj;hws&Vw*trY1Xq|{oxH&b`E%h&cL(4{B|NX@ZRj15z5@ouFW360jL$hj z!;S}{d8S{HUb66M$5^ZF&jdWA?ko8n*trupj^Hl{89;BMxSNb2OE(p2>(PZKoHnS3 zK#{%zU5`Cwd-I_lAgtD>gXfB}2AB4E1mcX7L<%{}?)5>$3a!8*W@EOucmJ$2KXYp+ z=bBmfhN#rCVW9F+sn53i&a`C&w(<)dl!$(FRgKdTY%VU60h7P|n!IlhnJp{oFrSjv zs_NN-=Y!6;0RzT6iJ)Y_%3I%VXLo*LSoc1!aFmRJBB*O`l+UV>xh{HS6}(Fp1=-0U zgV8|IAqSM>m1JpbM>ghVYk>}XGj$RPQgvBo5n*DG2Upe~>2*wfd93t6ZY*(jj>$t9 z3IwK~U?>UWss!6A1F!7JGYI!}4??V0{?}2j6-j0{%Z8R;hg(4urFXw1UuDEggBK#s zUntk}9%mtyrj`-@APT^Hcv!h@w}4E+xwX$Xo1`)dO~~hQxBl(^{;4-s{j6nrLz|CP z-T2;zec4{*qlm=k?1j&7v)xFzK|a+b;d(-j{}P*eqUS@EWBmO77RaZ`PY`Uf)+|+y zY&X=N?_=6`Tjkbqru)C{&S8(<8Xu4Do%pL4y%94sCa)kdxBat~xc1Y>aA{4zD-^YX=2T0ikK(o6DB z7_tghOsFt7sp#I?`gNR6YdI#&O3D#EmC|3bW@jrhT)!SFp`xT~%qk6+dS>)|M2@^V zdU)#-#qf_^gHo+o*gjnG0pFA0#m*ed>q%$gkoC`uE}uuY%B^5a@=f7$K5?=mrGyFa z&G(1N`lE*jN|$(dTO9`7VO`#F=coM=wfheVfrn@ynk00MUmGwlW{Vci*cWa2EL@k< zJ<%nSCVB$e06m4dElae}=q}R7JMPnC$K1dIW!^8lIrLDc)zB76EYh9X?IUO5QKz^F zf}0)RGh|ua*$$K;qur+-S#~k@Y;Ju&ecRMDJ%G%Ve=yL#Zo;gjp12L8W7 zt~?y-uKmvvLo!B$Y*}XP+a#Hccxn`rltiQ~5wc~38Dq<&7;80Ury?zQ9%Cyp;oH-w zmoidgnS?RQh#^Z;vgG~tUcdi;*LD6p=Q`&;_x8EYx$h6Cgy+^Y&ql)IKx0^;>o?V| zyD;UDjCYA5$hrra-ZgBG|I2yqaQo)2`{_$^LDV1UW75p~&a}<9$cnse!Fc8w+H9n) zIN1Ov`K$ov?nVVaf*BeIotyFXR5JaVi9}f$sBCKn?gMc~jwPI(lL2@BwGPq;xF%z{a+zbarX+YN^x4RGb<7 zCm{S2af2+>K1Nqty|9dckha$ql@!-@i3+7I-$qRQ9Z6j=)X) zH$uHYBS2eUK^EDwSS*6PuLQRVx133Ion+DsL96voGpx5i>9sYe?Z$K^6KoYcrXe>~ zou`}{N!!Ugz^!#jO>QEfBV%xFxKdX`n7sAe4T;s4}|GUY!Op)^X6WuTvK{IK-rXr*j&!EHw^ z_&NdU@jD_Rp9ofMp3U(;VR%?n5TZXISL@=uUGewd_G~q^FF0whec+~%MtM^aSrzP} z;hM4QDe5b-^>N_EkA$&T4Fjv*uJ=3E>OXH8dvi0VrVQj@uzO+rdII8<%LK~W~baI*}rd<5t7&B__Y9wcc9tJ7-GHM$7^)=Ra|^q zo$i|ZwE(cR!#N=h zSE?_!I?>l$w9Ea>W_5&2o3Xc21+4S@_P$aQ-h zPlUBV!^sovr)H_)edR@{`^{ZM*xO1gejJAib}++&LjRR4ZLUF7d(GaK!tg z_i4uTwS3vO^)X%AOS)Jj@8uYJ#WWL}7nFzjSfui$m74jjf`ijx+IgR_Um!eSaUOJx zFQ+xcH1=c})ody+#2#QinBf&q+myhP{{H+^b*o5rT@Gu^oxhsEg5dVzuO?V+!y%u^ zc(njSK6xtIOQ`5HLyxYf>{b=9C_r;%T!b>}jpwHXSbOqV7t$eq2u*GBzt_!vZE$^R zp)HWt3YHVuyH$&h=Gdv8aN{xMjov77NWsH@FQ?d(SOSFB~i`R*=<$WpvWZUlig zO0!30_qWq_$Ry02VM(evv}6(MnM>*tFvJ%(a#?$zH+^c07meLX%}A0*Ks$(;9%QfT5| zhu9sry)b`W8$e`k4sD|1>tKG)#h&`QW+U$KBEp`1Ac0F!hnlx1wBvv+Dya$ft9`X2 z*zAWsujL=38|}G6ksH4zxTBRC9v9~f#%q8~_APE(@c~xDl;twj7YrbOaetc@PLAKF8rJT4JyLxl8 z*jljY_4yUQWgkH><;V+W?W)R=*n!x!1+RhE8V;TU7YEOqGv^ee#~gSe$DBqRVc1LI zcSTw?WvxIz2r~}c4mPOw?Zd={q3g;^UftX)exIdxDTE_bftG=N=Y(&reRZ1;ZE_9X zDbHOJd`}?G^zKUSHArbLK+Cdyfk$8zXVhVArZL2lJ`U9*uj`lX2y`SQWBOgLLrc}A z{f8EPmX~|sdIjG)Kh(lJ{UW2|1*vf)CRrZlIpe@y>?l*f#D9gfpYw6uVKVIU(g(!DJjH~ zeH2)I7?>xM)#A1B4$c}bl&qM zB0@2$z(nRjBy&7pE{Gs$3vfOKM1<${>Z1zBV^r%Yh2tmP+S_H+;SnsW zPkO{A3qybK4Wml258By-tbm3eQfTd>$_}dYZ-6pm5yRJtZVg6yLT+sz^K@(9P#;FE zgpyH2w^OtMA-FJVpF~2BSl6 zqMwWy0$=`!=_6JorI4knt-?c*@~iGPkVX1>_@JNwJo zTAX5VlRwa;hlz$n7m6aMgxPe8%xMv1p9=LwW)ZF5~+_l^VW(h)SJ*0X#cw(oG>t^ z3s|Q%092r~H5yGc#daD_&HvHeL0hCK$Hm^|U6!;tc&X`r@RN-M?mJsPT0p2eHWL0w zLF0R25aK3R>bSfdbx2YVY8f->#PH@*{lfzgh3M)IUw!08iZ-6k^#j)z+<)vz+c#3m ze)~qDT3DBMn!Rl`@Nq~AabOunvIV&ibu`tDiAF;a0f?arpzN~=ql%UYAC`#M!qc|~ z3ZL(h>VrHA%6Oa^3WU&%o6;2d?*6a^H_&Qw7gb4;3-Xm0mYkEM42VX3JOByg{1G9g z^l%QM!w^s3;|iYOp(Rbfk>EaD3p4<$KzsM;tl0fn%hlM*`@W1Ui)P$4zMSL<~KVG`;ubWjhtWq5Q^KX%so4%4MrzL=u+7_%tI!~r=1>eP;O;(Oq6wCZB zW?#4;ml^Q!(Y+GN_-LOY3I;Qf8oce6b36{_!qEdGfIuv8M8}P6_2sAF|S| zx$;<~@OcjzLrNsmvx*RBksx9{h({VwOH>FA+9U#K1HG}kNH`10;q-W|3l+W$_uC<~ z4LaiAAq;5z*X&^|XrjFA4Yx?pRjkzyvha_DV1h1%u;AB)uZW9USl03~;L~~nZN)vy zKP8~?y#bv6W&T!vEBh>?DmY$cWp_QMKk4%?eb7eDAqa&@8qkUpCLl| z{6LZ{Ns#{DR@2@RG%VqT#XDvCY9EulyY-Wk`bte# zsbmsU=q)|0Sp-Rfdf8zMtELhj3)@ns0TsvCPdRYAr|+1x=Ou2V2YM{oAw3f8{Twn7 zAK=3}7+^*W%TlJ@Md@~3d{`_Lhjx4S?~97|9o-GbW#XEdoM43*{YU1PMrA&w`&gD2 zNuM2z$web$cqp$Xp?6?~pCcydHrHgCzPLrseClqlUe@|hl8idgh-xc5*K<9iCT>0ZauxWk9Z@_3aNZ~uOM)ZHzE#Q*e@ z<+f*>-ZLsyY1wx1ct>!7u_|mFIjo?YFyLbjr<~8YfhIEkpHXeSDL1q))8NACC?3f33Lzdk=1_Og|H^*> D(4BF@ literal 0 HcmV?d00001 diff --git a/website/static/img/staticvfx.png b/website/static/img/staticvfx.png new file mode 100644 index 0000000000000000000000000000000000000000..41efd7f12005db1c67e168d1447eaa0596603e1b GIT binary patch literal 12912 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 Date: Wed, 22 Jun 2022 17:11:34 +0200 Subject: [PATCH 0210/1030] fix lucan logo name --- ...o_On_White-HR.png => lucan_Logo_On_White-HR.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename website/static/img/{Logo_On_White-HR.png => lucan_Logo_On_White-HR.png} (100%) diff --git a/website/static/img/Logo_On_White-HR.png b/website/static/img/lucan_Logo_On_White-HR.png similarity index 100% rename from website/static/img/Logo_On_White-HR.png rename to website/static/img/lucan_Logo_On_White-HR.png 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 0211/1030] 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 0212/1030] 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 0213/1030] 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 0214/1030] 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 0215/1030] 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 0216/1030] 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 0217/1030] 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 3e058c6e8ac79ebb9933d0ad02957b0467f3a578 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:20:00 +0200 Subject: [PATCH 0218/1030] Move IntegrateAsset --- openpype/plugins/publish/integrate.py | 832 ++++++++++++++++++++++++++ 1 file changed, 832 insertions(+) create mode 100644 openpype/plugins/publish/integrate.py diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py new file mode 100644 index 0000000000..6ad0849ff7 --- /dev/null +++ b/openpype/plugins/publish/integrate.py @@ -0,0 +1,832 @@ +import os +import logging +import sys +import copy +import clique +import six + +from bson.objectid import ObjectId +from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne +import pyblish.api + +import openpype.api +from openpype.modules import ModulesManager +from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib.file_transaction import FileTransaction +from openpype.pipeline import legacy_io + +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? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + +def get_frame_padded(frame, padding): + """Return frame number as string with `padding` amount of padded zeros""" + 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) + + +def bulk_write(writes): + """Convenience function to bulk write into active project database""" + project = legacy_io.Session["AVALON_PROJECT"] + return legacy_io._database[project].bulk_write(writes) + + +class IntegrateAsset(pyblish.api.InstancePlugin): + """Register publish in the database and transfer files to destinations. + + Steps: + 1) Register the subset and version + 2) Transfer the representation files to the destination + 3) Register the representation + + Requires: + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. + """ + + label = "Integrate Asset New" + order = pyblish.api.IntegratorOrder + families = ["workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "usdComposition", + "usdOverride", + "simpleUnrealTexture" + ] + exclude_families = ["clip", "render.farm"] + default_template_name = "publish" + + # Representation context keys that should always be written to + # the database even if not used by the destination template + db_representation_context_keys = [ + "project", "asset", "task", "subset", "version", "representation", + "family", "hierarchy", "username" + ] + + # Attributes set by settings + template_name_profiles = None + + def process(self, instance): + + # Exclude instances that also contain families from exclude families + families = set(get_instance_families(instance)) + exclude = families & set(self.exclude_families) + if exclude: + self.log.debug("Instance not integrated due to exclude " + "families found: {}".format(", ".join(exclude))) + return + + file_transactions = FileTransaction(log=self.log) + try: + self.register(instance, file_transactions) + except Exception: + # clean destination + # todo: preferably we'd also rollback *any* changes to the database + file_transactions.rollback() + self.log.critical("Error when registering", exc_info=True) + six.reraise(*sys.exc_info()) + + # Finalizing can't rollback safely so no use for moving it to + # the try, except. + file_transactions.finalize() + + def register(self, instance, file_transactions): + + instance_stagingdir = instance.data.get("stagingDir") + if not instance_stagingdir: + self.log.info(( + "{0} is missing reference to staging directory." + " Will try to get it from representation." + ).format(instance)) + + else: + self.log.debug( + "Establishing staging directory " + "@ {0}".format(instance_stagingdir) + ) + + # Ensure at least one representation is set up for registering. + repres = instance.data.get("representations") + assert repres, "Instance has no representations data" + assert isinstance(repres, (list, tuple)), ( + "Instance 'representations' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + template_name = self.get_template_name(instance) + + subset, subset_writes = self.prepare_subset(instance) + version, version_writes = self.prepare_version(instance, subset) + instance.data["versionEntity"] = version + + # Get existing representations (if any) + existing_repres_by_name = { + repres["name"].lower(): repres for repres in legacy_io.find( + { + "parent": version["_id"], + "type": "representation" + }, + # Only care about id and name of existing representations + projection={"_id": True, "name": True} + ) + } + + # Prepare all representations + prepared_representations = [] + for repre in instance.data["representations"]: + + if "delete" in repre.get("tags", []): + self.log.debug("Skipping representation marked for deletion: " + "{}".format(repre)) + continue + + # todo: reduce/simplify what is returned from this function + prepared = self.prepare_representation(repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance) + + for src, dst in prepared["transfers"]: + # todo: add support for hardlink transfers + file_transactions.add(src, dst) + + prepared_representations.append(prepared) + + if not prepared_representations: + # Even though we check `instance.data["representations"]` earlier + # this could still happen if all representations were tagged with + # "delete" and thus are skipped for integration + raise RuntimeError("No representations prepared to publish.") + + # Each instance can also have pre-defined transfers not explicitly + # part of a representation - like texture resources used by a + # .ma representation. Those destination paths are pre-defined, etc. + # todo: should we move or simplify this logic? + resource_destinations = set() + for src, dst in instance.data.get("transfers", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + resource_destinations.add(os.path.abspath(dst)) + for src, dst in instance.data.get("hardlinks", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + resource_destinations.add(os.path.abspath(dst)) + + # Bulk write to the database + # We write the subset and version to the database before the File + # 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. + bulk_write(subset_writes + version_writes) + self.log.info("Subset {subset[name]} and Version {version[name]} " + "written to database..".format(subset=subset, + version=version)) + + # Process all file transfers of all integrations now + self.log.debug("Integrating source files to destination ...") + file_transactions.process() + self.log.debug("Backed up existing files: " + "{}".format(file_transactions.backups)) + self.log.debug("Transferred files: " + "{}".format(file_transactions.transferred)) + self.log.debug("Retrieving Representation Site Sync information ...") + + # Get the accessible sites for Site Sync + manager = ModulesManager() + sync_server_module = manager.modules_by_name["sync_server"] + sites = sync_server_module.compute_resource_sync_sites( + project_name=instance.data["projectEntity"]["name"] + ) + self.log.debug("Sync Server Sites: {}".format(sites)) + + # Compute the resource file infos once (files belonging to the + # version instance instead of an individual representation) so + # we can re-use those file infos per representation + anatomy = instance.context.data["anatomy"] + resource_file_infos = self.get_files_info(resource_destinations, + sites=sites, + anatomy=anatomy) + + # 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"] + transfers = prepared["transfers"] + destinations = [dst for src, dst in transfers] + representation["files"] = self.get_files_info( + destinations, sites=sites, anatomy=anatomy + ) + + # Add the version resource file infos to each representation + representation["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 + )) + + new_repre_names_low.add(representation["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)} + } + )) + + # Write representations to the database + bulk_write(representation_writes) + + # Backwards compatibility + # todo: can we avoid the need to store this? + instance.data["published_representations"] = { + p["representation"]["_id"]: p for p in prepared_representations + } + + self.log.info("Registered {} representations" + "".format(len(prepared_representations))) + + def prepare_subset(self, instance): + asset = instance.data.get("assetEntity") + subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + + # Get existing subset if it exists + subset = legacy_io.find_one({ + "type": "subset", + "parent": asset["_id"], + "name": subset_name + }) + + # Define subset data + data = { + "families": get_instance_families(instance) + } + + subset_group = instance.data.get("subsetGroup") + if subset_group: + data["subsetGroup"] = subset_group + + bulk_writes = [] + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), + "schema": "openpype:subset-3.0", + "type": "subset", + "name": subset_name, + "data": data, + "parent": asset["_id"] + } + bulk_writes.append(InsertOne(subset)) + + 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["data"].update(data) + bulk_writes.append(UpdateOne( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + )) + + self.log.info("Prepared subset: {}".format(subset_name)) + return subset, bulk_writes + + def prepare_version(self, instance, subset): + + version_number = instance.data["version"] + + version = { + "schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": self.create_version_data(instance) + } + + existing_version = legacy_io.find_one({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }, projection={"_id": True}) + + if existing_version: + self.log.debug("Updating existing version ...") + version["_id"] = existing_version["_id"] + else: + self.log.debug("Creating new version ...") + version["_id"] = ObjectId() + + bulk_writes = [ReplaceOne( + filter={"_id": version["_id"]}, + replacement=version, + upsert=True + )] + + self.log.info("Prepared version: v{0:03d}".format(version["name"])) + + return version, bulk_writes + + def prepare_representation(self, repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance): + + # pre-flight validations + if repre["ext"].startswith("."): + raise ValueError("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"])) + + # create template data for Anatomy + template_data = copy.deepcopy(instance.data["anatomyData"]) + + # required representation keys + files = repre['files'] + template_data["representation"] = repre["name"] + template_data["ext"] = repre["ext"] + + # optionals + # retrieve additional anatomy data from representation if exists + for key, anatomy_key in { + # Representation Key: Anatomy data key + "resolutionWidth": "resolution_width", + "resolutionHeight": "resolution_height", + "fps": "fps", + "outputName": "output", + "originalBasename": "originalBasename" + }.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 + + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] + else: + # 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)) + stagingdir = instance_stagingdir + if not stagingdir: + raise ValueError("No staging directory set for representation: " + "{}".format(repre)) + + self.log.debug("Anatomy template name: {}".format(template_name)) + anatomy = instance.context.data['anatomy'] + template = os.path.normpath(anatomy.templates[template_name]["path"]) + + is_udim = bool(repre.get("udim")) + 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" + ) + + 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) + destination_padding = len(get_first_frame_padded(src_collection)) + 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") + ) + ) + + # 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 + + # To construct the destination template with anatomy we require + # 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 + + # Construct destination collection from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + 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.padding = destination_padding + assert ( + len(src_collection.indexes) == len(dst_collection.indexes) + ), "This is a bug" + + # Multiple file transfers + transfers = [] + for src_file_name, dst in zip(src_collection, dst_collection): + src = os.path.join(stagingdir, src_file_name) + transfers.append((src, dst)) + + else: + # Single file + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + + # Manage anatomy template data + template_data.pop("frame", None) + if is_udim: + template_data["udim"] = repre["udim"][0] + + # Construct destination filepath from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) + + # Single file transfer + src = os.path.join(stagingdir, fname) + transfers = [(src, dst)] + + # todo: Are we sure the assumption each representation + # ends up in the same folder is valid? + if not instance.data.get("publishDir"): + instance.data["publishDir"] = ( + anatomy_filled + [template_name] + ["folder"] + ) + + for key in self.db_representation_context_keys: + # 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] + + # Explicitly store the full list even though template data might + # have a different value because it uses just a single udim tile + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + + # Use previous representation's id if there is a name match + existing = existing_repres_by_name.get(repre["name"].lower()) + if existing: + repre_id = existing["_id"] + 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? + published_path = transfers[0][1] + repre["published_path"] = published_path # Backwards compatibility + + # todo: `repre` is not the actual `representation` entity + # 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}) + 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 + } + + # 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"] + + return { + "representation": representation, + "anatomy_data": template_data, + "transfers": transfers, + # todo: avoid the need for 'published_files' used by Integrate Hero + # backwards compatibility + "published_files": [transfer[1] for transfer in transfers] + } + + def create_version_data(self, instance): + """Create the data dictionary for the version + + Args: + instance: the current instance being published + + Returns: + dict: the required information for version["data"] + """ + + context = instance.context + + # create relative source path for DB + if "source" in instance.data: + source = instance.data["source"] + else: + source = context.data["currentFile"] + anatomy = instance.context.data["anatomy"] + source = self.get_rootless_path(anatomy, source) + self.log.debug("Source: {}".format(source)) + + version_data = { + "families": get_instance_families(instance), + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment"), + "machine": context.data.get("machine"), + "fps": instance.data.get("fps", context.data.get("fps")) + } + + # todo: preferably we wouldn't need this "if dict" etc. logic and + # instead be able to rely what the input value is if it's set. + intent_value = context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + version_data["intent"] = intent_value + + # Include optional data if present in + optionals = [ + "frameStart", "frameEnd", "step", "handles", + "handleEnd", "handleStart", "sourceHashes" + ] + for key in optionals: + if key in instance.data: + version_data[key] = instance.data[key] + + # Include instance.data[versionData] directly + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + + return version_data + + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + if profile: + return profile["template_name"] + else: + return self.default_template_name + + 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"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } + + def get_rootless_path(self, anatomy, path): + """Returns, if possible, path without absolute portion from root + (eg. 'c:\' or '/opt/..') + + This information is platform dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' + + Args: + anatomy: anatomy part from instance + path: path (absolute) + Returns: + 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 + else: + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) + return path + + def get_files_info(self, destinations, sites, anatomy): + """Prepare 'files' info portion for representations. + + Arguments: + destinations (list): List of transferred file destinations + sites (list): array of published locations + anatomy: anatomy part from instance + Returns: + 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) + file_infos.append(file_info) + return file_infos + + def prepare_file_info(self, path, anatomy, sites): + """ Prepare information for one file (asset or resource) + + Arguments: + path: destination url of published file + anatomy: anatomy part from instance + sites: array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + + 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), + "sites": sites + } From fd2d07e94c0fb34730547c396e09ddc314b56983 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:22:29 +0200 Subject: [PATCH 0219/1030] Revert integrator to latest develop --- openpype/plugins/publish/integrate_new.py | 1710 +++++++++++++-------- 1 file changed, 1088 insertions(+), 622 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a07e8a1e0f..4c14c17dae 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -1,111 +1,63 @@ import os +from os.path import getsize import logging import sys import copy import clique +import errno import six +import re +import shutil +from collections import deque, defaultdict +from datetime import datetime from bson.objectid import ObjectId -from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne +from pymongo import DeleteOne, InsertOne import pyblish.api import openpype.api -from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles -from openpype.lib.file_transaction import FileTransaction +from openpype.lib import ( + prepare_template_data, + create_hard_link, + StringTemplate, + TemplateUnsolved +) from openpype.pipeline import legacy_io +# this is needed until speedcopy for linux is fixed +if sys.platform == "win32": + from speedcopy import copyfile +else: + from shutil import copyfile + 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? - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) - - return families - - -def get_frame_padded(frame, padding): - """Return frame number as string with `padding` amount of padded zeros""" - 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) - - -def bulk_write(writes): - """Convenience function to bulk write into active project database""" - project = legacy_io.Session["AVALON_PROJECT"] - return legacy_io._database[project].bulk_write(writes) - - class IntegrateAssetNew(pyblish.api.InstancePlugin): - """Register publish in the database and transfer files to destinations. + """Resolve any dependency issues - Steps: - 1) Register the subset and version - 2) Transfer the representation files to the destination - 3) Register the representation + This plug-in resolves any paths which, if not updated might break + the published file. - Requires: - instance.data['representations'] - must be a list and each member - must be a dictionary with following data: - 'files': list of filenames for sequence, string for single file. - Only the filename is allowed, without the folder path. - 'stagingDir': "path/to/folder/with/files" - 'name': representation name (usually the same as extension) - 'ext': file extension - optional data - "frameStart" - "frameEnd" - 'fps' - "data": additional metadata for each representation. + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + + Requirements for instance to be correctly integrated + + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. """ label = "Integrate Asset New" @@ -140,6 +92,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", + "source", "assembly", "fbx", "textures", @@ -156,51 +109,157 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usd", "staticMesh", "skeletalMesh", - "usdComposition", - "usdOverride", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] - default_template_name = "publish" - - # Representation context keys that should always be written to - # the database even if not used by the destination template + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "username" + "family", "hierarchy", "task", "username" ] + default_template_name = "publish" + + # suffix to denote temporary files, use without '.' + TMP_FILE_EXT = 'tmp' + + # file_url : file_size of all published and uploaded files + integrated_file_sizes = {} # Attributes set by settings template_name_profiles = None + subset_grouping_profiles = None def process(self, instance): + for ef in self.exclude_families: + if ( + instance.data["family"] == ef or + ef in instance.data["families"]): + self.log.debug("Excluded family '{}' in '{}' or {}".format( + ef, instance.data["family"], instance.data["families"])) + return - # Exclude instances that also contain families from exclude families - families = set(get_instance_families(instance)) - exclude = families & set(self.exclude_families) - if exclude: - self.log.debug("Instance not integrated due to exclude " - "families found: {}".format(", ".join(exclude))) + # instance should be published on a farm + if instance.data.get("farm"): return - file_transactions = FileTransaction(log=self.log) + # Prepare repsentations that should be integrated + repres = instance.data.get("representations") + # Raise error if instance don't have any representations + if not repres: + raise ValueError( + "Instance {} has no files to transfer".format( + instance.data["family"] + ) + ) + + # Validate type of stored representations + if not isinstance(repres, (list, tuple)): + raise TypeError( + "Instance 'files' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + # Filter representations + filtered_repres = [] + for repre in repres: + if "delete" in repre.get("tags", []): + continue + filtered_repres.append(repre) + + # Skip instance if there are not representations to integrate + # all representations should not be integrated + if not filtered_repres: + self.log.warning(( + "Skipping, there are no representations" + " to integrate for instance {}" + ).format(instance.data["family"])) + return + + self.integrated_file_sizes = {} try: - self.register(instance, file_transactions) + self.register(instance, filtered_repres) + self.log.info("Integrated Asset in to the database ...") + self.log.info("instance.data: {}".format(instance.data)) + self.handle_destination_files(self.integrated_file_sizes, + 'finalize') except Exception: # clean destination - # todo: preferably we'd also rollback *any* changes to the database - file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) + self.handle_destination_files(self.integrated_file_sizes, 'remove') six.reraise(*sys.exc_info()) - # Finalizing can't rollback safely so no use for moving it to - # the try, except. - file_transactions.finalize() + def register(self, instance, repres): + # Required environment variables + anatomy_data = instance.data["anatomyData"] - def register(self, instance, file_transactions): + legacy_io.install() - instance_stagingdir = instance.data.get("stagingDir") - if not instance_stagingdir: + context = instance.context + + project_entity = instance.data["projectEntity"] + + context_asset_name = None + context_asset_doc = context.data.get("assetEntity") + if context_asset_doc: + context_asset_name = context_asset_doc["name"] + + asset_name = instance.data["asset"] + asset_entity = instance.data.get("assetEntity") + if not asset_entity or asset_entity["name"] != context_asset_name: + asset_entity = legacy_io.find_one({ + "type": "asset", + "name": asset_name, + "parent": project_entity["_id"] + }) + assert asset_entity, ( + "No asset found by the name \"{0}\" in project \"{1}\"" + ).format(asset_name, project_entity["name"]) + + instance.data["assetEntity"] = asset_entity + + # update anatomy data with asset specific keys + # - name should already been set + hierarchy = "" + parents = asset_entity["data"]["parents"] + if parents: + hierarchy = "/".join(parents) + anatomy_data["hierarchy"] = hierarchy + + # Make sure task name in anatomy data is same as on instance.data + asset_tasks = ( + asset_entity.get("data", {}).get("tasks") + ) or {} + task_name = instance.data.get("task") + if task_name: + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + project_task_types = project_entity["config"]["tasks"] + task_code = project_task_types.get(task_type, {}).get("short_name") + anatomy_data["task"] = { + "name": task_name, + "type": task_type, + "short": task_code + } + + elif "task" in anatomy_data: + # Just set 'task_name' variable to context task + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] + + else: + task_name = None + task_type = None + + # Fill family in anatomy data + anatomy_data["family"] = instance.data.get("family") + + stagingdir = instance.data.get("stagingDir") + if not stagingdir: self.log.info(( "{0} is missing reference to staging directory." " Will try to get it from representation." @@ -208,515 +267,718 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: self.log.debug( - "Establishing staging directory " - "@ {0}".format(instance_stagingdir) + "Establishing staging directory @ {0}".format(stagingdir) ) - # Ensure at least one representation is set up for registering. - repres = instance.data.get("representations") - assert repres, "Instance has no representations data" - assert isinstance(repres, (list, tuple)), ( - "Instance 'representations' must be a list, got: {0} {1}".format( - str(type(repres)), str(repres) - ) + subset = self.get_subset(asset_entity, instance) + instance.data["subsetEntity"] = subset + + version_number = instance.data["version"] + self.log.debug("Next version: v{}".format(version_number)) + + version_data = self.create_version_data(context, instance) + + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + + # TODO rename method from `create_version` to + # `prepare_version` or similar... + version = self.create_version( + subset=subset, + version_number=version_number, + data=version_data ) - template_name = self.get_template_name(instance) + self.log.debug("Creating version ...") - subset, subset_writes = self.prepare_subset(instance) - version, version_writes = self.prepare_version(instance, subset) + new_repre_names_low = [ + _repre["name"].lower() + for _repre in repres + ] + + existing_version = legacy_io.find_one({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }) + + if existing_version is None: + version_id = legacy_io.insert_one(version).inserted_id + else: + # Check if instance have set `append` mode which cause that + # only replicated representations are set to archive + append_repres = instance.data.get("append", False) + + # Update version data + # TODO query by _id and + legacy_io.update_many({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }, { + '$set': version + }) + version_id = existing_version['_id'] + + # Find representations of existing version and archive them + current_repres = list(legacy_io.find({ + "type": "representation", + "parent": version_id + })) + bulk_writes = [] + for repre in current_repres: + if append_repres: + # archive only duplicated representations + if repre["name"].lower() not in new_repre_names_low: + continue + # Representation must change type, + # `_id` must be stored to other key and replaced with new + # - that is because new representations should have same ID + repre_id = repre["_id"] + bulk_writes.append(DeleteOne({"_id": repre_id})) + + repre["orig_id"] = repre_id + repre["_id"] = ObjectId() + repre["type"] = "archived_representation" + bulk_writes.append(InsertOne(repre)) + + # bulk updates + if bulk_writes: + project_name = legacy_io.Session["AVALON_PROJECT"] + legacy_io.database[project_name].bulk_write( + bulk_writes + ) + + version = legacy_io.find_one({"_id": version_id}) instance.data["versionEntity"] = version - # Get existing representations (if any) - existing_repres_by_name = { - repres["name"].lower(): repres for repres in legacy_io.find( - { - "parent": version["_id"], - "type": "representation" - }, - # Only care about id and name of existing representations - projection={"_id": True, "name": True} - ) + existing_repres = list(legacy_io.find({ + "parent": version_id, + "type": "archived_representation" + })) + + instance.data['version'] = version['name'] + + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + anatomy_data["intent"] = intent_value + + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + + orig_transfers = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + else: + orig_transfers = list(instance.data['transfers']) + + family = self.main_family_from_instance(instance) + + key_values = { + "families": family, + "tasks": task_name, + "hosts": instance.context.data["hostName"], + "task_types": task_type } - - # Prepare all representations - prepared_representations = [] - for repre in instance.data["representations"]: - - if "delete" in repre.get("tags", []): - self.log.debug("Skipping representation marked for deletion: " - "{}".format(repre)) - continue - - # todo: reduce/simplify what is returned from this function - prepared = self.prepare_representation(repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance) - - for src, dst in prepared["transfers"]: - # todo: add support for hardlink transfers - file_transactions.add(src, dst) - - prepared_representations.append(prepared) - - if not prepared_representations: - # Even though we check `instance.data["representations"]` earlier - # this could still happen if all representations were tagged with - # "delete" and thus are skipped for integration - raise RuntimeError("No representations prepared to publish.") - - # Each instance can also have pre-defined transfers not explicitly - # part of a representation - like texture resources used by a - # .ma representation. Those destination paths are pre-defined, etc. - # todo: should we move or simplify this logic? - resource_destinations = set() - for src, dst in instance.data.get("transfers", []): - file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) - resource_destinations.add(os.path.abspath(dst)) - for src, dst in instance.data.get("hardlinks", []): - file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) - resource_destinations.add(os.path.abspath(dst)) - - # Bulk write to the database - # We write the subset and version to the database before the File - # 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. - bulk_write(subset_writes + version_writes) - self.log.info("Subset {subset[name]} and Version {version[name]} " - "written to database..".format(subset=subset, - version=version)) - - # Process all file transfers of all integrations now - self.log.debug("Integrating source files to destination ...") - file_transactions.process() - self.log.debug("Backed up existing files: " - "{}".format(file_transactions.backups)) - self.log.debug("Transferred files: " - "{}".format(file_transactions.transferred)) - self.log.debug("Retrieving Representation Site Sync information ...") - - # Get the accessible sites for Site Sync - manager = ModulesManager() - sync_server_module = manager.modules_by_name["sync_server"] - sites = sync_server_module.compute_resource_sync_sites( - project_name=instance.data["projectEntity"]["name"] + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log ) - self.log.debug("Sync Server Sites: {}".format(sites)) - # Compute the resource file infos once (files belonging to the - # version instance instead of an individual representation) so - # we can re-use those file infos per representation - anatomy = instance.context.data["anatomy"] - resource_file_infos = self.get_files_info(resource_destinations, - sites=sites, - anatomy=anatomy) + template_name = "publish" + if profile: + template_name = profile["template_name"] - # 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"] - transfers = prepared["transfers"] - destinations = [dst for src, dst in transfers] + published_representations = {} + for idx, repre in enumerate(repres): + published_files = [] + + # create template data for Anatomy + template_data = copy.deepcopy(anatomy_data) + if intent_value is not None: + template_data["intent"] = intent_value + + resolution_width = repre.get("resolutionWidth") + resolution_height = repre.get("resolutionHeight") + fps = instance.data.get("fps") + + if resolution_width: + template_data["resolution_width"] = resolution_width + if resolution_width: + template_data["resolution_height"] = resolution_height + if resolution_width: + template_data["fps"] = fps + + if "originalBasename" in instance.data: + template_data.update({ + "originalBasename": instance.data.get("originalBasename") + }) + + files = repre['files'] + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] + + if repre.get("outputName"): + template_data["output"] = repre['outputName'] + + template_data["representation"] = repre["name"] + + ext = repre["ext"] + if ext.startswith("."): + self.log.warning(( + "Implementaion warning: <\"{}\">" + " Representation's extension stored under \"ext\" key " + " started with dot (\"{}\")." + ).format(repre["name"], ext)) + ext = ext[1:] + repre["ext"] = ext + template_data["ext"] = ext + + self.log.info(template_name) + template = os.path.normpath( + anatomy.templates[template_name]["path"]) + + sequence_repre = isinstance(files, list) + repre_context = None + if sequence_repre: + self.log.debug( + "files: {}".format(files)) + src_collections, remainder = clique.assemble(files) + self.log.debug( + "src_tail_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] + + # Assert that each member has identical suffix + src_head = src_collection.format("{head}") + src_tail = src_collection.format("{tail}") + + # fix dst_padding + valid_files = [x for x in files if src_collection.match(x)] + padd_len = len( + valid_files[0].replace(src_head, "").replace(src_tail, "") + ) + src_padding_exp = "%0{}d".format(padd_len) + + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = repre['ext'] + if not repre.get("udim"): + template_data["frame"] = src_padding_exp % i + else: + template_data["udim"] = src_padding_exp % i + + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + if repre_context is None: + repre_context = template_filled.used_values + test_dest_files.append( + os.path.normpath(template_filled) + ) + if not repre.get("udim"): + template_data["frame"] = repre_context["frame"] + else: + template_data["udim"] = repre_context["udim"] + + self.log.debug( + "test_dest_files: {}".format(str(test_dest_files))) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + index_frame_start = None + + # TODO use frame padding from right template group + if repre.get("frameStart") is not None: + frame_start_padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) + ) + + index_frame_start = int(repre.get("frameStart")) + + # exception for slate workflow + if index_frame_start and "slate" in instance.data["families"]: + index_frame_start -= 1 + + dst_padding_exp = src_padding_exp + dst_start_frame = None + collection_start = list(src_collection.indexes)[0] + for i in src_collection.indexes: + # TODO 1.) do not count padding in each index iteration + # 2.) do not count dst_padding from src_padding before + # index_frame_start check + frame_number = i - collection_start + src_padding = src_padding_exp % i + + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + + dst_padding = src_padding_exp % frame_number + + if index_frame_start is not None: + dst_padding_exp = "%0{}d".format(frame_start_padding) + dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 + elif repre.get("udim"): + dst_padding = int(i) + + dst = "{0}{1}{2}".format( + dst_head, + dst_padding, + dst_tail + ) + + self.log.debug("destination: `{}`".format(dst)) + src = os.path.join(stagingdir, src_file_name) + + self.log.debug("source: {}".format(src)) + instance.data["transfers"].append([src, dst]) + + published_files.append(dst) + + # for adding first frame into db + if not dst_start_frame: + dst_start_frame = dst_padding + + # Store used frame value to template data + if repre.get("frame"): + template_data["frame"] = dst_start_frame + + dst = "{0}{1}{2}".format( + dst_head, + dst_start_frame, + dst_tail + ) + repre['published_path'] = dst + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + template_data.pop("frame", None) + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + + template_data["representation"] = repre['ext'] + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] + src = os.path.join(stagingdir, fname) + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) + + instance.data["transfers"].append([src, dst]) + + published_files.append(dst) + repre['published_path'] = dst + self.log.debug("__ dst: {}".format(dst)) + + if not instance.data.get("publishDir"): + instance.data["publishDir"] = ( + anatomy_filled + [template_name] + ["folder"] + ) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + + repre["publishedFiles"] = published_files + + for key in self.db_representation_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] + + # Use previous representation's id if there are any + repre_id = None + repre_name_low = repre["name"].lower() + for _repre in existing_repres: + # NOTE should we check lowered names? + if repre_name_low == _repre["name"]: + repre_id = _repre["orig_id"] + break + + # Create new id if existing representations does not match + if repre_id is None: + repre_id = ObjectId() + + data = repre.get("data") or {} + data.update({'path': dst, 'template': template}) + representation = { + "_id": repre_id, + "schema": "openpype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": repre['name'], + "data": data, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": repre_context + } + + if repre.get("outputName"): + representation["context"]["output"] = repre['outputName'] + + if sequence_repre and repre.get("frameStart") is not None: + representation['context']['frame'] = ( + dst_padding_exp % int(repre.get("frameStart")) + ) + + # any file that should be physically copied is expected in + # 'transfers' or 'hardlinks' + if instance.data.get('transfers', False) or \ + instance.data.get('hardlinks', False): + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + self.log.debug("Integrating source files to destination ...") + self.integrated_file_sizes.update(self.integrate(instance)) + self.log.debug("Integrated files {}". + format(self.integrated_file_sizes)) + + # get 'files' info for representation and all attached resources + self.log.debug("Preparing files information ...") representation["files"] = self.get_files_info( - destinations, sites=sites, anatomy=anatomy - ) + instance, + self.integrated_file_sizes) - # Add the version resource file infos to each representation - representation["files"] += resource_file_infos + self.log.debug("__ representation: {}".format(representation)) + destination_list.append(dst) + self.log.debug("__ destination_list: {}".format(destination_list)) + instance.data['destination_list'] = destination_list + representations.append(representation) + published_representations[repre_id] = { + "representation": representation, + "anatomy_data": template_data, + "published_files": published_files + } + self.log.debug("__ representations: {}".format(representations)) + # reset transfers for next representation + # instance.data['transfers'] is used as a global variable + # in current codebase + instance.data['transfers'] = list(orig_transfers) - # 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 - )) + # Remove old representations if there are any (before insertion of new) + if existing_repres: + repre_ids_to_remove = [] + for repre in existing_repres: + repre_ids_to_remove.append(repre["_id"]) + legacy_io.delete_many({"_id": {"$in": repre_ids_to_remove}}) - new_repre_names_low.add(representation["name"].lower()) + for rep in instance.data["representations"]: + self.log.debug("__ rep: {}".format(rep)) - # 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)} - } - )) + legacy_io.insert_many(representations) + instance.data["published_representations"] = ( + published_representations + ) + # self.log.debug("Representation: {}".format(representations)) + self.log.info("Registered {} items".format(len(representations))) - # Write representations to the database - bulk_write(representation_writes) + def integrate(self, instance): + """ Move the files. - # Backwards compatibility - # todo: can we avoid the need to store this? - instance.data["published_representations"] = { - p["representation"]["_id"]: p for p in prepared_representations - } + Through `instance.data["transfers"]` - self.log.info("Registered {} representations" - "".format(len(prepared_representations))) + Args: + instance: the instance to integrate + Returns: + integrated_file_sizes: dictionary of destination file url and + its size in bytes + """ + # store destination url and size for reporting and rollback + integrated_file_sizes = {} + transfers = list(instance.data.get("transfers", list())) + for src, dest in transfers: + if os.path.normpath(src) != os.path.normpath(dest): + dest = self.get_dest_temp_url(dest) + self.copy_file(src, dest) + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) - def prepare_subset(self, instance): - asset = instance.data.get("assetEntity") + # Produce hardlinked copies + # Note: hardlink can only be produced between two files on the same + # server/disk and editing one of the two will edit both files at once. + # As such it is recommended to only make hardlinks between static files + # to ensure publishes remain safe and non-edited. + hardlinks = instance.data.get("hardlinks", list()) + for src, dest in hardlinks: + dest = self.get_dest_temp_url(dest) + self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) + if not os.path.exists(dest): + self.hardlink_file(src, dest) + + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) + + return integrated_file_sizes + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + src = os.path.normpath(src) + dst = os.path.normpath(dst) + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + # copy file with speedcopy and check if size of files are simetrical + while True: + if not shutil._samefile(src, dst): + copyfile(src, dst) + else: + self.log.critical( + "files are the same {} to {}".format(src, dst) + ) + os.remove(dst) + try: + shutil.copyfile(src, dst) + self.log.debug("Copying files with shutil...") + except OSError as e: + self.log.critical("Cannot copy {} to {}".format(src, dst)) + self.log.critical(e) + six.reraise(*sys.exc_info()) + if str(getsize(src)) in str(getsize(dst)): + break + + def hardlink_file(self, src, dst): + dirname = os.path.dirname(dst) + + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + create_hard_link(src, dst) + + def get_subset(self, asset, instance): subset_name = instance.data["subset"] - self.log.debug("Subset: {}".format(subset_name)) - - # Get existing subset if it exists subset = legacy_io.find_one({ "type": "subset", "parent": asset["_id"], "name": subset_name }) - # Define subset data - data = { - "families": get_instance_families(instance) - } - - subset_group = instance.data.get("subsetGroup") - if subset_group: - data["subsetGroup"] = subset_group - - bulk_writes = [] if subset is None: - # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset = { - "_id": ObjectId(), + self.log.debug("families. %s" % instance.data.get('families')) + self.log.debug( + "families. %s" % type(instance.data.get('families'))) + + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + _id = legacy_io.insert_one({ "schema": "openpype:subset-3.0", "type": "subset", "name": subset_name, - "data": data, + "data": { + "families": families + }, "parent": asset["_id"] - } - bulk_writes.append(InsertOne(subset)) + }).inserted_id - 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["data"].update(data) - bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset["_id"]}, - {"$set": { - "data": subset["data"] - }} - )) + subset = legacy_io.find_one({"_id": _id}) - self.log.info("Prepared subset: {}".format(subset_name)) - return subset, bulk_writes + # QUESTION Why is changing of group and updating it's + # families in 'get_subset'? + self._set_subset_group(instance, subset["_id"]) - def prepare_version(self, instance, subset): + # Update families on subset. + families = [instance.data["family"]] + families.extend(instance.data.get("families", [])) + legacy_io.update_many( + {"type": "subset", "_id": ObjectId(subset["_id"])}, + {"$set": {"data.families": families}} + ) - version_number = instance.data["version"] + return subset - version = { - "schema": "openpype:version-3.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "data": self.create_version_data(instance) + def _set_subset_group(self, instance, subset_id): + """ + Mark subset as belonging to group in DB. + + Uses Settings > Global > Publish plugins > IntegrateAssetNew + + Args: + instance (dict): processed instance + subset_id (str): DB's subset _id + + """ + # Fist look into instance data + subset_group = instance.data.get("subsetGroup") + if not subset_group: + subset_group = self._get_subset_group(instance) + + if subset_group: + legacy_io.update_many({ + 'type': 'subset', + '_id': ObjectId(subset_id) + }, {'$set': {'data.subsetGroup': subset_group}}) + + def _get_subset_group(self, instance): + """Look into subset group profiles set by settings. + + Attribute 'subset_grouping_profiles' is defined by OpenPype settings. + """ + # Skip if 'subset_grouping_profiles' is empty + if not self.subset_grouping_profiles: + return None + + # QUESTION + # - is there a chance that task name is not filled in anatomy + # data? + # - should we use context task in that case? + anatomy_data = instance.data["anatomyData"] + task_name = None + task_type = None + if "task" in anatomy_data: + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] + filtering_criteria = { + "families": instance.data["family"], + "hosts": instance.context.data["hostName"], + "tasks": task_name, + "task_types": task_type } + matching_profile = filter_profiles( + self.subset_grouping_profiles, + filtering_criteria + ) + # Skip if there is not matchin profile + if not matching_profile: + return None - existing_version = legacy_io.find_one({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number - }, projection={"_id": True}) + filled_template = None + template = matching_profile["template"] + fill_pairs = ( + ("family", filtering_criteria["families"]), + ("task", filtering_criteria["tasks"]), + ("host", filtering_criteria["hosts"]), + ("subset", instance.data["subset"]), + ("renderlayer", instance.data.get("renderlayer")) + ) + fill_pairs = prepare_template_data(fill_pairs) - if existing_version: - self.log.debug("Updating existing version ...") - version["_id"] = existing_version["_id"] - else: - self.log.debug("Creating new version ...") - version["_id"] = ObjectId() - - bulk_writes = [ReplaceOne( - filter={"_id": version["_id"]}, - replacement=version, - upsert=True - )] - - self.log.info("Prepared version: v{0:03d}".format(version["name"])) - - return version, bulk_writes - - def prepare_representation(self, repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance): - - # pre-flight validations - if repre["ext"].startswith("."): - raise ValueError("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"])) - - # create template data for Anatomy - template_data = copy.deepcopy(instance.data["anatomyData"]) - - # required representation keys - files = repre['files'] - template_data["representation"] = repre["name"] - template_data["ext"] = repre["ext"] - - # optionals - # retrieve additional anatomy data from representation if exists - for key, anatomy_key in { - # Representation Key: Anatomy data key - "resolutionWidth": "resolution_width", - "resolutionHeight": "resolution_height", - "fps": "fps", - "outputName": "output", - "originalBasename": "originalBasename" - }.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 - - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - else: - # 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)) - stagingdir = instance_stagingdir - if not stagingdir: - raise ValueError("No staging directory set for representation: " - "{}".format(repre)) - - self.log.debug("Anatomy template name: {}".format(template_name)) - anatomy = instance.context.data['anatomy'] - template = os.path.normpath(anatomy.templates[template_name]["path"]) - - is_udim = bool(repre.get("udim")) - 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" + try: + filled_template = StringTemplate.format_strict_template( + template, fill_pairs ) + except (KeyError, TemplateUnsolved): + keys = [] + if fill_pairs: + keys = fill_pairs.keys() - src_collection = assemble(files) + msg = "Subset grouping failed. " \ + "Only {} are expected in Settings".format(','.join(keys)) + self.log.warning(msg) - # 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) - destination_padding = len(get_first_frame_padded(src_collection)) - if repre.get("frameStart") is not None and not is_udim: - index_frame_start = int(repre.get("frameStart")) + return filled_template - 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") - ) - ) - - # 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 - - # To construct the destination template with anatomy we require - # 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 - - # Construct destination collection from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - 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.padding = destination_padding - assert ( - len(src_collection.indexes) == len(dst_collection.indexes) - ), "This is a bug" - - # Multiple file transfers - transfers = [] - for src_file_name, dst in zip(src_collection, dst_collection): - src = os.path.join(stagingdir, src_file_name) - transfers.append((src, dst)) - - else: - # Single file - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) - - # Manage anatomy template data - template_data.pop("frame", None) - if is_udim: - template_data["udim"] = repre["udim"][0] - - # Construct destination filepath from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - dst = os.path.normpath(template_filled) - - # Single file transfer - src = os.path.join(stagingdir, fname) - transfers = [(src, dst)] - - # todo: Are we sure the assumption each representation - # ends up in the same folder is valid? - if not instance.data.get("publishDir"): - instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] - ) - - for key in self.db_representation_context_keys: - # 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] - - # Explicitly store the full list even though template data might - # have a different value because it uses just a single udim tile - if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list - - # Use previous representation's id if there is a name match - existing = existing_repres_by_name.get(repre["name"].lower()) - if existing: - repre_id = existing["_id"] - 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? - published_path = transfers[0][1] - repre["published_path"] = published_path # Backwards compatibility - - # todo: `repre` is not the actual `representation` entity - # 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}) - 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 - } - - # 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"] - - return { - "representation": representation, - "anatomy_data": template_data, - "transfers": transfers, - # todo: avoid the need for 'published_files' used by Integrate Hero - # backwards compatibility - "published_files": [transfer[1] for transfer in transfers] - } - - def create_version_data(self, instance): - """Create the data dictionary for the version + def create_version(self, subset, version_number, data=None): + """ Copy given source to destination Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + + Returns: + dict: collection of data to create a version + """ + + return {"schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context instance: the current instance being published Returns: - dict: the required information for version["data"] + dict: the required information with instance.data as key """ - context = instance.context + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families # create relative source path for DB - if "source" in instance.data: - source = instance.data["source"] - else: + source = instance.data.get("source") + if not source: source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] source = self.get_rootless_path(anatomy, source) - self.log.debug("Source: {}".format(source)) + self.log.debug("Source: {}".format(source)) version_data = { - "families": get_instance_families(instance), + "families": families, "time": context.data["time"], "author": context.data["user"], "source": source, "comment": context.data.get("comment"), "machine": context.data.get("machine"), - "fps": instance.data.get("fps", context.data.get("fps")) + "fps": context.data.get( + "fps", instance.data.get("fps") + ) } - # todo: preferably we wouldn't need this "if dict" etc. logic and - # instead be able to rely what the input value is if it's set. - intent_value = context.data.get("intent") + intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") @@ -732,58 +994,33 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if key in instance.data: version_data[key] = instance.data[key] - # Include instance.data[versionData] directly - version_data_instance = instance.data.get('versionData') - if version_data_instance: - version_data.update(version_data_instance) - return version_data - def get_template_name(self, instance): - """Return anatomy template name to use for integration""" - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - if profile: - return profile["template_name"] - else: - return self.default_template_name - - 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"] - - # Task can be optional in anatomy data - task = anatomy_data.get("task", {}) - - # Return filter criteria - return { - "families": anatomy_data["family"], - "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") - } + def main_family_from_instance(self, instance): + """Returns main family of entered instance.""" + family = instance.data.get("family") + if not family: + family = instance.data["families"][0] + return family def get_rootless_path(self, anatomy, path): - """Returns, if possible, path without absolute portion from root - (eg. 'c:\' or '/opt/..') - - This information is platform dependent and shouldn't be captured. - Example: - 'c:/projects/MyProject1/Assets/publish...' > - '{root}/MyProject1/Assets...' + """ Returns, if possible, path without absolute portion from host + (eg. 'c:\' or '/opt/..') + This information is host dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' Args: - anatomy: anatomy part from instance - path: path (absolute) + anatomy: anatomy part from instance + path: path (absolute) Returns: - path: modified path if possible, or unmodified path - + warning logged + path: modified path if possible, or unmodified path + + warning logged """ - success, rootless_path = anatomy.find_root_template_from_path(path) + success, rootless_path = ( + anatomy.find_root_template_from_path(path) + ) if success: path = rootless_path else: @@ -793,40 +1030,269 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, destinations, sites, anatomy): - """Prepare 'files' info portion for representations. + def get_files_info(self, instance, integrated_file_sizes): + """ Prepare 'files' portion for attached resources and main asset. + Combining records from 'transfers' and 'hardlinks' parts from + instance. + All attached resources should be added, currently without + Context info. Arguments: - destinations (list): List of transferred file destinations - sites (list): array of published locations - anatomy: anatomy part from instance + instance: the current instance being published + integrated_file_sizes: dictionary of destination path (absolute) + and its file size Returns: 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) - file_infos.append(file_info) - return file_infos + resources = list(instance.data.get("transfers", [])) + resources.extend(list(instance.data.get("hardlinks", []))) - def prepare_file_info(self, path, anatomy, sites): + self.log.debug("get_resource_files_info.resources:{}". + format(resources)) + + output_resources = [] + anatomy = instance.context.data["anatomy"] + for _src, dest in resources: + path = self.get_rootless_path(anatomy, dest) + dest = self.get_dest_temp_url(dest) + file_hash = openpype.api.source_hash(dest) + if self.TMP_FILE_EXT and \ + ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), + '') + + file_info = self.prepare_file_info(path, + integrated_file_sizes[dest], + file_hash, + instance=instance) + output_resources.append(file_info) + + return output_resources + + def get_dest_temp_url(self, dest): + """ Enhance destination path with TMP_FILE_EXT to denote temporary + file. + Temporary files will be renamed after successful registration + into DB and full copy to destination + + Arguments: + dest: destination url of published file (absolute) + Returns: + dest: destination path + '.TMP_FILE_EXT' + """ + if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: + dest += '.{}'.format(self.TMP_FILE_EXT) + return dest + + def prepare_file_info(self, path, size=None, file_hash=None, + sites=None, instance=None): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file - anatomy: anatomy part from instance - sites: array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] - + path: destination url of published file (rootless) + size(optional): size of file in bytes + file_hash(optional): hash of file for synchronization validation + sites(optional): array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + instance(dict, optional): to get collected settings Returns: - dict: file info dictionary + rec: dictionary with filled info """ - return { + local_site = 'studio' # default + remote_site = None + always_accesible = [] + sync_project_presets = None + + rec = { "_id": ObjectId(), - "path": self.get_rootless_path(anatomy, path), - "size": os.path.getsize(path), - "hash": openpype.api.source_hash(path), - "sites": sites + "path": path } + if size: + rec["size"] = size + + if file_hash: + rec["hash"] = file_hash + + if sites: + rec["sites"] = sites + else: + system_sync_server_presets = ( + instance.context.data["system_settings"] + ["modules"] + ["sync_server"]) + log.debug("system_sett:: {}".format(system_sync_server_presets)) + + if system_sync_server_presets["enabled"]: + sync_project_presets = ( + instance.context.data["project_settings"] + ["global"] + ["sync_server"]) + + if sync_project_presets and sync_project_presets["enabled"]: + local_site, remote_site = self._get_sites(sync_project_presets) + + always_accesible = sync_project_presets["config"]. \ + get("always_accessible_on", []) + + already_attached_sites = {} + meta = {"name": local_site, "created_dt": datetime.now()} + rec["sites"] = [meta] + already_attached_sites[meta["name"]] = meta["created_dt"] + + if sync_project_presets and sync_project_presets["enabled"]: + if remote_site and \ + remote_site not in already_attached_sites.keys(): + # add remote + meta = {"name": remote_site.strip()} + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = None + + # add alternative sites + rec, already_attached_sites = self._add_alternative_sites( + system_sync_server_presets, already_attached_sites, rec) + + # add skeleton for site where it should be always synced to + for always_on_site in set(always_accesible): + if always_on_site not in already_attached_sites.keys(): + meta = {"name": always_on_site.strip()} + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = None + + log.debug("final sites:: {}".format(rec["sites"])) + + return rec + + def _get_sites(self, sync_project_presets): + """Returns tuple (local_site, remote_site)""" + local_site_id = openpype.api.get_local_site_id() + local_site = sync_project_presets["config"]. \ + get("active_site", "studio").strip() + + if local_site == 'local': + local_site = local_site_id + + remote_site = sync_project_presets["config"].get("remote_site") + + if remote_site == 'local': + remote_site = local_site_id + + return local_site, remote_site + + def _add_alternative_sites(self, + system_sync_server_presets, + already_attached_sites, + rec): + """Loop through all configured sites and add alternatives. + + See SyncServerModule.handle_alternate_site + """ + conf_sites = system_sync_server_presets.get("sites", {}) + + alt_site_pairs = self._get_alt_site_pairs(conf_sites) + + already_attached_keys = list(already_attached_sites.keys()) + for added_site in already_attached_keys: + real_created = already_attached_sites[added_site] + for alt_site in alt_site_pairs.get(added_site, []): + if alt_site in already_attached_sites.keys(): + continue + meta = {"name": alt_site} + # alt site inherits state of 'created_dt' + if real_created: + meta["created_dt"] = real_created + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = real_created + + return rec, already_attached_sites + + def _get_alt_site_pairs(self, conf_sites): + """Returns dict of site and its alternative sites. + + If `site` has alternative site, it means that alt_site has 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) + + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() + + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue + + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) + + return alt_site_pairs + + def handle_destination_files(self, integrated_file_sizes, mode): + """ Clean destination files + Called when error happened during integrating to DB or to disk + OR called to rename uploaded files from temporary name to final to + highlight publishing in progress/broken + Used to clean unwanted files + + Arguments: + integrated_file_sizes: dictionary, file urls as keys, size as value + mode: 'remove' - clean files, + 'finalize' - rename files, + remove TMP_FILE_EXT suffix denoting temp file + """ + if integrated_file_sizes: + for file_url, _file_size in integrated_file_sizes.items(): + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + + try: + if mode == 'remove': + self.log.debug("Removing file {}".format(file_url)) + os.remove(file_url) + if mode == 'finalize': + new_name = re.sub( + r'\.{}$'.format(self.TMP_FILE_EXT), + '', + file_url + ) + + if os.path.exists(new_name): + self.log.debug( + "Overwriting file {} to {}".format( + file_url, new_name + ) + ) + shutil.copy(file_url, new_name) + os.remove(file_url) + else: + self.log.debug( + "Renaming file {} to {}".format( + file_url, new_name + ) + ) + os.rename(file_url, new_name) + except OSError: + self.log.error("Cannot {} file {}".format(mode, file_url), + exc_info=True) + six.reraise(*sys.exc_info()) From 271a829f6d441bcf26e6ddaf33510f984dc0c703 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:24:38 +0200 Subject: [PATCH 0220/1030] Remove duplicate source family --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4c14c17dae..fd3cf8882d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -92,7 +92,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", - "source", "assembly", "fbx", "textures", From 148ac26bf961aa8e44ffcd453efbdbb0f4a8df75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:28:00 +0200 Subject: [PATCH 0221/1030] Update USD families with latest develop --- 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 6ad0849ff7..6253a3ec11 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -156,8 +156,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "usd", "staticMesh", "skeletalMesh", - "usdComposition", - "usdOverride", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", "simpleUnrealTexture" ] exclude_families = ["clip", "render.farm"] From 035c4d2f93fd0a29ba8f6f1789a327878861284a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:30:07 +0200 Subject: [PATCH 0222/1030] Set up old vs. new integrator per host --- openpype/plugins/publish/integrate.py | 1 + openpype/plugins/publish/integrate_new.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6253a3ec11..d098147603 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -110,6 +110,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset New" order = pyblish.api.IntegratorOrder + hosts = ["maya"] families = ["workfile", "pointcache", "camera", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index fd3cf8882d..c9848abc14 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -62,6 +62,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): label = "Integrate Asset New" order = pyblish.api.IntegratorOrder + hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", + "hiero", "houdini", "nuke", "photoshop", "resolve", + "standalonepublisher", "traypublisher", "tvpaint", "unreal", + "webpublisher"] families = ["workfile", "pointcache", "camera", From a3757636e7705b34699adff7e1e23f7ff57284d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:31:53 +0200 Subject: [PATCH 0223/1030] Remove 'intent' context data override @iLLiCiTiT says: Intent should be a dictionary with "value" and "label", to be able tell if you want use value or label of the intent in templates. --- openpype/plugins/publish/collect_anatomy_context_data.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 8db9d0d3d7..0794adfb67 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -92,13 +92,5 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) - # todo: some code actually expects the dict itself and others doesn't - # question: what should it be? - intent = context.data.get("intent") - if intent and isinstance(intent, dict): - intent = intent.get("value") - if intent: - context_data["intent"] = intent - self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) From b4697b6e1a0cc778765d617b68d1e516ca7dcea9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 10:36:37 +0200 Subject: [PATCH 0224/1030] Refactor integrator labels --- openpype/plugins/publish/integrate.py | 2 +- openpype/plugins/publish/integrate_new.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index d098147603..5e86eb014a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -108,7 +108,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "data": additional metadata for each representation. """ - label = "Integrate Asset New" + label = "Integrate Asset" order = pyblish.api.IntegratorOrder hosts = ["maya"] families = ["workfile", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c9848abc14..baa14b285c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -60,7 +60,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": additional metadata for each representation. """ - label = "Integrate Asset New" + label = "Integrate Asset (legacy)" order = pyblish.api.IntegratorOrder hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", "hiero", "houdini", "nuke", "photoshop", "resolve", From 49fd9e6308f711ee261293081ac1c5375c669043 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Jul 2022 15:51:54 +0200 Subject: [PATCH 0225/1030] 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 0226/1030] 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 0227/1030] 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 0228/1030] 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 0229/1030] 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 0230/1030] 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 0231/1030] 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 0232/1030] 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 0233/1030] 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 0234/1030] 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 0235/1030] 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 0236/1030] 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 0237/1030] 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 0238/1030] 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 0239/1030] 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 0240/1030] 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 0241/1030] 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 0242/1030] 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 0243/1030] 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 0244/1030] 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 0245/1030] 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 0246/1030] 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 0247/1030] 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 0248/1030] 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 0249/1030] 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 0250/1030] 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 0251/1030] 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 0252/1030] 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 0253/1030] 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 0254/1030] 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 0255/1030] 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 dada2f4831045370265037a8b1c01c43f4dd2f92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:06:35 +0200 Subject: [PATCH 0256/1030] added function which extract project name based on project id --- .../modules/kitsu/utils/update_op_with_zou.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index de74b0c677..c39d1c5e36 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -33,6 +33,20 @@ def create_op_asset(gazu_entity: dict) -> dict: } +def get_kitsu_project_name(project_id: str): + """Get project name based on project id in kitsu. + + Args: + project_id (str): Id of project in Kitsu. + + Returns: + str: Project name which has project in Kitsu. + """ + + project = gazu.project.get_project(project_id) + return project["name"] + + def set_op_project(dbcon: AvalonMongoDB, project_id: str): """Set project context. @@ -40,9 +54,8 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): dbcon (AvalonMongoDB): Connection to DB project_id (str): Project zou ID """ - project = gazu.project.get_project(project_id) - project_name = project["name"] - dbcon.Session["AVALON_PROJECT"] = project_name + + dbcon.Session["AVALON_PROJECT"] = get_kitsu_project_name(project_id) def update_op_assets( From b4d11d4ae709fc65f41ec9c09cbf5e52a183677c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:07:10 +0200 Subject: [PATCH 0257/1030] use project name function to drop project collection --- openpype/modules/kitsu/utils/sync_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 577050c5af..677d269bca 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -7,6 +7,7 @@ from .credentials import validate_credentials from .update_op_with_zou import ( create_op_asset, set_op_project, + get_kitsu_project_name, write_project_to_op, update_op_assets, ) @@ -124,12 +125,11 @@ class Listener: def _delete_project(self, data): """Delete project.""" - project_doc = self.dbcon.find_one( - {"type": "project", "data.zou_id": data["project_id"]} - ) + + project_name = get_kitsu_project_name(data["project_id"]) # Delete project collection - self.dbcon.database[project_doc["name"]].drop() + self.dbcon.database[project_name].drop() # == Asset == From ee370cb7a315237608f3b60a6b6778d3fc900852 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:07:40 +0200 Subject: [PATCH 0258/1030] change project in session instead of replacing AvalonMongoDB with pymongo.Collection --- openpype/modules/kitsu/utils/sync_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 677d269bca..d93197a4bc 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -120,7 +120,7 @@ class Listener: # Write into DB if update_project: - self.dbcon = self.dbcon.database[project_name] + self.dbcon.Session["AVALON_PROJECT"] = project_name self.dbcon.bulk_write([update_project]) def _delete_project(self, data): From e2920ffdb53df5dd8d68b8800436f19edca2ff9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:08:15 +0200 Subject: [PATCH 0259/1030] use query functions in sync service --- openpype/modules/kitsu/utils/sync_service.py | 29 +++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index d93197a4bc..38c1176df9 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -2,6 +2,10 @@ import os import gazu +from openpype.client import ( + get_project, + get_assets +) from openpype.pipeline import AvalonMongoDB from .credentials import validate_credentials from .update_op_with_zou import ( @@ -150,7 +154,8 @@ class Listener: def _update_asset(self, data): """Update asset into OP DB.""" set_op_project(self.dbcon, data["project_id"]) - project_doc = self.dbcon.find_one({"type": "project"}) + project_name = self.dbcon.active_project() + project_doc = get_project(project_name) # Get gazu entity asset = gazu.asset.get_asset(data["asset_id"]) @@ -159,7 +164,7 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in self.dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[asset["project_id"]] = project_doc @@ -199,7 +204,8 @@ class Listener: def _update_episode(self, data): """Update episode into OP DB.""" set_op_project(self.dbcon, data["project_id"]) - project_doc = self.dbcon.find_one({"type": "project"}) + project_name = self.dbcon.active_project() + project_doc = get_project(project_name) # Get gazu entity episode = gazu.shot.get_episode(data["episode_id"]) @@ -208,7 +214,7 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in self.dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[episode["project_id"]] = project_doc @@ -249,7 +255,8 @@ class Listener: def _update_sequence(self, data): """Update sequence into OP DB.""" set_op_project(self.dbcon, data["project_id"]) - project_doc = self.dbcon.find_one({"type": "project"}) + project_name = self.dbcon.active_project() + project_doc = get_project(project_name) # Get gazu entity sequence = gazu.shot.get_sequence(data["sequence_id"]) @@ -258,7 +265,7 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in self.dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[sequence["project_id"]] = project_doc @@ -299,7 +306,8 @@ class Listener: def _update_shot(self, data): """Update shot into OP DB.""" set_op_project(self.dbcon, data["project_id"]) - project_doc = self.dbcon.find_one({"type": "project"}) + project_name = self.dbcon.active_project() + project_doc = get_project(project_name) # Get gazu entity shot = gazu.shot.get_shot(data["shot_id"]) @@ -308,7 +316,7 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in self.dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[shot["project_id"]] = project_doc @@ -359,10 +367,11 @@ class Listener: def _delete_task(self, data): """Delete task of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) + project_name = self.dbcon.active_project() # Find asset doc - asset_docs = [doc for doc in self.dbcon.find({"type": "asset"})] + asset_docs = list(get_assets(project_name)) for doc in asset_docs: # Match task for name, task in doc["data"]["tasks"].items(): From d393d6c8956af8365afe5309780762740ff0205c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:09:30 +0200 Subject: [PATCH 0260/1030] a little bit more complicated way how to get asset matching zou id --- openpype/modules/kitsu/utils/sync_service.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 38c1176df9..3848eda7ae 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -343,14 +343,25 @@ class Listener: """Create new task into OP DB.""" # Get project entity set_op_project(self.dbcon, data["project_id"]) + project_name = self.dbcon.active_project() # Get gazu entity task = gazu.task.get_task(data["task_id"]) # Find asset doc - asset_doc = self.dbcon.find_one( - {"type": "asset", "data.zou.id": task["entity"]["id"]} + parent_name = task["entity"]["name"] + parent_zou_id = task["entity"]["id"] + asset_docs = get_assets( + project_name, + asset_names=[parent_name], + fields=["_id", "data.zou.id", "data.tasks"] ) + asset_doc = None + for _asset_doc in asset_docs: + doc_zou_id = _asset_doc.get("data", {}).get("zou", {}).get("id") + if doc_zou_id == parent_zou_id: + asset_doc = _asset_doc + break # Update asset tasks with new one asset_tasks = asset_doc["data"].get("tasks") From 20c2cedf75436e463cc6799ab1f3bcb579116abc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:09:47 +0200 Subject: [PATCH 0261/1030] use query functions in op to zou sync --- openpype/modules/kitsu/utils/update_zou_with_op.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 81d421206f..b7bc418c98 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,6 +6,7 @@ from typing import List import gazu from pymongo import UpdateOne +from openpype.client import get_project from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials @@ -53,9 +54,7 @@ def sync_zou_from_op_project( """ # Get project doc if not provided if not project_doc: - project_doc = dbcon.database[project_name].find_one( - {"type": "project"} - ) + project_doc = get_project(project_name) # Get all entities from zou print(f"Synchronizing {project_name}...") @@ -96,7 +95,7 @@ def sync_zou_from_op_project( dbcon.Session["AVALON_PROJECT"] = project_name asset_docs = { asset_doc["_id"]: asset_doc - for asset_doc in dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) } # Create new assets From 96863fa8aed63dee0b72f6a38e532525f5b6863d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:10:37 +0200 Subject: [PATCH 0262/1030] use query functions in zou to op sync --- .../modules/kitsu/utils/update_op_with_zou.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index c39d1c5e36..86dee3ce65 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -10,6 +10,12 @@ from gazu.task import ( all_tasks_for_shot, ) +from openpype.client import ( + get_project, + get_assets, + get_asset_by_id, + get_asset_by_name +) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.lib import create_project @@ -85,9 +91,7 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = dbcon.find_one( - {"type": "asset", "_id": insert_result.inserted_id} - ) + item_doc = get_asset_by_id(project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -235,7 +239,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: UpdateOne: Update instance for the project """ project_name = project["name"] - project_doc = dbcon.database[project_name].find_one({"type": "project"}) + project_doc = get_project(project_name) if not project_doc: print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_name, dbcon=dbcon) @@ -332,19 +336,20 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): bulk_writes.append(write_project_to_op(project, dbcon)) # Try to find project document - dbcon.Session["AVALON_PROJECT"] = project["name"] - project_doc = dbcon.find_one({"type": "project"}) + project_name = project["name"] + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = get_project(project_name) # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in dbcon.find({"type": "asset"}) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[project["id"]] = project_doc # Create entities root folders - project_module_settings = get_project_settings(project["name"])["kitsu"] + project_module_settings = get_project_settings(project_name)["kitsu"] for entity_type, root in project_module_settings["entities_root"].items(): parent_folders = root.split("/") direct_parent_doc = None @@ -384,7 +389,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): zou_ids_and_asset_docs.update( { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in dbcon.find({"type": "asset"}) + for asset_doc in get_assets(projec_name) if asset_doc["data"].get("zou") } ) From 767dc3dbaba27063289c6853c306acdda8493869 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:11:26 +0200 Subject: [PATCH 0263/1030] a little bit more complicated queries using zou id --- .../modules/kitsu/utils/update_op_with_zou.py | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 86dee3ce65..bf3705447c 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -179,14 +179,32 @@ def update_op_assets( ) if visual_parent_doc_id is None: # Find root folder doc - root_folder_doc = dbcon.find_one( - { - "type": "asset", - "name": entity_parent_folders[-1], - "data.root_of": substitute_item_type, - }, - ["_id"], + root_folder_docs = get_assets( + project_name, + asset_name=[entity_parent_folders[-1]], + fields=["_id", "data.root_of"] ) + # NOTE: Not sure why it's checking for entity type? + # OP3 does not support multiple assets with same names so type + # filtering is irelevant. + # This way mimics previous implementation: + # ``` + # root_folder_doc = dbcon.find_one( + # { + # "type": "asset", + # "name": entity_parent_folders[-1], + # "data.root_of": substitute_item_type, + # }, + # ["_id"], + # ) + # ``` + root_folder_doc = None + for folder_doc in root_folder_docs: + root_of = folder_doc.get("data", {}).get("root_of") + if root_of == substitute_item_type: + root_folder_doc = folder_doc + break + if root_folder_doc: visual_parent_doc_id = root_folder_doc["_id"] @@ -354,9 +372,26 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): parent_folders = root.split("/") direct_parent_doc = None for i, folder in enumerate(parent_folders, 1): - parent_doc = dbcon.find_one( - {"type": "asset", "name": folder, "data.root_of": entity_type} + parent_doc = get_asset_by_name( + project_name, folder, fields=["_id", "data.root_of"] ) + # NOTE: Not sure why it's checking for entity type? + # OP3 does not support multiple assets with same names so type + # filtering is irelevant. + # Also all of the entities could find be queried at once using + # 'get_assets'. + # This way mimics previous implementation: + # ``` + # parent_doc = dbcon.find_one( + # {"type": "asset", "name": folder, "data.root_of": entity_type} + # ) + # ``` + if ( + parent_doc + and parent_doc.get("data", {}).get("root_of") != entity_type + ): + parent_doc = None + if not parent_doc: direct_parent_doc = dbcon.insert_one( { From 2988af04ffcd1a42a4ca40fe4a2dbe80f163ffff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 16:57:49 +0200 Subject: [PATCH 0264/1030] added missing import --- openpype/modules/kitsu/utils/update_zou_with_op.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index b7bc418c98..57d7094e95 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,7 +6,7 @@ from typing import List import gazu from pymongo import UpdateOne -from openpype.client import get_project +from openpype.client import get_project, get_assets from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials From 5e14a54d8248b7ddb3f02b778d89ca4a391f15c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Jul 2022 17:02:54 +0200 Subject: [PATCH 0265/1030] fix typo --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 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 bf3705447c..f56a131b8e 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -424,7 +424,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): zou_ids_and_asset_docs.update( { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in get_assets(projec_name) + for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou") } ) From 2e8e4a0ee9724f0345038f1b9a2d78954ecf476d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Jul 2022 16:31:03 +0100 Subject: [PATCH 0266/1030] 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 0267/1030] 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 e8f30eea9a8914ed09360d56bcd7b2a60d8367fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Jul 2022 18:30:46 +0200 Subject: [PATCH 0268/1030] Added typing notation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix David --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 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 f56a131b8e..a68d6d31c3 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -39,7 +39,7 @@ def create_op_asset(gazu_entity: dict) -> dict: } -def get_kitsu_project_name(project_id: str): +def get_kitsu_project_name(project_id: str)->str: """Get project name based on project id in kitsu. Args: From eae292edc856be2e81ff78787929835c5f7fd91c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Jul 2022 10:10:31 +0200 Subject: [PATCH 0269/1030] 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 0270/1030] 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 0271/1030] 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 0272/1030] 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 45473c5a832b4db881ca328ad89324ada93ae0e5 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 13 Jul 2022 16:24:08 +0200 Subject: [PATCH 0273/1030] add host and families settings to integrators --- .../defaults/project_settings/global.json | 171 ++++++++++++++++++ .../schemas/schema_global_publish.json | 79 ++++++++ 2 files changed, 250 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4e9b61100e..545c792d47 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,6 +171,177 @@ ] }, "IntegrateAssetNew": { + "hosts": [ + "aftereffects", + "blender", + "celaction", + "flame", + "fusion", + "harmony", + "hiero", + "houdini", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher", + "webpublisher" + ], + "families": [ + "workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", + "simpleUnrealTexture" + ], + "template_name_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template_name": "publish" + }, + { + "families": [ + "review", + "render", + "prerender" + ], + "hosts": [], + "task_types": [], + "tasks": [], + "template_name": "render" + }, + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "simpleUnrealTexture" + }, + { + "families": [ + "staticMesh", + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template_name": "maya2unreal" + } + ] + }, + "IntegrateAsset": { + "hosts": [ + "maya" + ], + "families": [ + "workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", + "simpleUnrealTexture" + ], "template_name_profiles": [ { "families": [], 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 e368916cc9..71eed2e2de 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 @@ -587,6 +587,85 @@ "label": "IntegrateAssetNew", "is_group": true, "children": [ + { + "type": "list", + "key": "hosts", + "label": "Hosts", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "list", + "key": "template_name_profiles", + "label": "Template name profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template_name", + "label": "Template name" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateAsset", + "label": "IntegrateAsset", + "is_group": true, + "children": [ + { + "type": "list", + "key": "hosts", + "label": "Hosts", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { "type": "list", "key": "template_name_profiles", From 3cb9748613ef8cf8fc9a563e8df93c11b275a7d6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Jul 2022 13:23:40 +0200 Subject: [PATCH 0274/1030] 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 0275/1030] 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 0276/1030] 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 0277/1030] 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 0278/1030] 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 a6c029d9fb1462054ca74817393beaebf32c7346 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 10:13:32 +0200 Subject: [PATCH 0279/1030] skip validation of zou id --- openpype/modules/kitsu/utils/sync_service.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 3848eda7ae..441b95a7ec 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -4,7 +4,8 @@ import gazu from openpype.client import ( get_project, - get_assets + get_assets, + get_asset_by_name ) from openpype.pipeline import AvalonMongoDB from .credentials import validate_credentials @@ -350,18 +351,8 @@ class Listener: # Find asset doc parent_name = task["entity"]["name"] - parent_zou_id = task["entity"]["id"] - asset_docs = get_assets( - project_name, - asset_names=[parent_name], - fields=["_id", "data.zou.id", "data.tasks"] - ) - asset_doc = None - for _asset_doc in asset_docs: - doc_zou_id = _asset_doc.get("data", {}).get("zou", {}).get("id") - if doc_zou_id == parent_zou_id: - asset_doc = _asset_doc - break + + asset_doc = get_asset_by_name(project_name, parent_name) # Update asset tasks with new one asset_tasks = asset_doc["data"].get("tasks") From c210c93b325aaef0aac5f90fddbe5253a4702166 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 10:15:32 +0200 Subject: [PATCH 0280/1030] changes from comments --- openpype/modules/kitsu/utils/update_op_with_zou.py | 8 ++++---- 1 file changed, 4 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 a68d6d31c3..4695a49159 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -39,14 +39,14 @@ def create_op_asset(gazu_entity: dict) -> dict: } -def get_kitsu_project_name(project_id: str)->str: +def get_kitsu_project_name(project_id: str) -> str: """Get project name based on project id in kitsu. Args: - project_id (str): Id of project in Kitsu. + project_id (str): UUID of project in Kitsu. Returns: - str: Project name which has project in Kitsu. + str: Name of Kitsu project. """ project = gazu.project.get_project(project_id) @@ -178,7 +178,7 @@ def update_op_assets( asset_doc_ids[parent_zou_id]["_id"] if parent_zou_id else None ) if visual_parent_doc_id is None: - # Find root folder doc + # Find root folder docs root_folder_docs = get_assets( project_name, asset_name=[entity_parent_folders[-1]], From 7f9cdaaa0e90b8c00dcdc91a523d09e1f8d32459 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 11:43:19 +0200 Subject: [PATCH 0281/1030] 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 0282/1030] 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 0283/1030] 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 0284/1030] 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 0285/1030] 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 b9be23496924fe4ac99e764b11a82db348ef0b3e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:19:07 +0200 Subject: [PATCH 0286/1030] removed default host used on deregister of host --- openpype/pipeline/context_tools.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index e719e46514..fd4dc6e3fd 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -240,29 +240,7 @@ def registered_host(): def deregister_host(): - _registered_host["_"] = default_host() - - -def default_host(): - """A default host, in place of anything better - - This may be considered as reference for the - interface a host must implement. It also ensures - that the system runs, even when nothing is there - to support it. - - """ - - host = types.ModuleType("defaultHost") - - def ls(): - return list() - - host.__dict__.update({ - "ls": ls - }) - - return host + _registered_host["_"] = None def debug_host(): From 636e46cfd673f13bba211024bf1b56180f17abad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:29:18 +0200 Subject: [PATCH 0287/1030] implemented functions to query project and asset documents based on current context --- openpype/pipeline/context_tools.py | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index fd4dc6e3fd..80ad939ccd 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -10,6 +10,11 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype.client import ( + get_project, + get_asset_by_id, + get_asset_by_name, +) from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import filter_pyblish_plugins @@ -282,3 +287,50 @@ def debug_host(): }) return host + + +def get_current_project(fields=None): + """Helper function to get project document based on global Session. + + This function should be called only in process where host is installed. + + Returns: + dict: Project document. + None: Project is not set. + """ + + project_name = legacy_io.active_project() + return get_project(project_name, fields=fields) + + +def get_current_project_asset(asset_name=None, asset_id=None, fields=None): + """Helper function to get asset document based on global Session. + + This function should be called only in process where host is installed. + + Asset is found out based on passed asset name or id (not both). Asset name + is not used for filtering if asset id is passed. When both asset name and + id are missing then asset name from current process is used. + + Args: + asset_name (str): Name of asset used for filter. + asset_id (Union[str, ObjectId]): Asset document id. If entered then + is used as only filter. + fields (Union[List[str], None]): Limit returned data of asset documents + to specific keys. + + Returns: + dict: Asset document. + None: Asset is not set or not exist. + """ + + project_name = legacy_io.active_project() + if asset_id: + return get_asset_by_id(project_name, asset_id, fields=fields) + + if not asset_name: + asset_name = legacy_io.Session.get("AVALON_ASSET") + # Skip if is not set even on context + if not asset_name: + return None + return get_asset_by_name(project_name, asset_name, fields=fields) From 35bd841939a78a5d963bb2972c4b14b0bace13b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:31:51 +0200 Subject: [PATCH 0288/1030] marked 'get_asset' as deprecated --- openpype/lib/avalon_context.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 76ed6cbbd3..7ed22d6de6 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -236,7 +236,7 @@ def any_outdated(): return False -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.get_current_project_asset") def get_asset(asset_name=None): """ Returning asset document from database by its name. @@ -249,15 +249,9 @@ def get_asset(asset_name=None): (MongoDB document) """ - project_name = legacy_io.active_project() - if not asset_name: - asset_name = legacy_io.Session["AVALON_ASSET"] + from openpype.pipeline.context_tools import get_current_project_asset - asset_document = get_asset_by_name(project_name, asset_name) - if not asset_document: - raise TypeError("Entity \"{}\" was not found in DB".format(asset_name)) - - return asset_document + return get_current_project_asset(asset_name=asset_name) def get_system_general_anatomy_data(system_settings=None): From de0c0effe60e38f07bc47f577f8e5fb67f61814c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 15 Jul 2022 15:39:54 +0200 Subject: [PATCH 0289/1030] reencode with concat, fix audio --- .../plugins/publish/extract_review_slate.py | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 28685c2e90..737b7db295 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -285,36 +285,32 @@ class ExtractReviewSlate(openpype.api.Extractor): audio_channels, audio_sample_rate, audio_channel_layout, + input_frame_rate ) # replace slate with silent slate for concat slate_v_path = slate_silent_path - # create ffmpeg concat text file path - conc_text_file = input_file.replace(ext, "") + "_concat" + ".txt" - conc_text_path = os.path.join( - os.path.normpath(stagingdir), conc_text_file) - _remove_at_end.append(conc_text_path) - self.log.debug("__ conc_text_path: {}".format(conc_text_path)) - - new_line = "\n" - with open(conc_text_path, "w") as conc_text_f: - conc_text_f.writelines([ - "file {}".format( - slate_v_path.replace("\\", "/")), - new_line, - "file {}".format(input_path.replace("\\", "/")) - ]) - - # concat slate and videos together + # concat slate and videos together with concat filter + # this will reencode the output + if input_audio: + fmap = [ + "[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]", + "-map", '[v]', + "-map", '[a]' + ] + else: + fmap = [ + "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", + "-map", '[v]' + ] concat_args = [ ffmpeg_path, - "-y", - "-f", "concat", - "-safe", "0", - "-i", conc_text_path, - "-c", "copy", + "-i", slate_v_path, + "-i", input_path, + "-filter_complex", ] + concat_args.extend(fmap) if offset_timecode: concat_args.extend(["-timecode", offset_timecode]) # NOTE: Added because of OP Atom demuxers @@ -328,6 +324,10 @@ class ExtractReviewSlate(openpype.api.Extractor): copy_args = ( "-metadata", "-metadata:s:v:0", + "-codec:v", + "-pixfmt", + "-b:v", + "-b:a", ) args = source_ffmpeg_cmd.split(" ") for indx, arg in enumerate(args): @@ -335,12 +335,14 @@ class ExtractReviewSlate(openpype.api.Extractor): concat_args.append(arg) # assumes arg has one parameter concat_args.append(args[indx + 1]) + concat_args.append("-y") # add final output path concat_args.append(output_path) # ffmpeg concat subprocess self.log.debug( - "Executing concat: {}".format(" ".join(concat_args)) + "Executing concat filter: {}".format + (" ".join(concat_args)) ) openpype.api.run_subprocess( concat_args, logger=self.log @@ -488,9 +490,10 @@ class ExtractReviewSlate(openpype.api.Extractor): audio_channels, audio_sample_rate, audio_channel_layout, + input_frame_rate ): # Get duration of one frame in micro seconds - items = audio_sample_rate.split("/") + items = input_frame_rate.split("/") if len(items) == 1: one_frame_duration = 1.0 / float(items[0]) elif len(items) == 2: From ad8a7c86e4b655014e6dc776c813e9966cb9e1f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:57:01 +0200 Subject: [PATCH 0290/1030] use 'get_current_project_asset' in hosts --- openpype/hosts/harmony/api/pipeline.py | 5 ++++- openpype/hosts/hiero/api/plugin.py | 3 ++- openpype/hosts/houdini/api/lib.py | 4 ++-- openpype/hosts/maya/api/lib.py | 14 ++++++++------ .../hosts/maya/plugins/create/create_render.py | 6 +++--- .../maya/plugins/publish/validate_maya_units.py | 10 +++++++--- openpype/hosts/nuke/api/lib.py | 4 ++-- .../hosts/nuke/plugins/publish/validate_script.py | 10 +++++----- openpype/hosts/resolve/api/plugin.py | 4 ++-- .../plugins/publish/collect_editorial.py | 3 ++- .../plugins/publish/collect_editorial_instances.py | 8 ++++++-- .../plugins/publish/validate_frame_ranges.py | 5 +++-- .../hosts/unreal/plugins/load/load_animation.py | 9 ++++++--- openpype/hosts/unreal/plugins/load/load_layout.py | 5 +++-- 14 files changed, 55 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 86b5753f7e..94ca134205 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.context_tools import get_current_project_asset import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -50,7 +51,9 @@ def get_asset_settings(): dict: Scene data. """ - asset_data = lib.get_asset()["data"] + + asset_doc = get_current_project_asset() + asset_data = asset_doc["data"] fps = asset_data.get("fps") frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index add416d04e..28a9dfb492 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -10,6 +10,7 @@ import qargparse import openpype.api as openpype from openpype.pipeline import LoaderPlugin, LegacyCreator +from openpype.pipeline.context_tools import get_current_project_asset from . import lib log = openpype.Logger().get_logger(__name__) @@ -484,7 +485,7 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - asset_doc = openpype.get_asset(asset_name) + asset_doc = get_current_project_asset(asset_name) log.debug("__ asset_doc: {}".format(pformat(asset_doc))) self.data["assetData"] = asset_doc["data"] diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index dd8a5ba473..c8a7f92bb9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -5,8 +5,8 @@ from contextlib import contextmanager import six from openpype.client import get_asset_by_name -from openpype.api import get_asset from openpype.pipeline import legacy_io +from openpype.pipeline.context_tools import get_current_project_asset import hou @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) def get_asset_fps(): """Return current asset fps.""" - return get_asset()["data"].get("fps") + return get_current_project_asset()["data"].get("fps") def set_id(node, unique_id, overwrite=False): diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e4221978c0..58e160cb2f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -23,7 +23,6 @@ from openpype.client import ( get_last_versions, get_representation_by_name ) -from openpype import lib from openpype.api import get_anatomy_settings from openpype.pipeline import ( legacy_io, @@ -33,6 +32,7 @@ from openpype.pipeline import ( load_container, registered_host, ) +from openpype.pipeline.context_tools import get_current_project_asset from .commands import reset_frame_range @@ -2174,7 +2174,7 @@ def reset_scene_resolution(): project_name = legacy_io.active_project() project_doc = get_project(project_name) project_data = project_doc["data"] - asset_data = lib.get_asset()["data"] + asset_data = get_current_project_asset()["data"] # Set project resolution width_key = "resolutionWidth" @@ -2208,7 +2208,8 @@ def set_context_settings(): project_name = legacy_io.active_project() project_doc = get_project(project_name) project_data = project_doc["data"] - asset_data = lib.get_asset()["data"] + asset_doc = get_current_project_asset(fields=["data.fps"]) + asset_data = asset_doc.get("data", {}) # Set project fps fps = asset_data.get("fps", project_data.get("fps", 25)) @@ -2233,7 +2234,7 @@ def validate_fps(): """ - fps = lib.get_asset()["data"]["fps"] + fps = get_current_project_asset(fields=["data.fps"])["data"]["fps"] # TODO(antirotor): This is hack as for framerates having multiple # decimal places. FTrack is ceiling decimal values on # fps to two decimal places but Maya 2019+ is reporting those fps @@ -3051,8 +3052,9 @@ def update_content_on_context_change(): This will update scene content to match new asset on context change """ scene_sets = cmds.listSets(allSets=True) - new_asset = legacy_io.Session["AVALON_ASSET"] - new_data = lib.get_asset()["data"] + asset_doc = get_current_project_asset() + new_asset = asset_doc["name"] + new_data = asset_doc["data"] for s in scene_sets: try: if cmds.getAttr("{}.id".format(s)) == "pyblish.avalon.instance": diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 93ee6679e5..de07a0b23d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -15,13 +15,13 @@ from openpype.hosts.maya.api import ( from openpype.lib import requests_get from openpype.api import ( get_system_settings, - get_project_settings, - get_asset) + get_project_settings) from openpype.modules import ModulesManager from openpype.pipeline import ( CreatorError, legacy_io, ) +from openpype.pipeline.context_tools import get_current_project_asset class CreateRender(plugin.Creator): @@ -413,7 +413,7 @@ class CreateRender(plugin.Creator): prefix, type="string") - asset = get_asset() + asset = get_current_project_asset() if renderer == "arnold": # set format to exr diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index d5a8c350d5..5f67adec76 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -2,8 +2,8 @@ import maya.cmds as cmds import pyblish.api import openpype.api -from openpype import lib import openpype.hosts.maya.api.lib as mayalib +from openpype.pipeline.context_tools import get_current_project_asset from math import ceil @@ -41,7 +41,9 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): # now flooring the value? fps = float_round(context.data.get('fps'), 2, ceil) - asset_fps = lib.get_asset()["data"]["fps"] + # TODO repace query with using 'context.data["assetEntity"]' + asset_doc = get_current_project_asset() + asset_fps = asset_doc["data"]["fps"] self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) @@ -91,5 +93,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") - asset_fps = lib.get_asset()["data"]["fps"] + # TODO repace query with using 'context.data["assetEntity"]' + asset_doc = get_current_project_asset() + asset_fps = asset_doc["data"]["fps"] mayalib.set_scene_fps(asset_fps) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0929415c00..7be7c1169c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -24,7 +24,6 @@ from openpype.api import ( BuildWorkfile, get_version_from_path, get_workdir_data, - get_asset, get_current_project_settings, ) from openpype.tools.utils import host_tools @@ -40,6 +39,7 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) +from openpype.pipeline.context_tools import get_current_project_asset from . import gizmo_menu @@ -1766,7 +1766,7 @@ class WorkfileSettings(object): kwargs.get("asset_name") or legacy_io.Session["AVALON_ASSET"] ) - self._asset_entity = get_asset(self._asset) + self._asset_entity = get_current_project_asset(self._asset) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script.py b/openpype/hosts/nuke/plugins/publish/validate_script.py index 9bda0da85e..b8d7494b9d 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script.py @@ -1,7 +1,6 @@ import pyblish.api -from openpype.client import get_project, get_asset_by_id -from openpype import lib +from openpype.client import get_project, get_asset_by_id, get_asset_by_name from openpype.pipeline import legacy_io @@ -17,10 +16,11 @@ class ValidateScript(pyblish.api.InstancePlugin): def process(self, instance): ctx_data = instance.context.data - asset_name = ctx_data["asset"] - asset = lib.get_asset(asset_name) - asset_data = asset["data"] project_name = legacy_io.active_project() + asset_name = ctx_data["asset"] + # TODO repace query with using 'instance.data["assetEntity"]' + asset = get_asset_by_name(project_name, asset_name) + asset_data = asset["data"] # These attributes will be checked attributes = [ diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 49b478fb3b..b03125d502 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -4,11 +4,11 @@ import uuid import qargparse from Qt import QtWidgets, QtCore -import openpype.api as pype from openpype.pipeline import ( LegacyCreator, LoaderPlugin, ) +from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts import resolve from . import lib @@ -375,7 +375,7 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = pype.get_asset(asset_name)["data"] + self.data["assetData"] = get_current_project_asset(asset_name)["data"] def load(self): # create project bin for the media to be imported into diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index 0a1d29ccdc..8633d4bf9d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -19,6 +19,7 @@ import os import opentimelineio as otio import pyblish.api from openpype import lib as plib +from openpype.pipeline.context_tools import get_current_project_asset class OTIO_View(pyblish.api.Action): @@ -116,7 +117,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): 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"] + kwargs["rate"] = get_current_project_asset()["data"]["fps"] instance.data["otio_timeline"] = otio.adapters.read_from_file( file_path, **kwargs) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index d0d36bb717..3237fbbe12 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -1,8 +1,12 @@ import os +from copy import deepcopy + import opentimelineio as otio import pyblish.api + from openpype import lib as plib -from copy import deepcopy +from openpype.pipeline.context_tools import get_current_project_asset + class CollectInstances(pyblish.api.InstancePlugin): """Collect instances from editorial's OTIO sequence""" @@ -48,7 +52,7 @@ class CollectInstances(pyblish.api.InstancePlugin): # get timeline otio data timeline = instance.data["otio_timeline"] - fps = plib.get_asset()["data"]["fps"] + fps = get_current_project_asset()["data"]["fps"] tracks = timeline.each_child( descended_from_type=otio.schema.Track diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index 005157af62..ff7f60354e 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -3,8 +3,8 @@ import re import pyblish.api import openpype.api -from openpype import lib from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline.context_tools import get_current_project_asset class ValidateFrameRange(pyblish.api.InstancePlugin): @@ -27,7 +27,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): for pattern in self.skip_timelines_check): self.log.info("Skipping for {} task".format(instance.data["task"])) - asset_data = lib.get_asset(instance.data["asset"])["data"] + # TODO repace query with using 'instance.data["assetEntity"]' + asset_data = get_current_project_asset(instance.data["asset"])["data"] frame_start = asset_data["frameStart"] frame_end = asset_data["frameEnd"] handle_start = asset_data["handleStart"] diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index da2830bc52..1fe0bef462 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -8,13 +8,13 @@ from unreal import EditorAssetLibrary from unreal import MovieSceneSkeletalAnimationTrack from unreal import MovieSceneSkeletalAnimationSection +from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline -from openpype.api import get_asset class AnimationFBXLoader(plugin.Loader): @@ -53,6 +53,8 @@ class AnimationFBXLoader(plugin.Loader): if not actor: return None + asset_doc = get_current_project_asset(fields=["data.fps"]) + task.set_editor_property('filename', self.fname) task.set_editor_property('destination_path', asset_dir) task.set_editor_property('destination_name', asset_name) @@ -80,7 +82,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -246,6 +248,7 @@ class AnimationFBXLoader(plugin.Loader): def update(self, container, representation): name = container["asset_name"] source_path = get_representation_path(representation) + asset_doc = get_current_project_asset(fields=["data.fps"]) destination_path = container["namespace"] task = unreal.AssetImportTask() @@ -279,7 +282,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 3f16a68ead..01d589c69b 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -20,7 +20,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) -from openpype.api import get_asset +from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -225,6 +225,7 @@ class LayoutLoader(plugin.Loader): anim_path = f"{asset_dir}/animations/{anim_file_name}" + asset_doc = get_current_project_asset() # Import animation task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() @@ -259,7 +260,7 @@ class LayoutLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( From b5d7ae0d2a38d93ba5014c9a1aec455b9ca982ce Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 15 Jul 2022 16:29:05 +0200 Subject: [PATCH 0291/1030] no need to copy codec and pixel format --- openpype/plugins/publish/extract_review_slate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 737b7db295..2edaf10e6b 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -324,8 +324,6 @@ class ExtractReviewSlate(openpype.api.Extractor): copy_args = ( "-metadata", "-metadata:s:v:0", - "-codec:v", - "-pixfmt", "-b:v", "-b:a", ) From e8b4a3389e9ac0095bdafcdd008398dc69aac38c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 16:33:47 +0200 Subject: [PATCH 0292/1030] added comment do harmony plugin --- .../hosts/harmony/plugins/publish/validate_scene_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py index 4c3a6c4465..936533abd6 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -55,6 +55,10 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + + # TODO 'get_asset_settings' could expect asset document as argument + # which is available on 'context.data["assetEntity"]' + # - the same approach can be used in 'ValidateSceneSettingsRepair' expected_settings = harmony.get_asset_settings() self.log.info("scene settings from DB:".format(expected_settings)) From 6f521242cbaa88a4bae403fa7b23c4d9faa9cd18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:01:00 +0200 Subject: [PATCH 0293/1030] implemented functions to filter containers into 4 possible categories --- openpype/pipeline/load/__init__.py | 4 + openpype/pipeline/load/utils.py | 132 +++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 6e7612d4c1..e05dde2f9c 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -24,6 +24,8 @@ from .utils import ( loaders_from_repre_context, loaders_from_representation, + + filter_containers, ) from .plugins import ( @@ -66,6 +68,8 @@ __all__ = ( "loaders_from_repre_context", "loaders_from_representation", + "filter_containers", + # plugins.py "LoaderPlugin", "SubsetLoaderPlugin", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 2c213aff6f..68850c095a 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -4,6 +4,7 @@ import copy import getpass import logging import inspect +import collections import numbers from openpype.client import ( @@ -15,6 +16,7 @@ from openpype.client import ( get_last_version_by_subset_id, get_hero_version_by_subset_id, get_version_by_name, + get_last_versions, get_representations, get_representation_by_id, get_representation_by_name, @@ -28,6 +30,11 @@ from openpype.pipeline import ( log = logging.getLogger(__name__) +ContainersFilterResult = collections.namedtuple( + "ContainersFilterResult", + ["latest", "outdated", "not_foud", "invalid"] +) + class HeroVersionType(object): def __init__(self, version): @@ -685,3 +692,128 @@ def loaders_from_representation(loaders, representation): context = get_representation_context(representation) return loaders_from_repre_context(loaders, context) + + +def filter_containers(containers, project_name): + """Filter containers and split them into 4 categories. + + Categories are 'latest', 'outdated', 'invalid' and 'not_found'. + The 'lastest' containers are from last version, 'outdated' are not, + 'invalid' are invalid containers (invalid content) and 'not_foud' has + some missing entity in database. + + Args: + containers (list[dict]): List of containers referenced into scene. + project_name (str): Name of project in which context shoud look for + versions. + + Returns: + ContainersFilterResult: Named tuple with 'latest', 'outdated', + 'invalid' and 'not_found' containers. + """ + + outdated_containers = [] + uptodate_containers = [] + not_found_containers = [] + invalid_containers = [] + output = ContainersFilterResult( + uptodate_containers, + outdated_containers, + not_found_containers, + invalid_containers + ) + # Query representation docs to get it's version ids + repre_ids = { + container["representation"] + for container in containers + if container["representation"] + } + if not repre_ids: + if containers: + invalid_containers.extend(containers) + return output + + repre_docs = get_representations( + project_name, + representation_ids=repre_ids, + fields=["_id", "parent"] + ) + # Store representations by stringified representation id + repre_docs_by_str_id = {} + repre_docs_by_version_id = collections.defaultdict(list) + for repre_doc in repre_docs: + repre_id = str(repre_doc["_id"]) + version_id = repre_doc["parent"] + repre_docs_by_str_id[repre_id] = repre_doc + repre_docs_by_version_id[version_id].append(repre_doc) + + # Query version docs to get it's subset ids + # - also query hero version to be able identify if representation + # belongs to existing version + version_docs = get_versions( + project_name, + version_ids=repre_docs_by_version_id.keys(), + hero=True, + fields=["_id", "parent", "type"] + ) + verisons_by_id = {} + versions_by_subset_id = collections.defaultdict(list) + hero_version_ids = set() + for version_doc in version_docs: + version_id = version_doc["_id"] + # Store versions by their ids + verisons_by_id[version_id] = version_doc + # There's no need to query subsets for hero versions + # - they are considered as latest? + if version_doc["type"] == "hero_version": + hero_version_ids.add(version_id) + continue + subset_id = version_doc["parent"] + versions_by_subset_id[subset_id].append(version_doc) + + last_versions = get_last_versions( + project_name, + subset_ids=versions_by_subset_id.keys(), + fields=["_id"] + ) + # Figure out which versions are outdated + outdated_version_ids = set() + for subset_id, last_version_doc in last_versions.items(): + for version_doc in versions_by_subset_id[subset_id]: + version_id = version_doc["_id"] + if version_id != last_version_doc["_id"]: + outdated_version_ids.add(version_id) + + # Based on all collected data figure out which containers are outdated + # - log out if there are missing representation or version documents + for container in containers: + container_name = container["objectName"] + repre_id = container["representation"] + if not repre_id: + invalid_containers.append(container) + continue + + repre_doc = repre_docs_by_str_id.get(repre_id) + if not repre_doc: + log.debug(( + "Container '{}' has an invalid representation." + " It is missing in the database." + ).format(container_name)) + not_found_containers.append(container) + continue + + version_id = repre_doc["parent"] + if version_id in outdated_version_ids: + outdated_containers.append(container) + + elif version_id not in verisons_by_id: + log.debug(( + "Representation on container '{}' has an invalid version." + " It is missing in the database." + ).format(container_name)) + not_found_containers.append(container) + + else: + uptodate_containers.append(container) + + return output From 1ec708ce7f5786a6cff9bbd490beff0872553d01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:01:28 +0200 Subject: [PATCH 0294/1030] added helper functions to get outdated containers or just check if there are any outdated --- openpype/pipeline/load/__init__.py | 4 ++++ openpype/pipeline/load/utils.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e05dde2f9c..e46d9f152b 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -25,6 +25,8 @@ from .utils import ( loaders_from_repre_context, loaders_from_representation, + any_outdated_containers, + get_outdated_containers, filter_containers, ) @@ -68,6 +70,8 @@ __all__ = ( "loaders_from_repre_context", "loaders_from_representation", + "any_outdated_containers", + "get_outdated_containers", "filter_containers", # plugins.py diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 68850c095a..a9aa240ff6 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -694,6 +694,35 @@ def loaders_from_representation(loaders, representation): return loaders_from_repre_context(loaders, context) +def any_outdated_containers(host=None, project_name=None): + """Check if there are any outdated containers in scene.""" + + if get_outdated_containers(host, project_name): + return True + return False + + +def get_outdated_containers(host=None, project_name=None): + """Collect outdated containers from host scene. + + Currently registered host and project in global session are used if + arguments are not passed. + + Args: + host (ModuleType): Host implementation with 'ls' function available. + project_name (str): Name of project in which context we are. + """ + + if host is None: + from openpype.pipeline import registered_host + host = registered_host() + + if project_name is None: + project_name = legacy_io.active_project() + containers = host.ls() + return filter_containers(containers, project_name).outdated + + def filter_containers(containers, project_name): """Filter containers and split them into 4 categories. From f3b628843b5f8e986d4d52483bbaf9a94a0440b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:06:35 +0200 Subject: [PATCH 0295/1030] marked 'any_outdated' in 'openpype.lib' as deprecated --- openpype/lib/avalon_context.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 76ed6cbbd3..b3113ce188 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -19,7 +19,6 @@ from openpype.client import ( get_last_versions, get_last_version_by_subset_id, get_representations, - get_representation_by_id, get_workfile_info, ) from openpype.settings import ( @@ -208,32 +207,13 @@ def is_latest(representation): return version["_id"] == last_version["_id"] -@with_pipeline_io +@deprecated("openpype.pipeline.load.any_outdated_containers") def any_outdated(): """Return whether the current scene has any outdated content""" - from openpype.pipeline import registered_host - project_name = legacy_io.active_project() - checked = set() - host = registered_host() - for container in host.ls(): - representation = container['representation'] - if representation in checked: - continue + from openpype.pipeline.load import any_outdated_containers - representation_doc = get_representation_by_id( - project_name, representation, fields=["parent"] - ) - if representation_doc and not is_latest(representation_doc): - return True - elif not representation_doc: - log.debug("Container '{objectName}' has an invalid " - "representation, it is missing in the " - "database".format(**container)) - - checked.add(representation) - - return False + return any_outdated_containers() @with_pipeline_io From 6e90984528199eb697d55ba2c8fe0df8d7cec87b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:07:22 +0200 Subject: [PATCH 0296/1030] replace usage of 'any_outdated' with 'any_outdated_containers' --- openpype/hosts/aftereffects/api/pipeline.py | 4 ++-- openpype/hosts/houdini/api/pipeline.py | 4 ++-- openpype/hosts/maya/api/pipeline.py | 4 ++-- openpype/hosts/photoshop/api/pipeline.py | 5 ++--- openpype/plugins/publish/validate_containers.py | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 0bc47665b0..c13c22ced5 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -1,5 +1,4 @@ import os -import sys from Qt import QtWidgets @@ -15,6 +14,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects from openpype.lib import register_event_callback @@ -136,7 +136,7 @@ def ls(): def check_inventory(): """Checks loaded containers if they are of highest version""" - if not lib.any_outdated(): + if not any_outdated_containers(): return # Warn about outdated containers. diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 7048accceb..b5f5459392 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -12,13 +12,13 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.houdini from openpype.hosts.houdini.api import lib from openpype.lib import ( register_event_callback, emit_event, - any_outdated, ) from .lib import get_asset_fps @@ -245,7 +245,7 @@ def on_open(): # ensure it is using correct FPS for the asset lib.validate_fps() - if any_outdated(): + if any_outdated_containers(): from openpype.widgets import popup log.warning("Scene has outdated content.") diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index d08e8d1926..f565f6a308 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -13,7 +13,6 @@ from openpype.host import HostBase, IWorkfileHost, ILoadHost import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( - any_outdated, register_event_callback, emit_event ) @@ -28,6 +27,7 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import any_outdated_containers from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib from .workio import ( @@ -470,7 +470,7 @@ def on_open(): lib.validate_fps() lib.fix_incompatible_containers() - if any_outdated(): + if any_outdated_containers(): log.warning("Scene has outdated content.") # Find maya main window diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 20a6e3169f..ee150d1808 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -1,6 +1,5 @@ import os from Qt import QtWidgets -from bson.objectid import ObjectId import pyblish.api @@ -13,8 +12,8 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, - registered_host, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.photoshop from . import lib @@ -30,7 +29,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def check_inventory(): - if not lib.any_outdated(): + if not any_outdated_containers(): return # Warn about outdated containers. diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index ce91bd3396..7732ec5ea9 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -1,5 +1,5 @@ import pyblish.api -import openpype.lib +from openpype.pipeline.load import any_outdated_containers class ShowInventory(pyblish.api.Action): @@ -24,5 +24,5 @@ class ValidateContainers(pyblish.api.ContextPlugin): actions = [ShowInventory] def process(self, context): - if openpype.lib.any_outdated(): + if any_outdated_containers(): raise ValueError("There are outdated containers in the scene.") From b0ce3e851ddc03850f1c05bc3a7eda78a7621708 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:25:53 +0200 Subject: [PATCH 0297/1030] added function to check if version is latest --- openpype/client/__init__.py | 4 ++++ openpype/client/entities.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 97e6755d09..4b8213a8ac 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -25,6 +25,8 @@ from .entities import ( get_last_version_by_subset_name, get_output_link_versions, + version_is_latest, + get_representation_by_id, get_representation_by_name, get_representations, @@ -66,6 +68,8 @@ __all__ = ( "get_last_version_by_subset_name", "get_output_link_versions", + "version_is_latest", + "get_representation_by_id", "get_representation_by_name", "get_representations", diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 9d65355d1b..468f569c7f 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -557,6 +557,42 @@ def get_version_by_name(project_name, version, subset_id, fields=None): return conn.find_one(query_filter, _prepare_fields(fields)) +def version_is_latest(project_name, version_id): + """Is version the latest from it's subset. + + Note: + Hero versions are considered as latest. + + Todo: + Maybe raise exception when version was not found? + + Args: + project_name (str):Name of project where to look for queried entities. + version_id (str|ObjectId): Version id which is checked. + + Returns: + bool: True if is latest version from subset else False. + """ + + version_id = _convert_id(version_id) + if not version_id: + return False + version_doc = get_version_by_id( + project_name, version_id, fields=["_id", "type", "parent"] + ) + # What to de when version is not found? + if not version_doc: + return False + + if version_doc["type"] == "hero_version": + return True + + last_version = get_last_version_by_subset_id( + project_name, version_doc["parent"], fields=["_id"] + ) + return last_version["_id"] == version_id + + def _get_versions( project_name, subset_ids=None, From 95eb83d8e05749a430d04c22a6b0486b983ba315 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:45:45 +0200 Subject: [PATCH 0298/1030] use 'get_outdated_containers' in harmony --- openpype/hosts/harmony/api/pipeline.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 86b5753f7e..3246f1add9 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -5,16 +5,15 @@ import logging import pyblish.api from openpype import lib -from openpype.client import get_representation_by_id from openpype.lib import register_event_callback from openpype.pipeline import ( - legacy_io, register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import get_outdated_containers import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -105,16 +104,7 @@ def check_inventory(): in Harmony. """ - project_name = legacy_io.active_project() - outdated_containers = [] - for container in ls(): - representation_id = container['representation'] - representation_doc = get_representation_by_id( - project_name, representation_id, fields=["parent"] - ) - if representation_doc and not lib.is_latest(representation_doc): - outdated_containers.append(container) - + outdated_containers = get_outdated_containers() if not outdated_containers: return From c8d18dafa1a9366ddf07f1451e8b926533fdf07a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:46:21 +0200 Subject: [PATCH 0299/1030] 'is_latest' moved to pipeline as 'is_representation_from_latest' --- .../harmony/plugins/load/load_background.py | 9 +++------ .../plugins/load/load_imagesequence.py | 4 ++-- .../harmony/plugins/load/load_template.py | 4 ++-- openpype/lib/avalon_context.py | 19 +++---------------- openpype/pipeline/context_tools.py | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/harmony/plugins/load/load_background.py b/openpype/hosts/harmony/plugins/load/load_background.py index 9c01fe3cd8..9e9fcbfa32 100644 --- a/openpype/hosts/harmony/plugins/load/load_background.py +++ b/openpype/hosts/harmony/plugins/load/load_background.py @@ -5,8 +5,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib copy_files = """function copyFile(srcFilename, dstFilename) @@ -280,9 +280,7 @@ class BackgroundLoader(load.LoaderPlugin): ) def update(self, container, representation): - path = get_representation_path(representation) - with open(path) as json_file: data = json.load(json_file) @@ -300,10 +298,9 @@ class BackgroundLoader(load.LoaderPlugin): bg_folder = os.path.dirname(path) - path = get_representation_path(representation) - print(container) + is_latest = is_representation_from_latest(representation["parent"]) for layer in sorted(layers): file_to_import = [ os.path.join(bg_folder, layer).replace("\\", "/") @@ -347,7 +344,7 @@ class BackgroundLoader(load.LoaderPlugin): } %s """ % (sig, sig) - if openpype.lib.is_latest(representation): + if is_latest: harmony.send({"function": func, "args": [node, "green"]}) else: harmony.send({"function": func, "args": [node, "red"]}) diff --git a/openpype/hosts/harmony/plugins/load/load_imagesequence.py b/openpype/hosts/harmony/plugins/load/load_imagesequence.py index 18695438d5..8d6421a6aa 100644 --- a/openpype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/openpype/hosts/harmony/plugins/load/load_imagesequence.py @@ -10,8 +10,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib class ImageSequenceLoader(load.LoaderPlugin): @@ -109,7 +109,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) # Colour node. - if openpype.lib.is_latest(representation): + if is_representation_from_latest(representation["parent"]): harmony.send( { "function": "PypeHarmony.setColor", diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index c6dc9d913b..8ddd3934f7 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -10,8 +10,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib class TemplateLoader(load.LoaderPlugin): @@ -83,7 +83,7 @@ class TemplateLoader(load.LoaderPlugin): self_name = self.__class__.__name__ update_and_replace = False - if openpype.lib.is_latest(representation): + if is_representation_from_latest(representation["parent"]): self._set_green(node) else: self._set_red(node) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b3113ce188..1108791953 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,7 +15,6 @@ from openpype.client import ( get_asset_by_name, get_subset_by_name, get_subsets, - get_version_by_id, get_last_versions, get_last_version_by_subset_id, get_representations, @@ -179,7 +178,7 @@ def with_pipeline_io(func): return wrapped -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.is_representation_from_latest") def is_latest(representation): """Return whether the representation is from latest version @@ -190,21 +189,9 @@ def is_latest(representation): bool: Whether the representation is of latest version. """ - project_name = legacy_io.active_project() - version = get_version_by_id( - project_name, - representation["parent"], - fields=["_id", "type", "parent"] - ) - if version["type"] == "hero_version": - return True + from openpype.pipeline.context_tools import is_representation_from_latest - # Get highest version under the parent - last_version = get_last_version_by_subset_id( - project_name, version["parent"], fields=["_id"] - ) - - return version["_id"] == last_version["_id"] + return is_representation_from_latest(representation) @deprecated("openpype.pipeline.load.any_outdated_containers") diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index e719e46514..e2f9df5dae 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -10,6 +10,7 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype.client import version_is_latest from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import filter_pyblish_plugins @@ -304,3 +305,17 @@ def debug_host(): }) return host + + +def is_representation_from_latest(representation): + """Return whether the representation is from latest version + + Args: + representation (dict): The representation document from the database. + + Returns: + bool: Whether the representation is of latest version. + """ + + project_name = legacy_io.active_project() + return version_is_latest(project_name, representation["parent"]) From 3cc78c2f98d3fd652dbe9d865d54df86bf6cd688 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Jul 2022 17:56:30 +0200 Subject: [PATCH 0300/1030] 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 0301/1030] 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 3aa38ae0cc7e0799c6b510ad258c8fe7e3315bfe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:11:28 +0200 Subject: [PATCH 0302/1030] use 'get_last_version_by_subset_name' instead of 'get_latest_version' --- .../plugins/publish/submit_publish_job.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 9dd1428a63..9ef80efa50 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -10,7 +10,7 @@ import clique import pyblish.api -import openpype.api +from openpype.client import get_last_version_by_subset_name from openpype.pipeline import ( get_representation_path, legacy_io, @@ -333,8 +333,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # get latest version of subset # this will stop if subset wasn't published yet - version = openpype.api.get_latest_version(instance.data.get("asset"), - instance.data.get("subset")) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + instance.data.get("subset"), + asset_name=instance.data.get("asset") + ) + # get its files based on extension subset_resources = get_resources(version, representation.get("ext")) r_col, _ = clique.assemble(subset_resources) @@ -1013,9 +1018,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): prev_start = None prev_end = None - version = openpype.api.get_latest_version(asset_name=asset, - subset_name=subset - ) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) # Set prev start / end frames for comparison if not prev_start and not prev_end: @@ -1060,7 +1068,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): based on 'publish' template """ if not version: - version = openpype.api.get_latest_version(asset, subset) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) if version: version = int(version["name"]) + 1 else: From 1a61bd03e027053f39196314f7866e59e004a4e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:12:39 +0200 Subject: [PATCH 0303/1030] marked 'get_latest_version' as deprecated --- openpype/lib/avalon_context.py | 37 +++---------------- .../tests/test_lib_restructuralization.py | 1 - 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 1108791953..be5f1117a7 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -17,6 +17,7 @@ from openpype.client import ( get_subsets, get_last_versions, get_last_version_by_subset_id, + get_last_version_by_subset_name, get_representations, get_workfile_info, ) @@ -286,7 +287,7 @@ def get_linked_assets(asset_doc): return list(get_assets(project_name, link_ids)) -@with_pipeline_io +@deprecated("openpype.client.get_last_version_by_subset_name") def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): """Retrieve latest version from `asset_name`, and `subset_name`. @@ -307,6 +308,8 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): if not project_name: if not dbcon: + from openpype.pipeline import legacy_io + log.debug("Using `legacy_io` for query.") dbcon = legacy_io # Make sure is installed @@ -314,37 +317,9 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): project_name = dbcon.active_project() - log.debug(( - "Getting latest version for Project: \"{}\" Asset: \"{}\"" - " and Subset: \"{}\"" - ).format(project_name, asset_name, subset_name)) - - # Query asset document id by asset name - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - if not asset_doc: - log.info( - "Asset \"{}\" was not found in Database.".format(asset_name) - ) - return None - - subset_doc = get_subset_by_name( - project_name, subset_name, asset_doc["_id"] + return get_last_version_by_subset_name( + project_name, subset_name, asset_name=asset_name ) - if not subset_doc: - log.info( - "Subset \"{}\" was not found in Database.".format(subset_name) - ) - return None - - version_doc = get_last_version_by_subset_id( - project_name, subset_doc["_id"] - ) - if not version_doc: - log.info( - "Subset \"{}\" does not have any version yet.".format(subset_name) - ) - return None - return version_doc def get_workfile_template_key_from_context( diff --git a/openpype/tests/test_lib_restructuralization.py b/openpype/tests/test_lib_restructuralization.py index ccccc76a08..c8952e5a1c 100644 --- a/openpype/tests/test_lib_restructuralization.py +++ b/openpype/tests/test_lib_restructuralization.py @@ -22,7 +22,6 @@ def test_backward_compatibility(printer): from openpype.lib import any_outdated from openpype.lib import get_asset from openpype.lib import get_linked_assets - from openpype.lib import get_latest_version from openpype.lib import get_ffprobe_streams from openpype.hosts.fusion.lib import switch_item From 539d4c8fa99c26d4e7b1b226dd146b60d04b1622 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:55:20 +0200 Subject: [PATCH 0304/1030] modify docstring --- 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 468f569c7f..cc22a0a835 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -568,7 +568,7 @@ def version_is_latest(project_name, version_id): Args: project_name (str):Name of project where to look for queried entities. - version_id (str|ObjectId): Version id which is checked. + version_id (Union[str, ObjectId]): Version id which is checked. Returns: bool: True if is latest version from subset else False. From fd8a801f40050995d49e3cd8d885ec2cb6951152 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 10:03:34 +0200 Subject: [PATCH 0305/1030] fix typo --- 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 cc22a0a835..81640f75e5 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -580,7 +580,7 @@ def version_is_latest(project_name, version_id): version_doc = get_version_by_id( project_name, version_id, fields=["_id", "type", "parent"] ) - # What to de when version is not found? + # What to do when version is not found? if not version_doc: return False From 846e23dbabbbc9fd64f2620cfa139ea87ca1fcd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:16:53 +0200 Subject: [PATCH 0306/1030] copied mongo.py from lib to client --- openpype/client/mongo.py | 210 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 openpype/client/mongo.py diff --git a/openpype/client/mongo.py b/openpype/client/mongo.py new file mode 100644 index 0000000000..a747250107 --- /dev/null +++ b/openpype/client/mongo.py @@ -0,0 +1,210 @@ +import os +import sys +import time +import logging +import pymongo +import certifi + +if sys.version_info[0] == 2: + from urlparse import urlparse, parse_qs +else: + from urllib.parse import urlparse, parse_qs + + +class MongoEnvNotSet(Exception): + pass + + +def _decompose_url(url): + """Decompose mongo url to basic components. + + Used for creation of MongoHandler which expect mongo url components as + separated kwargs. Components are at the end not used as we're setting + connection directly this is just a dumb components for MongoHandler + validation pass. + """ + + # Use first url from passed url + # - this is because it is possible to pass multiple urls for multiple + # replica sets which would crash on urlparse otherwise + # - please don't use comma in username of password + url = url.split(",")[0] + components = { + "scheme": None, + "host": None, + "port": None, + "username": None, + "password": None, + "auth_db": None + } + + result = urlparse(url) + if result.scheme is None: + _url = "mongodb://{}".format(url) + result = urlparse(_url) + + components["scheme"] = result.scheme + components["host"] = result.hostname + try: + components["port"] = result.port + except ValueError: + raise RuntimeError("invalid port specified") + components["username"] = result.username + components["password"] = result.password + + try: + components["auth_db"] = parse_qs(result.query)['authSource'][0] + except KeyError: + # no auth db provided, mongo will use the one we are connecting to + pass + + return components + + +def get_default_components(): + mongo_url = os.environ.get("OPENPYPE_MONGO") + if mongo_url is None: + raise MongoEnvNotSet( + "URL for Mongo logging connection is not set." + ) + return _decompose_url(mongo_url) + + +def should_add_certificate_path_to_mongo_url(mongo_url): + """Check if should add ca certificate to mongo url. + + Since 30.9.2021 cloud mongo requires newer certificates that are not + available on most of workstation. This adds path to certifi certificate + which is valid for it. To add the certificate path url must have scheme + 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. + """ + + parsed = urlparse(mongo_url) + query = parse_qs(parsed.query) + lowered_query_keys = set(key.lower() for key in query.keys()) + add_certificate = False + # Check if url 'ssl' or 'tls' are set to 'true' + for key in ("ssl", "tls"): + if key in query and "true" in query["ssl"]: + add_certificate = True + break + + # Check if url contains 'mongodb+srv' + if not add_certificate and parsed.scheme == "mongodb+srv": + add_certificate = True + + # Check if url does already contain certificate path + if add_certificate and "tlscafile" in lowered_query_keys: + add_certificate = False + + return add_certificate + + +def validate_mongo_connection(mongo_uri): + """Check if provided mongodb URL is valid. + + Args: + mongo_uri (str): URL to validate. + + Raises: + ValueError: When port in mongo uri is not valid. + pymongo.errors.InvalidURI: If passed mongo is invalid. + pymongo.errors.ServerSelectionTimeoutError: If connection timeout + passed so probably couldn't connect to mongo server. + + """ + + client = OpenPypeMongoConnection.create_connection( + mongo_uri, retry_attempts=1 + ) + client.close() + + +class OpenPypeMongoConnection: + """Singleton MongoDB connection. + + Keeps MongoDB connections by url. + """ + + mongo_clients = {} + log = logging.getLogger("OpenPypeMongoConnection") + + @staticmethod + def get_default_mongo_url(): + return os.environ["OPENPYPE_MONGO"] + + @classmethod + def get_mongo_client(cls, mongo_url=None): + if mongo_url is None: + mongo_url = cls.get_default_mongo_url() + + connection = cls.mongo_clients.get(mongo_url) + if connection: + # Naive validation of existing connection + try: + connection.server_info() + with connection.start_session(): + pass + except Exception: + connection = None + + if not connection: + cls.log.debug("Creating mongo connection to {}".format(mongo_url)) + connection = cls.create_connection(mongo_url) + cls.mongo_clients[mongo_url] = connection + + return connection + + @classmethod + def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): + parsed = urlparse(mongo_url) + # Force validation of scheme + if parsed.scheme not in ["mongodb", "mongodb+srv"]: + raise pymongo.errors.InvalidURI(( + "Invalid URI scheme:" + " URI must begin with 'mongodb://' or 'mongodb+srv://'" + )) + + if timeout is None: + timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) + + kwargs = { + "serverSelectionTimeoutMS": timeout + } + if should_add_certificate_path_to_mongo_url(mongo_url): + kwargs["ssl_ca_certs"] = certifi.where() + + mongo_client = pymongo.MongoClient(mongo_url, **kwargs) + + if retry_attempts is None: + retry_attempts = 3 + + elif not retry_attempts: + retry_attempts = 1 + + last_exc = None + valid = False + t1 = time.time() + for attempt in range(1, retry_attempts + 1): + try: + mongo_client.server_info() + with mongo_client.start_session(): + pass + valid = True + break + + except Exception as exc: + last_exc = exc + if attempt < retry_attempts: + cls.log.warning( + "Attempt {} failed. Retrying... ".format(attempt) + ) + time.sleep(1) + + if not valid: + raise last_exc + + cls.log.info("Connected to {}, delay {:.3f}s".format( + mongo_url, time.time() - t1 + )) + return mongo_client From ccbc18fd82d7a00c2ab187489ca50977da5e9b25 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:17:01 +0200 Subject: [PATCH 0307/1030] 'OpenPypeMongoConnection' is available in 'openpype.client' --- openpype/client/__init__.py | 6 ++++++ openpype/client/entities.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 97e6755d09..0bd79de140 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -1,3 +1,7 @@ +from .mongo import ( + OpenPypeMongoConnection, +) + from .entities import ( get_projects, get_project, @@ -40,6 +44,8 @@ from .entities import ( ) __all__ = ( + "OpenPypeMongoConnection", + "get_projects", "get_project", "get_whole_project", diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 9d65355d1b..1c32632915 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -12,7 +12,7 @@ import collections import six from bson.objectid import ObjectId -from openpype.lib.mongo import OpenPypeMongoConnection +from .mongo import OpenPypeMongoConnection def _get_project_database(): From 308d9e9c498642ebc0a8dfa3e2e86603a4c01ad5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:20:14 +0200 Subject: [PATCH 0308/1030] use 'OpenPypeMongoConnection' from 'openpype.client' --- openpype/hosts/maya/api/shader_definition_editor.py | 2 +- .../hosts/maya/plugins/publish/validate_model_name.py | 2 +- .../webpublisher/webserver_service/webpublish_routes.py | 2 +- .../hosts/webpublisher/webserver_service/webserver_cli.py | 3 +-- openpype/lib/local_settings.py | 2 +- openpype/lib/log.py | 7 ++++--- openpype/lib/remote_publish.py | 2 +- openpype/modules/ftrack/ftrack_server/event_server_cli.py | 8 ++++---- openpype/modules/ftrack/ftrack_server/lib.py | 2 +- openpype/modules/ftrack/scripts/sub_event_storer.py | 3 ++- .../modules/slack/plugins/publish/integrate_slack_api.py | 2 +- openpype/pipeline/mongodb.py | 4 ++-- openpype/settings/handlers.py | 2 +- 13 files changed, 21 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py index 911db48ac2..6ea5e1a127 100644 --- a/openpype/hosts/maya/api/shader_definition_editor.py +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -6,7 +6,7 @@ Shader names are stored as simple text file over GridFS in mongodb. """ import os from Qt import QtWidgets, QtCore, QtGui -from openpype.lib.mongo import OpenPypeMongoConnection +from openpype.client.mongo import OpenPypeMongoConnection from openpype import resources import gridfs diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 50acf2b8b7..02107d5732 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -10,7 +10,7 @@ from openpype.pipeline import legacy_io import openpype.hosts.maya.api.action from openpype.hosts.maya.api.shader_definition_editor import ( DEFINITION_FILENAME) -from openpype.lib.mongo import OpenPypeMongoConnection +from openpype.client.mongo import OpenPypeMongoConnection import gridfs diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 4cb3cee8e1..6444a5191d 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -10,9 +10,9 @@ from aiohttp.web_response import Response from openpype.client import ( get_projects, get_assets, + OpenPypeMongoConnection, ) from openpype.lib import ( - OpenPypeMongoConnection, PypeLogger, ) from openpype.lib.remote_publish import ( diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py index 1ed8f22b2c..6620e5d5cf 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py @@ -6,6 +6,7 @@ import requests import json import subprocess +from openpype.client import OpenPypeMongoConnection from openpype.lib import PypeLogger from .webpublish_routes import ( @@ -121,8 +122,6 @@ def run_webserver(*args, **kwargs): def reprocess_failed(upload_dir, webserver_url): # log.info("check_reprocesable_records") - from openpype.lib import OpenPypeMongoConnection - mongo_client = OpenPypeMongoConnection.get_mongo_client() database_name = os.environ["OPENPYPE_DATABASE_NAME"] dbcon = mongo_client[database_name]["webpublishes"] diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 97e99b4b5a..c6c9699240 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -34,7 +34,7 @@ from openpype.settings import ( get_system_settings ) -from .import validate_mongo_connection +from openpype.client.mongo import validate_mongo_connection _PLACEHOLDER = object() diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 2cdb7ec8e4..e0fc7b33b1 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -24,12 +24,13 @@ import traceback import threading import copy -from . import Terminal -from .mongo import ( +from openpype.client.mongo import ( MongoEnvNotSet, get_default_components, - OpenPypeMongoConnection + OpenPypeMongoConnection, ) +from . import Terminal + try: import log4mongo from log4mongo.handlers import MongoHandler diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index d7884d0200..38c6b07c5b 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -7,7 +7,7 @@ from bson.objectid import ObjectId import pyblish.util import pyblish.api -from openpype.lib.mongo import OpenPypeMongoConnection +from openpype.client.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json ERROR_STATUS = "error" diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index 90ce757242..3ef7c8270a 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -1,11 +1,9 @@ import os -import sys import signal import datetime import subprocess import socket import json -import platform import getpass import atexit import time @@ -13,12 +11,14 @@ import uuid import ftrack_api import pymongo +from openpype.client.mongo import ( + OpenPypeMongoConnection, + validate_mongo_connection, +) from openpype.lib import ( get_openpype_execute_args, - OpenPypeMongoConnection, get_openpype_version, get_build_version, - validate_mongo_connection ) from openpype_modules.ftrack import FTRACK_MODULE_DIR from openpype_modules.ftrack.lib import credentials diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index 5c6d6352d2..3da1e7c7f0 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -24,7 +24,7 @@ except ImportError: from ftrack_api._weakref import WeakMethod from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info -from openpype.lib import OpenPypeMongoConnection +from openpype.client import OpenPypeMongoConnection from openpype.api import Logger TOPIC_STATUS_SERVER = "openpype.event.server.status" diff --git a/openpype/modules/ftrack/scripts/sub_event_storer.py b/openpype/modules/ftrack/scripts/sub_event_storer.py index 946ecbff79..204cce89e8 100644 --- a/openpype/modules/ftrack/scripts/sub_event_storer.py +++ b/openpype/modules/ftrack/scripts/sub_event_storer.py @@ -6,6 +6,8 @@ import socket import pymongo import ftrack_api + +from openpype.client import OpenPypeMongoConnection from openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer from openpype_modules.ftrack.ftrack_server.lib import ( SocketSession, @@ -15,7 +17,6 @@ from openpype_modules.ftrack.ftrack_server.lib import ( ) from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info from openpype.lib import ( - OpenPypeMongoConnection, get_openpype_version, get_build_version ) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 10bde7d4c0..c3b288f0cd 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -4,8 +4,8 @@ import pyblish.api import copy from datetime import datetime +from openpype.client import OpenPypeMongoConnection from openpype.lib.plugin_tools import prepare_template_data -from openpype.lib import OpenPypeMongoConnection class IntegrateSlackAPI(pyblish.api.InstancePlugin): diff --git a/openpype/pipeline/mongodb.py b/openpype/pipeline/mongodb.py index dab5bb9e13..be2b67a5e7 100644 --- a/openpype/pipeline/mongodb.py +++ b/openpype/pipeline/mongodb.py @@ -5,6 +5,8 @@ import logging import pymongo from uuid import uuid4 +from openpype.client import OpenPypeMongoConnection + from . import schema @@ -156,8 +158,6 @@ class AvalonMongoDB: @property def mongo_client(self): - from openpype.lib import OpenPypeMongoConnection - return OpenPypeMongoConnection.get_mongo_client() @property diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index c99fc6080b..2bcc2e06dd 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -7,6 +7,7 @@ from abc import ABCMeta, abstractmethod import six import openpype.version +from openpype.client.mongo import OpenPypeMongoConnection from .constants import ( GLOBAL_SETTINGS_KEY, @@ -337,7 +338,6 @@ class MongoSettingsHandler(SettingsHandler): def __init__(self): # Get mongo connection - from openpype.lib import OpenPypeMongoConnection from openpype.pipeline import AvalonMongoDB settings_collection = OpenPypeMongoConnection.get_mongo_client() From dc6b02c234c9862dbcf60139e7f5463b919d3e20 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:20:49 +0200 Subject: [PATCH 0309/1030] mongo.py in openpype.lib is reusing functionality from openpype.client for backwards compatibility --- openpype/lib/mongo.py | 211 ++++-------------------------------------- 1 file changed, 18 insertions(+), 193 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index c08e76c75c..80487f317d 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -1,206 +1,31 @@ -import os -import sys -import time -import logging -import pymongo -import certifi - -if sys.version_info[0] == 2: - from urlparse import urlparse, parse_qs -else: - from urllib.parse import urlparse, parse_qs - - -class MongoEnvNotSet(Exception): - pass - - -def _decompose_url(url): - """Decompose mongo url to basic components. - - Used for creation of MongoHandler which expect mongo url components as - separated kwargs. Components are at the end not used as we're setting - connection directly this is just a dumb components for MongoHandler - validation pass. - """ - # Use first url from passed url - # - this is because it is possible to pass multiple urls for multiple - # replica sets which would crash on urlparse otherwise - # - please don't use comma in username of password - url = url.split(",")[0] - components = { - "scheme": None, - "host": None, - "port": None, - "username": None, - "password": None, - "auth_db": None - } - - result = urlparse(url) - if result.scheme is None: - _url = "mongodb://{}".format(url) - result = urlparse(_url) - - components["scheme"] = result.scheme - components["host"] = result.hostname - try: - components["port"] = result.port - except ValueError: - raise RuntimeError("invalid port specified") - components["username"] = result.username - components["password"] = result.password - - try: - components["auth_db"] = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - - return components +from openpype.client.mongo import ( + MongoEnvNotSet, + OpenPypeMongoConnection, +) def get_default_components(): - mongo_url = os.environ.get("OPENPYPE_MONGO") - if mongo_url is None: - raise MongoEnvNotSet( - "URL for Mongo logging connection is not set." - ) - return _decompose_url(mongo_url) + from openpype.client.mongo import get_default_components + + return get_default_components() def should_add_certificate_path_to_mongo_url(mongo_url): - """Check if should add ca certificate to mongo url. + from openpype.client.mongo import should_add_certificate_path_to_mongo_url - Since 30.9.2021 cloud mongo requires newer certificates that are not - available on most of workstation. This adds path to certifi certificate - which is valid for it. To add the certificate path url must have scheme - 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. - """ - parsed = urlparse(mongo_url) - query = parse_qs(parsed.query) - lowered_query_keys = set(key.lower() for key in query.keys()) - add_certificate = False - # Check if url 'ssl' or 'tls' are set to 'true' - for key in ("ssl", "tls"): - if key in query and "true" in query["ssl"]: - add_certificate = True - break - - # Check if url contains 'mongodb+srv' - if not add_certificate and parsed.scheme == "mongodb+srv": - add_certificate = True - - # Check if url does already contain certificate path - if add_certificate and "tlscafile" in lowered_query_keys: - add_certificate = False - - return add_certificate + return should_add_certificate_path_to_mongo_url(mongo_url) def validate_mongo_connection(mongo_uri): - """Check if provided mongodb URL is valid. + from openpype.client.mongo import validate_mongo_connection - Args: - mongo_uri (str): URL to validate. - - Raises: - ValueError: When port in mongo uri is not valid. - pymongo.errors.InvalidURI: If passed mongo is invalid. - pymongo.errors.ServerSelectionTimeoutError: If connection timeout - passed so probably couldn't connect to mongo server. - - """ - client = OpenPypeMongoConnection.create_connection( - mongo_uri, retry_attempts=1 - ) - client.close() + return validate_mongo_connection(mongo_uri) -class OpenPypeMongoConnection: - """Singleton MongoDB connection. - - Keeps MongoDB connections by url. - """ - mongo_clients = {} - log = logging.getLogger("OpenPypeMongoConnection") - - @staticmethod - def get_default_mongo_url(): - return os.environ["OPENPYPE_MONGO"] - - @classmethod - def get_mongo_client(cls, mongo_url=None): - if mongo_url is None: - mongo_url = cls.get_default_mongo_url() - - connection = cls.mongo_clients.get(mongo_url) - if connection: - # Naive validation of existing connection - try: - connection.server_info() - with connection.start_session(): - pass - except Exception: - connection = None - - if not connection: - cls.log.debug("Creating mongo connection to {}".format(mongo_url)) - connection = cls.create_connection(mongo_url) - cls.mongo_clients[mongo_url] = connection - - return connection - - @classmethod - def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): - parsed = urlparse(mongo_url) - # Force validation of scheme - if parsed.scheme not in ["mongodb", "mongodb+srv"]: - raise pymongo.errors.InvalidURI(( - "Invalid URI scheme:" - " URI must begin with 'mongodb://' or 'mongodb+srv://'" - )) - - if timeout is None: - timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) - - kwargs = { - "serverSelectionTimeoutMS": timeout - } - if should_add_certificate_path_to_mongo_url(mongo_url): - kwargs["ssl_ca_certs"] = certifi.where() - - mongo_client = pymongo.MongoClient(mongo_url, **kwargs) - - if retry_attempts is None: - retry_attempts = 3 - - elif not retry_attempts: - retry_attempts = 1 - - last_exc = None - valid = False - t1 = time.time() - for attempt in range(1, retry_attempts + 1): - try: - mongo_client.server_info() - with mongo_client.start_session(): - pass - valid = True - break - - except Exception as exc: - last_exc = exc - if attempt < retry_attempts: - cls.log.warning( - "Attempt {} failed. Retrying... ".format(attempt) - ) - time.sleep(1) - - if not valid: - raise last_exc - - cls.log.info("Connected to {}, delay {:.3f}s".format( - mongo_url, time.time() - t1 - )) - return mongo_client +__all__ = ( + "MongoEnvNotSet", + "OpenPypeMongoConnection", + "get_default_components", + "should_add_certificate_path_to_mongo_url", + "validate_mongo_connection", +) From 69ad12d61dc93b66ad9620496a4a1776f9f92306 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:21:30 +0200 Subject: [PATCH 0310/1030] made '_get_project_connection' function temporarily public for create, update and remove --- openpype/client/entities.py | 60 +++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 1c32632915..aacc3a2304 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -20,7 +20,21 @@ def _get_project_database(): return OpenPypeMongoConnection.get_mongo_client()[db_name] -def _get_project_connection(project_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] @@ -93,7 +107,7 @@ def get_project(project_name, active=True, inactive=False, fields=None): {"data.active": False}, ] - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -108,7 +122,7 @@ def get_whole_project(project_name): project collection. """ - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find({}) @@ -131,7 +145,7 @@ def get_asset_by_id(project_name, asset_id, fields=None): return None query_filter = {"type": "asset", "_id": asset_id} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -153,7 +167,7 @@ def get_asset_by_name(project_name, asset_name, fields=None): return None query_filter = {"type": "asset", "name": asset_name} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -222,7 +236,7 @@ def _get_assets( return [] query_filter["data.visualParent"] = {"$in": parent_ids} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -319,7 +333,7 @@ def get_asset_ids_with_subsets(project_name, asset_ids=None): return [] subset_query["parent"] = {"$in": asset_ids} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) result = conn.aggregate([ { "$match": subset_query @@ -359,7 +373,7 @@ def get_subset_by_id(project_name, subset_id, fields=None): return None query_filters = {"type": "subset", "_id": subset_id} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filters, _prepare_fields(fields)) @@ -390,7 +404,7 @@ def get_subset_by_name(project_name, subset_name, asset_id, fields=None): "name": subset_name, "parent": asset_id } - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filters, _prepare_fields(fields)) @@ -463,7 +477,7 @@ def get_subsets( return [] query_filter["$or"] = or_query - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -487,7 +501,7 @@ def get_subset_families(project_name, subset_ids=None): return set() subset_filter["_id"] = {"$in": list(subset_ids)} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) result = list(conn.aggregate([ {"$match": subset_filter}, {"$project": { @@ -525,7 +539,7 @@ def get_version_by_id(project_name, version_id, fields=None): "type": {"$in": ["version", "hero_version"]}, "_id": version_id } - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -548,7 +562,7 @@ def get_version_by_name(project_name, version, subset_id, fields=None): if not subset_id: return None - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) query_filter = { "type": "version", "parent": subset_id, @@ -602,7 +616,7 @@ def _get_versions( else: query_filter["name"] = {"$in": versions} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -760,7 +774,7 @@ def get_output_link_versions(project_name, version_id, fields=None): if not version_id: return [] - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) # Does make sense to look for hero versions? query_filter = { "type": "version", @@ -825,7 +839,7 @@ def get_last_versions(project_name, subset_ids, fields=None): {"$group": group_item} ] - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) aggregate_result = conn.aggregate(aggregation_pipeline) if limit_query: output = {} @@ -943,7 +957,7 @@ def get_representation_by_id(project_name, representation_id, fields=None): if representation_id is not None: query_filter["_id"] = _convert_id(representation_id) - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -976,7 +990,7 @@ def get_representation_by_name( "parent": version_id } - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -1039,7 +1053,7 @@ def _get_representations( return [] query_filter["$or"] = or_query - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -1250,7 +1264,7 @@ def get_thumbnail_id_from_source(project_name, src_type, src_id): query_filter = {"_id": _convert_id(src_id)} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) src_doc = conn.find_one(query_filter, {"data.thumbnail_id"}) if src_doc: return src_doc.get("data", {}).get("thumbnail_id") @@ -1282,7 +1296,7 @@ def get_thumbnails(project_name, thumbnail_ids, fields=None): "type": "thumbnail", "_id": {"$in": thumbnail_ids} } - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -1303,7 +1317,7 @@ def get_thumbnail(project_name, thumbnail_id, fields=None): if not thumbnail_id: return None query_filter = {"type": "thumbnail", "_id": _convert_id(thumbnail_id)} - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) @@ -1334,7 +1348,7 @@ def get_workfile_info( "task_name": task_name, "filename": filename } - conn = _get_project_connection(project_name) + conn = get_project_connection(project_name) return conn.find_one(query_filter, _prepare_fields(fields)) From b23d89a149efb105db1f7ff8aa1c71726b7782b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:21:59 +0200 Subject: [PATCH 0311/1030] use client function in settings handlers instead of AvalonMongoDB --- openpype/settings/handlers.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 2bcc2e06dd..15ae2351fd 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -8,6 +8,7 @@ import six import openpype.version from openpype.client.mongo import OpenPypeMongoConnection +from openpype.client.entities import get_project_connection, get_project from .constants import ( GLOBAL_SETTINGS_KEY, @@ -338,8 +339,6 @@ class MongoSettingsHandler(SettingsHandler): def __init__(self): # Get mongo connection - from openpype.pipeline import AvalonMongoDB - settings_collection = OpenPypeMongoConnection.get_mongo_client() self._anatomy_keys = None @@ -362,7 +361,6 @@ class MongoSettingsHandler(SettingsHandler): self.collection_name = collection_name self.collection = settings_collection[database_name][collection_name] - self.avalon_db = AvalonMongoDB() self.system_settings_cache = CacheValues() self.project_settings_cache = collections.defaultdict(CacheValues) @@ -607,16 +605,14 @@ class MongoSettingsHandler(SettingsHandler): new_data = data_cache.data_copy() # Prepare avalon project document - collection = self.avalon_db.database[project_name] - project_doc = collection.find_one({ - "type": "project" - }) + project_doc = get_project(project_name) if not project_doc: raise ValueError(( "Project document of project \"{}\" does not exists." " Create project first." ).format(project_name)) + collection = get_project_connection(project_name) # Project's data update_dict_data = {} project_doc_data = project_doc.get("data") or {} @@ -1145,8 +1141,7 @@ class MongoSettingsHandler(SettingsHandler): document, version ) else: - collection = self.avalon_db.database[project_name] - project_doc = collection.find_one({"type": "project"}) + project_doc = get_project(project_name) self.project_anatomy_cache[project_name].update_data( self.project_doc_to_anatomy_data(project_doc), self._current_version From 9ef9c79e8fb7cf6e2a783abe3eba1ba13f1eaa6d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:34:30 +0200 Subject: [PATCH 0312/1030] move all hosts and families to the new integrator --- .../defaults/project_settings/global.json | 83 ++++--------------- .../schemas/schema_global_publish.json | 4 +- 2 files changed, 18 insertions(+), 69 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 545c792d47..d923fc65c9 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -172,74 +172,8 @@ }, "IntegrateAssetNew": { "hosts": [ - "aftereffects", - "blender", - "celaction", - "flame", - "fusion", - "harmony", - "hiero", - "houdini", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher", - "webpublisher" ], "families": [ - "workfile", - "pointcache", - "camera", - "animation", - "model", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsd", - "mvUsdComposition", - "mvUsdOverride", - "simpleUnrealTexture" ], "template_name_profiles": [ { @@ -287,7 +221,22 @@ }, "IntegrateAsset": { "hosts": [ - "maya" + "maya", + "aftereffects", + "blender", + "celaction", + "flame", + "fusion", + "harmony", + "hiero", + "houdini", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher", + "webpublisher" ], "families": [ "workfile", 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 71eed2e2de..5e3978a2df 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 @@ -584,7 +584,7 @@ "type": "dict", "collapsible": true, "key": "IntegrateAssetNew", - "label": "IntegrateAssetNew", + "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ { @@ -651,7 +651,7 @@ "type": "dict", "collapsible": true, "key": "IntegrateAsset", - "label": "IntegrateAsset", + "label": "Integrate Asset", "is_group": true, "children": [ { From 7646c54da87fe04570ba67027bbf5af308cc7b83 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:36:09 +0200 Subject: [PATCH 0313/1030] move subset group collecting to early integrator --- ...{collect_subset_group.py => integrate_subset_group.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename openpype/plugins/publish/{collect_subset_group.py => integrate_subset_group.py} (94%) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py similarity index 94% rename from openpype/plugins/publish/collect_subset_group.py rename to openpype/plugins/publish/integrate_subset_group.py index 56cd7de94e..4b566e8908 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -17,12 +17,12 @@ from openpype.lib import ( ) -class CollectSubsetGroup(pyblish.api.InstancePlugin): - """Collect Subset Group for publish.""" +class IntegrateSubsetGroup(pyblish.api.InstancePlugin): + """Integrate Subset Group for publish.""" # Run after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.495 - label = "Collect Subset Group" + order = pyblish.api.IntegratorOrder - 0.1 + label = "Subset Group" # Attributes set by settings subset_grouping_profiles = None From 0c59f1539872981ff896119ef7bb729c4c08064d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:38:28 +0200 Subject: [PATCH 0314/1030] rename old integrator to integrate legacy --- .../plugins/publish/{integrate_new.py => integrate_legacy.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/plugins/publish/{integrate_new.py => integrate_legacy.py} (100%) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_legacy.py similarity index 100% rename from openpype/plugins/publish/integrate_new.py rename to openpype/plugins/publish/integrate_legacy.py From d4a29c39aaae2c383ea9b887b294eab91352901d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 11:40:48 +0200 Subject: [PATCH 0315/1030] added deprecation warning to functions --- openpype/lib/mongo.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 80487f317d..bb2ee6016a 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -1,21 +1,51 @@ +import warnings +import functools from openpype.client.mongo import ( MongoEnvNotSet, OpenPypeMongoConnection, ) +class MongoDeprecatedWarning(DeprecationWarning): + pass + + +def mongo_deprecated(func): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.simplefilter("always", MongoDeprecatedWarning) + warnings.warn( + ( + "Call to deprecated function '{}'." + " Function was moved to 'openpype.client.mongo'." + ).format(func.__name__), + category=MongoDeprecatedWarning, + stacklevel=2 + ) + return func(*args, **kwargs) + return new_func + + +@mongo_deprecated def get_default_components(): from openpype.client.mongo import get_default_components return get_default_components() +@mongo_deprecated def should_add_certificate_path_to_mongo_url(mongo_url): from openpype.client.mongo import should_add_certificate_path_to_mongo_url return should_add_certificate_path_to_mongo_url(mongo_url) +@mongo_deprecated def validate_mongo_connection(mongo_uri): from openpype.client.mongo import validate_mongo_connection From 284c152ff0d892d273763f0428c4fce645162886 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Jul 2022 12:47:23 +0100 Subject: [PATCH 0316/1030] 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 01f2c59049be47fce42a5bfdcf67ef1227be1d11 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 15:15:40 +0200 Subject: [PATCH 0317/1030] the update placeholder keep placeholder info when canceled or closed --- openpype/hosts/maya/api/lib_template_builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index a30b3868b0..855c72e361 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -107,11 +107,13 @@ def update_placeholder(): placeholder = placeholder[0] args = placeholder_window(get_placeholder_attributes(placeholder)) - # delete placeholder attributes - delete_placeholder_attributes(placeholder) + if not args: return # operation canceled + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + options = create_options(args) imprint(placeholder, options) From 84781c12e570b085f533421c5c8ef0712f611504 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:12:04 +0200 Subject: [PATCH 0318/1030] call 'bulk_write' directly on legacy_io --- openpype/plugins/publish/integrate.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 5e86eb014a..790f96d419 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -79,12 +79,6 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) -def bulk_write(writes): - """Convenience function to bulk write into active project database""" - project = legacy_io.Session["AVALON_PROJECT"] - return legacy_io._database[project].bulk_write(writes) - - class IntegrateAsset(pyblish.api.InstancePlugin): """Register publish in the database and transfer files to destinations. @@ -288,7 +282,7 @@ 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. - bulk_write(subset_writes + version_writes) + legacy_io.bulk_write(subset_writes + version_writes) self.log.info("Subset {subset[name]} and Version {version[name]} " "written to database..".format(subset=subset, version=version)) @@ -362,7 +356,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): )) # Write representations to the database - bulk_write(representation_writes) + legacy_io.bulk_write(representation_writes) # Backwards compatibility # todo: can we avoid the need to store this? From 842cf06bf95458537d22eae3031bb7c64586e308 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:13:33 +0200 Subject: [PATCH 0319/1030] new integrator can tell legacy one that should not process the instance --- openpype/plugins/publish/integrate.py | 2 +- openpype/plugins/publish/integrate_legacy.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 790f96d419..71032a1d96 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -171,7 +171,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name_profiles = None def process(self, instance): - + instance.data["processedWithNewIntegrator"] = True # Exclude instances that also contain families from exclude families families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 797479af45..18e4035602 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -145,6 +145,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_grouping_profiles = None def process(self, instance): + if instance.data.get("processedWithNewIntegrator"): + self.log.info("Instance was already processed with new integrator") + return + for ef in self.exclude_families: if ( instance.data["family"] == ef or From a1784fc25e2b433e1f32e35a36588205b9904e98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:26:27 +0200 Subject: [PATCH 0320/1030] representations are checked before instance registration begins --- openpype/plugins/publish/integrate.py | 80 +++++++++++++++++---------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 71032a1d96..97f99bdba7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -14,6 +14,7 @@ from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io +from openpype.pipeline.publish import KnownPublishError log = logging.getLogger(__name__) @@ -172,6 +173,17 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def process(self, instance): instance.data["processedWithNewIntegrator"] = True + + filtered_repres = self.filter_representations(instance) + # Skip instance if there are not representations to integrate + # all representations should not be integrated + if not filtered_repres: + self.log.warning(( + "Skipping, there are no representations" + " to integrate for instance {}" + ).format(instance.data["family"])) + return + # Exclude instances that also contain families from exclude families families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) @@ -182,7 +194,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): file_transactions = FileTransaction(log=self.log) try: - self.register(instance, file_transactions) + self.register(instance, file_transactions, filtered_repres) except Exception: # clean destination # todo: preferably we'd also rollback *any* changes to the database @@ -194,8 +206,35 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def register(self, instance, file_transactions): + def filter_representations(self, instance): + # Prepare repsentations that should be integrated + repres = instance.data.get("representations") + # Raise error if instance don't have any representations + if not repres: + raise KnownPublishError( + "Instance {} has no representations to integrate".format( + instance.data["family"] + ) + ) + # Validate type of stored representations + if not isinstance(repres, (list, tuple)): + raise TypeError( + "Instance 'files' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + # Filter representations + filtered_repres = [] + for repre in repres: + if "delete" in repre.get("tags", []): + continue + filtered_repres.append(repre) + + return filtered_repres + + def register(self, instance, file_transactions, filtered_repres): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: self.log.info(( @@ -209,15 +248,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "@ {0}".format(instance_stagingdir) ) - # Ensure at least one representation is set up for registering. - repres = instance.data.get("representations") - assert repres, "Instance has no representations data" - assert isinstance(repres, (list, tuple)), ( - "Instance 'representations' must be a list, got: {0} {1}".format( - str(type(repres)), str(repres) - ) - ) - template_name = self.get_template_name(instance) subset, subset_writes = self.prepare_subset(instance) @@ -238,20 +268,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Prepare all representations prepared_representations = [] - for repre in instance.data["representations"]: - - if "delete" in repre.get("tags", []): - self.log.debug("Skipping representation marked for deletion: " - "{}".format(repre)) - continue - + for repre in filtered_repres: # todo: reduce/simplify what is returned from this function - prepared = self.prepare_representation(repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance) + prepared = self.prepare_representation( + repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance) for src, dst in prepared["transfers"]: # todo: add support for hardlink transfers @@ -259,12 +284,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): prepared_representations.append(prepared) - if not prepared_representations: - # Even though we check `instance.data["representations"]` earlier - # this could still happen if all representations were tagged with - # "delete" and thus are skipped for integration - raise RuntimeError("No representations prepared to publish.") - # Each instance can also have pre-defined transfers not explicitly # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. @@ -273,6 +292,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): for src, dst in instance.data.get("transfers", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) resource_destinations.add(os.path.abspath(dst)) + for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) resource_destinations.add(os.path.abspath(dst)) From 7016ca41f7809a1d3729e4999ddf4c1c0dfe1299 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:27:37 +0200 Subject: [PATCH 0321/1030] use already prepared modules from context --- openpype/plugins/publish/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 97f99bdba7..8fe5138963 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -317,8 +317,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync - manager = ModulesManager() - sync_server_module = manager.modules_by_name["sync_server"] + modules_by_name = instance.context.data["openPypeModules"] + sync_server_module = modules_by_name["sync_server"] sites = sync_server_module.compute_resource_sync_sites( project_name=instance.data["projectEntity"]["name"] ) From d04abc3767f7c5990c6e0a8a420229bc034075f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:27:54 +0200 Subject: [PATCH 0322/1030] use host name from context data --- openpype/plugins/publish/integrate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8fe5138963..e76adb55b8 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -310,10 +310,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() - self.log.debug("Backed up existing files: " - "{}".format(file_transactions.backups)) - self.log.debug("Transferred files: " - "{}".format(file_transactions.transferred)) + self.log.debug( + "Backed up existing files: {}".format(file_transactions.backups)) + self.log.debug( + "Transferred files: {}".format(file_transactions.transferred)) self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync @@ -780,8 +780,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") + "task_types": task.get("type"), + "hosts": instance.context["hostName"], } def get_rootless_path(self, anatomy, path): From 79c01bdf1a83f4b5d4b57ce0afcc237f696a6387 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:28:29 +0200 Subject: [PATCH 0323/1030] skip instances marked to be integrated on farm --- openpype/plugins/publish/integrate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e76adb55b8..e3a81091ba 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -172,8 +172,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name_profiles = None def process(self, instance): + # Mark instance as processed for legacy integrator instance.data["processedWithNewIntegrator"] = True + # Instance should be integrated on a farm + if instance.data.get("farm"): + self.log.info( + "Instance is marked to be processed on farm. Skipping") + return + filtered_repres = self.filter_representations(instance) # Skip instance if there are not representations to integrate # all representations should not be integrated From e6cad709cd5904087f687a4e2ec5a15641ab48e0 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 18:43:34 +0200 Subject: [PATCH 0324/1030] fix error when updating workfile from template with empty scene --- openpype/hosts/maya/api/template_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 0e346ca411..c7946b6ad3 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -80,7 +80,11 @@ class MayaTemplateLoader(AbstractTemplateLoader): return [attribute.rpartition('.')[0] for attribute in attributes] def get_loaded_containers_by_id(self): - containers = cmds.sets('AVALON_CONTAINERS', q=True) + try: + containers = cmds.sets("AVALON_CONTAINERS", q=True) + except ValueError: + return None + return [ cmds.getAttr(container + '.representation') for container in containers] From f398fae425d66c1b9934e9ec016fa1634761f633 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:48:22 +0200 Subject: [PATCH 0325/1030] simplified settings for skipping of families --- .../defaults/project_settings/global.json | 119 +----------------- .../schemas/schema_global_publish.json | 60 +-------- 2 files changed, 7 insertions(+), 172 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index d923fc65c9..bdcf85d1b2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,10 +171,6 @@ ] }, "IntegrateAssetNew": { - "hosts": [ - ], - "families": [ - ], "template_name_profiles": [ { "families": [], @@ -220,120 +216,7 @@ ] }, "IntegrateAsset": { - "hosts": [ - "maya", - "aftereffects", - "blender", - "celaction", - "flame", - "fusion", - "harmony", - "hiero", - "houdini", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher", - "webpublisher" - ], - "families": [ - "workfile", - "pointcache", - "camera", - "animation", - "model", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsd", - "mvUsdComposition", - "mvUsdOverride", - "simpleUnrealTexture" - ], - "template_name_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "publish" - }, - { - "families": [ - "review", - "render", - "prerender" - ], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "render" - }, - { - "families": [ - "simpleUnrealTexture" - ], - "hosts": [ - "standalonepublisher" - ], - "task_types": [], - "tasks": [], - "template_name": "simpleUnrealTexture" - }, - { - "families": [ - "staticMesh", - "skeletalMesh" - ], - "hosts": [ - "maya" - ], - "task_types": [], - "tasks": [], - "template_name": "maya2unreal" - } - ] + "skip_host_families": [] }, "IntegrateHeroVersion": { "enabled": true, 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 5e3978a2df..41eb04be4e 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 @@ -587,18 +587,6 @@ "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ - { - "type": "list", - "key": "hosts", - "label": "Hosts", - "object_type": "text" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, { "type": "list", "key": "template_name_profiles", @@ -656,58 +644,22 @@ "children": [ { "type": "list", - "key": "hosts", - "label": "Hosts", - "object_type": "text" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "list", - "key": "template_name_profiles", - "label": "Template name profiles", + "key": "skip_host_families", + "label": "Skip hosts and families", "use_label_wrap": true, "object_type": { "type": "dict", "children": [ { - "type": "label", - "label": "" + "type": "hosts-enum", + "key": "host", + "label": "Host" }, { + "type": "list", "key": "families", "label": "Families", - "type": "list", "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template_name", - "label": "Template name" } ] } From 1a024d3552723245c273362793ecee6b99f29823 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:56:52 +0200 Subject: [PATCH 0326/1030] use settings to decide if new integrator should skip instances --- openpype/plugins/publish/integrate.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e3a81091ba..0b725750aa 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -167,11 +167,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "username" ] + skip_host_families = [] # Attributes set by settings template_name_profiles = None def process(self, instance): + if self._temp_skip_instance_by_settings(instance): + return + # Mark instance as processed for legacy integrator instance.data["processedWithNewIntegrator"] = True @@ -213,6 +217,39 @@ 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. + + This is temporary solution until we test all usecases with new (this) + integrator plugin. + """ + + host_name = instance.context.data["hostName"] + instance_family = instance.data["family"] + instance_families = set(instance.data.get("families") or []) + + skip = False + for item in self.skip_host_families: + if item["host"] != host_name: + continue + + families = set(item["families"]) + if instance_family in families: + skip = True + break + + for family in instance_families: + if family in families: + skip = True + break + + if skip: + break + + if skip: + self.log.debug("Instance is marked to be skipped by settings.") + return skip + def filter_representations(self, instance): # Prepare repsentations that should be integrated repres = instance.data.get("representations") From 0ebd4b7c9afffa3174ae876f94ca22fa52f83627 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:57:00 +0200 Subject: [PATCH 0327/1030] added remaining hosts to integrator hosts --- openpype/plugins/publish/integrate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 0b725750aa..3c61d01858 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -105,7 +105,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - hosts = ["maya"] + hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", + "hiero", "houdini", "nuke", "photoshop", "resolve", + "standalonepublisher", "traypublisher", "tvpaint", "unreal", + "webpublisher"] families = ["workfile", "pointcache", "camera", From 6c457b2ed1331bb193a572803670836b40f16667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:10:22 +0200 Subject: [PATCH 0328/1030] use settings for publish templates from legacy integrator --- openpype/plugins/publish/integrate.py | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 3c61d01858..1b8015c946 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -172,9 +172,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ] skip_host_families = [] - # Attributes set by settings - template_name_profiles = None - def process(self, instance): if self._temp_skip_instance_by_settings(instance): return @@ -807,13 +804,33 @@ class IntegrateAsset(pyblish.api.InstancePlugin): """Return anatomy template name to use for integration""" # Define publish template name from profiles filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) + template_name_profiles = self._get_template_name_profiles(instance) + profile = filter_profiles( + template_name_profiles, + filter_criteria, + logger=self.log + ) + if profile: return profile["template_name"] - else: - return self.default_template_name + return self.default_template_name + + def _get_template_name_profiles(self, instance): + """Receive profiles for publish template keys. + + Reuse template name profiles from legacy integrator. Goal is to move + the profile settings out of plugin settings but until that happens we + want to be able set it at one place and don't break backwards + compatibility (more then once). + """ + + return ( + instance.context["project_settings"] + ["global"] + ["publish"] + ["IntegrateAssetNew"] + ["template_name_profiles"] + ) def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" From 30db574170a526759ac6c40c574aac0ed41bcdea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:18:41 +0200 Subject: [PATCH 0329/1030] fixed data access --- openpype/plugins/publish/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 1b8015c946..c9eb26d0b7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -825,7 +825,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): """ return ( - instance.context["project_settings"] + instance.context.data["project_settings"] ["global"] ["publish"] ["IntegrateAssetNew"] @@ -845,7 +845,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "families": anatomy_data["family"], "tasks": task.get("name"), "task_types": task.get("type"), - "hosts": instance.context["hostName"], + "hosts": instance.context.data["hostName"], } def get_rootless_path(self, anatomy, path): From 3c35cbc700102270c55adfa8b249ad9f75ebd226 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:23:13 +0200 Subject: [PATCH 0330/1030] make sure legacy integrator happens after new integrator --- openpype/plugins/publish/integrate.py | 1 - openpype/plugins/publish/integrate_legacy.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index c9eb26d0b7..cfaff4067b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -10,7 +10,6 @@ from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api import openpype.api -from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 18e4035602..34e81a3839 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -70,7 +70,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ label = "Integrate Asset (legacy)" - order = pyblish.api.IntegratorOrder + # Make sure it happens after new integrator + order = pyblish.api.IntegratorOrder + 0.00001 hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", "hiero", "houdini", "nuke", "photoshop", "resolve", "standalonepublisher", "traypublisher", "tvpaint", "unreal", From dc569c6d65e2a211681a4f0d17aea1874cef9268 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:30:31 +0200 Subject: [PATCH 0331/1030] rename and move CollectSubsetGroup to IntegrateSubsetGroup in settings --- .../defaults/project_settings/global.json | 22 ++-- .../schemas/schema_global_publish.json | 110 +++++++++--------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index bdcf85d1b2..9247c6ceb6 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -20,17 +20,6 @@ ], "skip_hosts_headless_publish": [] }, - "CollectSubsetGroup": { - "subset_grouping_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template": "" - } - ] - }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -170,6 +159,17 @@ } ] }, + "IntegrateSubsetGroup": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ] + }, "IntegrateAssetNew": { "template_name_profiles": [ { 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 41eb04be4e..af08bbec3c 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 @@ -39,61 +39,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "CollectSubsetGroup", - "label": "Collect Subset Group", - "is_group": true, - "children": [ - { - "type": "list", - "key": "subset_grouping_profiles", - "label": "Subset grouping profiles", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template", - "label": "Template" - } - ] - } - } - ] - }, { "type": "dict", "collapsible": true, @@ -580,6 +525,61 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateSubsetGroup", + "label": "Integrate Subset Group", + "is_group": true, + "children": [ + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 76c594af9a39f22bbbb00156c09d67bd9d0d2d0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:30:48 +0200 Subject: [PATCH 0332/1030] check for existence of subset group on instance before profiling --- .../plugins/publish/integrate_subset_group.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 4b566e8908..910cb060a6 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -37,18 +37,22 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return - # Skip if there is no matching profile - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.subset_grouping_profiles, - filter_criteria, - logger=self.log) - if not profile: - return - if instance.data.get("subsetGroup"): # If subsetGroup is already set then allow that value to remain - self.log.debug("Skipping collect subset group due to existing " - "value: {}".format(instance.data["subsetGroup"])) + self.log.debug(( + "Skipping collect subset group due to existing value: {}" + ).format(instance.data["subsetGroup"])) + return + + # Skip if there is no matching profile + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles( + self.subset_grouping_profiles, + filter_criteria, + logger=self.log + ) + + if not profile: return template = profile["template"] @@ -68,9 +72,9 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): ) except (KeyError, TemplateUnsolved): keys = fill_pairs.keys() - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) + self.log.warning(( + "Subset grouping failed. Only {} are expected in Settings" + ).format(','.join(keys))) if filled_template: instance.data["subsetGroup"] = filled_template From c59a9cb05f641ccf8ec3638d69b1cf764cee9262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 19 Jul 2022 14:40:01 +0200 Subject: [PATCH 0333/1030] Fix: shot duplicate name using shot's hierarchy (ep, seq) --- openpype/modules/kitsu/utils/update_op_with_zou.py | 12 ++++++++++-- 1 file changed, 10 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 4695a49159..f43bf07e25 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -181,7 +181,7 @@ def update_op_assets( # Find root folder docs root_folder_docs = get_assets( project_name, - asset_name=[entity_parent_folders[-1]], + asset_names=[entity_parent_folders[-1]], fields=["_id", "data.root_of"] ) # NOTE: Not sure why it's checking for entity type? @@ -221,6 +221,14 @@ def update_op_assets( parent_entity = parent_doc["data"]["zou"] parent_zou_id = parent_entity["parent_id"] + # Item name + if item_type == "Asset": + item_name = item_doc["name"] + elif item_type == "Shot": + # Name with parents hierarchy "({episode}_){sequence}_{shot}" + # to avoid duplicate name issue + item_name = "_".join(item_data["parents"] + [item_doc["name"]]) + # Set root folders parents item_data["parents"] = entity_parent_folders + item_data["parents"] @@ -234,7 +242,7 @@ def update_op_assets( item_doc["_id"], { "$set": { - "name": item["name"], + "name": item_name, "data": item_data, "parent": asset_doc_ids[item["project_id"]]["_id"], } From 9f41a512cb4ab27884ee2ad0465c23837f8ccad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 19 Jul 2022 14:44:25 +0200 Subject: [PATCH 0334/1030] black --- openpype/modules/kitsu/utils/update_op_with_zou.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index f43bf07e25..9a5dd7db61 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -14,7 +14,7 @@ from openpype.client import ( get_project, get_assets, get_asset_by_id, - get_asset_by_name + get_asset_by_name, ) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings @@ -182,7 +182,7 @@ def update_op_assets( root_folder_docs = get_assets( project_name, asset_names=[entity_parent_folders[-1]], - fields=["_id", "data.root_of"] + fields=["_id", "data.root_of"], ) # NOTE: Not sure why it's checking for entity type? # OP3 does not support multiple assets with same names so type @@ -224,7 +224,7 @@ def update_op_assets( # Item name if item_type == "Asset": item_name = item_doc["name"] - elif item_type == "Shot": + elif item_type == "Shot": # Name with parents hierarchy "({episode}_){sequence}_{shot}" # to avoid duplicate name issue item_name = "_".join(item_data["parents"] + [item_doc["name"]]) From 2bf3cf1b3094bb7402e7e3a8de55267fd4440edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 19 Jul 2022 14:54:36 +0200 Subject: [PATCH 0335/1030] Fix wrong name for sequence --- openpype/modules/kitsu/utils/update_op_with_zou.py | 7 +++---- 1 file changed, 3 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 9a5dd7db61..b2f0caf52b 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -221,13 +221,12 @@ def update_op_assets( parent_entity = parent_doc["data"]["zou"] parent_zou_id = parent_entity["parent_id"] - # Item name - if item_type == "Asset": - item_name = item_doc["name"] - elif item_type == "Shot": + 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"]]) + else: + item_name = item_doc["name"] # Set root folders parents item_data["parents"] = entity_parent_folders + item_data["parents"] From c9ad287c7b4521da8c56ccf4d197c3e3befed61f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Jul 2022 16:40:14 +0200 Subject: [PATCH 0336/1030] 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 0337/1030] 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 0338/1030] 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 0339/1030] 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 0340/1030] 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 0341/1030] 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 0342/1030] 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 0343/1030] 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 037ed71f60eba9d5947aef8c973e8b791596b942 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Jul 2022 13:52:15 +0200 Subject: [PATCH 0344/1030] keep subset group template settings but mark them as deprecated with hint where to move the value --- .../defaults/project_settings/global.json | 9 ++++ .../schemas/schema_global_publish.json | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9247c6ceb6..e509db2791 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,6 +171,15 @@ ] }, "IntegrateAssetNew": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ], "template_name_profiles": [ { "families": [], 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 af08bbec3c..b9d0b7daba 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 @@ -587,6 +587,52 @@ "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ + { + "type": "label", + "label": "NOTE: Subset grouping profiles settings were moved to
Integrate Subset Group. Please move values there." + }, + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles (DEPRECATED)", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + }, { "type": "list", "key": "template_name_profiles", From bedca7eaf98212d1a5450b097029d25dfdaf4d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 20 Jul 2022 16:30:03 +0200 Subject: [PATCH 0345/1030] :wrench: add relevant Maya validators to Settings add missing validators and add ability to set them optional if needed --- .../validate_review_subset_uniqueness.py | 4 +- .../plugins/publish/validate_setdress_root.py | 3 +- .../defaults/project_settings/maya.json | 178 +++++++++++++++++- .../schemas/schema_maya_publish.json | 155 +++++++++++++++ 4 files changed, 329 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py index d70096ee45..04cc9ab5fb 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py +++ b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py @@ -6,7 +6,7 @@ from openpype.pipeline import PublishXmlValidationError class ValidateReviewSubsetUniqueness(pyblish.api.ContextPlugin): - """Validates that nodes has common root.""" + """Validates that review subset has unique name.""" order = openpype.api.ValidateContentsOrder hosts = ["maya"] @@ -17,7 +17,7 @@ class ValidateReviewSubsetUniqueness(pyblish.api.ContextPlugin): subset_names = [] for instance in context: - self.log.info("instance:: {}".format(instance.data)) + self.log.debug("Instance: {}".format(instance.data)) if instance.data.get('publish'): subset_names.append(instance.data.get('subset')) diff --git a/openpype/hosts/maya/plugins/publish/validate_setdress_root.py b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py index 0b4842d208..8e23a7c04f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_setdress_root.py +++ b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py @@ -4,8 +4,7 @@ import openpype.api class ValidateSetdressRoot(pyblish.api.InstancePlugin): - """ - """ + """Validate if set dress top root node is published.""" order = openpype.api.ValidateContentsOrder label = "SetDress Root" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5976c6a823..c96acbff6d 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -205,10 +205,15 @@ "enabled": true, "optional": true, "active": true, - "exclude_families": ["model", "rig", "staticMesh"] + "exclude_families": [ + "model", + "rig", + "staticMesh" + ] }, "ValidateShaderName": { "enabled": false, + "optional": true, "regex": "(?P.*)_(.*)_SHD" }, "ValidateShadingEngine": { @@ -222,6 +227,7 @@ }, "ValidateLoadedPlugin": { "enabled": false, + "optional": true, "whitelist_native_plugins": false, "authorized_plugins": [] }, @@ -236,6 +242,7 @@ }, "ValidateUnrealStaticMeshName": { "enabled": true, + "optional": true, "validate_mesh": false, "validate_collision": true }, @@ -252,6 +259,81 @@ "redshift_render_attributes": [], "renderman_render_attributes": [] }, + "ValidateCurrentRenderLayerIsRenderable": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderImageRule": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderNoDefaultCameras": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderSingleCamera": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderLayerAOVs": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateStepSize": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVRayDistributedRendering": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayReferencedAOVs": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVRayTranslatorEnabled": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayProxy": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayProxyMembers": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRenderScriptCallbacks": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigCacheState": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigInputShapesInInstance": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigSettings": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateModelName": { "enabled": false, "database": true, @@ -270,6 +352,7 @@ }, "ValidateTransformNamingSuffix": { "enabled": true, + "optional": true, "SUFFIX_NAMING_TABLE": { "mesh": [ "_GEO", @@ -293,7 +376,7 @@ "ALLOW_IF_NOT_IN_SUFFIX_TABLE": true }, "ValidateColorSets": { - "enabled": false, + "enabled": true, "optional": true, "active": true }, @@ -337,6 +420,16 @@ "optional": true, "active": true }, + "ValidateMeshNoNegativeScale": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateMeshNonZeroEdgeLength": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateMeshNormalsUnlocked": { "enabled": false, "optional": true, @@ -359,22 +452,22 @@ }, "ValidateNoNamespace": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNoNullTransforms": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNoUnknownNodes": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNodeNoGhosting": { "enabled": false, - "optional": true, + "optional": false, "active": true }, "ValidateShapeDefaultNames": { @@ -402,6 +495,21 @@ "optional": true, "active": true }, + "ValidateNoVRayMesh": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateUnrealMeshTriangulated": { + "enabled": false, + "optional": true, + "active": true + }, + "ValidateAlembicVisibleOnly": { + "enabled": true, + "optional": false, + "active": true + }, "ExtractAlembic": { "enabled": true, "families": [ @@ -425,8 +533,34 @@ "optional": true, "active": true }, + "ValidateAnimationContent": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateOutRelatedNodeIds": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRigControllersArnoldAttributes": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateSkeletalMeshHierarchy": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateSkinclusterDeformerSet": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateRigOutSetNodeIds": { "enabled": true, + "optional": false, "allow_history_only": false }, "ValidateCameraAttributes": { @@ -439,14 +573,44 @@ "optional": true, "active": true }, + "ValidateAssemblyNamespaces": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateAssemblyModelTransforms": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateAssRelativePaths": { "enabled": true, + "optional": false, + "active": true + }, + "ValidateInstancerContent": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateInstancerFrameRanges": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateNoDefaultCameras": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateUnrealUpAxis": { + "enabled": false, "optional": true, "active": true }, "ValidateCameraContents": { "enabled": true, - "optional": true, + "optional": false, "validate_shapes": true }, "ExtractPlayblast": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 84182973a1..53247f6bd4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -107,6 +107,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "label", "label": "Shader name regex can use named capture group asset to validate against current asset name.

Example:
^.*(?P=<asset>.+)_SHD

" @@ -159,6 +164,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "whitelist_native_plugins", @@ -246,6 +256,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "validate_mesh", @@ -332,6 +347,72 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateCurrentRenderLayerIsRenderable", + "label": "Validate Current Render Layer Has Renderable Camera" + }, + { + "key": "ValidateRenderImageRule", + "label": "Validate Images File Rule (Workspace)" + }, + { + "key": "ValidateRenderNoDefaultCameras", + "label": "Validate No Default Cameras Renderable" + }, + { + "key": "ValidateRenderSingleCamera", + "label": "Validate Render Single Camera" + }, + { + "key": "ValidateRenderLayerAOVs", + "label": "Validate Render Passes / AOVs Are Registered" + }, + { + "key": "ValidateStepSize", + "label": "Validate Step Size" + }, + { + "key": "ValidateVRayDistributedRendering", + "label": "VRay Distributed Rendering" + }, + { + "key": "ValidateVrayReferencedAOVs", + "label": "VRay Referenced AOVs" + }, + { + "key": "ValidateVRayTranslatorEnabled", + "label": "VRay Translator Settings" + }, + { + "key": "ValidateVrayProxy", + "label": "VRay Proxy Settings" + }, + { + "key": "ValidateVrayProxyMembers", + "label": "VRay Proxy Members" + }, + { + "key": "ValidateYetiRenderScriptCallbacks", + "label": "Yeti Render Script Callbacks" + }, + { + "key": "ValidateYetiRigCacheState", + "label": "Yeti Rig Cache State" + }, + { + "key": "ValidateYetiRigInputShapesInInstance", + "label": "Yeti Rig Input Shapes In Instance" + }, + { + "key": "ValidateYetiRigSettings", + "label": "Yeti Rig Settings" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", @@ -416,6 +497,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "label", "label": "Validates transform suffix based on the type of its children shapes." @@ -472,6 +558,14 @@ "key": "ValidateMeshNonManifold", "label": "ValidateMeshNonManifold" }, + { + "key": "ValidateMeshNoNegativeScale", + "label": "Validate Mesh No Negative Scale" + }, + { + "key": "ValidateMeshNonZeroEdgeLength", + "label": "Validate Mesh Edge Length Non Zero" + }, { "key": "ValidateMeshNormalsUnlocked", "label": "ValidateMeshNormalsUnlocked" @@ -525,6 +619,18 @@ { "key": "ValidateUniqueNames", "label": "ValidateUniqueNames" + }, + { + "key": "ValidateNoVRayMesh", + "label": "Validate No V-Ray Proxies (VRayMesh)" + }, + { + "key": "ValidateUnrealMeshTriangulated", + "label": "Validate if Mesh is Triangulated" + }, + { + "key": "ValidateAlembicVisibleOnly", + "label": "Validate Alembic visible node" } ] }, @@ -573,6 +679,26 @@ { "key": "ValidateRigControllers", "label": "Validate Rig Controllers" + }, + { + "key": "ValidateAnimationContent", + "label": "Validate Animation Content" + }, + { + "key": "ValidateOutRelatedNodeIds", + "label": "Validate Animation Out Set Related Node Ids" + }, + { + "key": "ValidateRigControllersArnoldAttributes", + "label": "Validate Rig Controllers (Arnold Attributes)" + }, + { + "key": "ValidateSkeletalMeshHierarchy", + "label": "Validate Skeletal Mesh Top Node" + }, + { + "key": "ValidateSkinclusterDeformerSet", + "label": "Validate Skincluster Deformer Relationships" } ] }, @@ -589,6 +715,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "allow_history_only", @@ -611,9 +742,33 @@ "key": "ValidateAssemblyName", "label": "Validate Assembly Name" }, + { + "key": "ValidateAssemblyNamespaces", + "label": "Validate Assembly Namespaces" + }, + { + "key": "ValidateAssemblyModelTransforms", + "label": "Validate Assembly Model Transforms" + }, { "key": "ValidateAssRelativePaths", "label": "ValidateAssRelativePaths" + }, + { + "key": "ValidateInstancerContent", + "label": "Validate Instancer Content" + }, + { + "key": "ValidateInstancerFrameRanges", + "label": "Validate Instancer Cache Frame Ranges" + }, + { + "key": "ValidateNoDefaultCameras", + "label": "Validate No Default Cameras" + }, + { + "key": "ValidateUnrealUpAxis", + "label": "Validate Unreal Up-Axis check" } ] }, From 59395883170cac8ec8c11ca7b66ccd40d499cbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 20 Jul 2022 17:30:40 +0200 Subject: [PATCH 0346/1030] Change: Asset is put under an AssetType folder --- .../modules/kitsu/utils/update_op_with_zou.py | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4695a49159..e0ff87adf7 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -14,7 +14,7 @@ from openpype.client import ( get_project, get_assets, get_asset_by_id, - get_asset_by_name + get_asset_by_name, ) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings @@ -154,17 +154,23 @@ def update_op_assets( parent_zou_id = substitute_parent_item["parent_id"] else: parent_zou_id = ( - item.get("parent_id") + # For Asset, put under asset type directory + item.get("entity_type_id") + if item_type == "Asset" + else None + # Else, fallback on usual hierarchy + or item.get("parent_id") or item.get("episode_id") or item.get("source_id") - ) # TODO check consistency + ) - # Substitute Episode and Sequence by Shot - substitute_item_type = ( - "shots" - if item_type in ["Episode", "Sequence"] - else f"{item_type.lower()}s" - ) + # Substitute item type for general classification (assets or shots) + if item_type in ["Asset", "AssetType"]: + substitute_item_type = "assets" + elif item_type in ["Episode", "Sequence"]: + substitute_item_type = "shots" + else: + substitute_item_type = f"{item_type.lower()}s" entity_parent_folders = [ f for f in project_module_settings["entities_root"] @@ -181,8 +187,8 @@ def update_op_assets( # Find root folder docs root_folder_docs = get_assets( project_name, - asset_name=[entity_parent_folders[-1]], - fields=["_id", "data.root_of"] + asset_names=[entity_parent_folders[-1]], + fields=["_id", "data.root_of"], ) # NOTE: Not sure why it's checking for entity type? # OP3 does not support multiple assets with same names so type @@ -219,7 +225,7 @@ def update_op_assets( # Get parent entity parent_entity = parent_doc["data"]["zou"] - parent_zou_id = parent_entity["parent_id"] + parent_zou_id = parent_entity.get("parent_id") # Set root folders parents item_data["parents"] = entity_parent_folders + item_data["parents"] @@ -236,7 +242,7 @@ def update_op_assets( "$set": { "name": item["name"], "data": item_data, - "parent": asset_doc_ids[item["project_id"]]["_id"], + "parent": project_doc["_id"], } }, ) @@ -327,6 +333,10 @@ def sync_all_projects(login: str, password: str): def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): """Update OP project in DB with Zou data. + `root_of` is meant to sort entities by type for a better readability in the data tree. It + puts all shot like (Shot and Episode and Sequence) and asset entities under two different root + folders or hierarchy, defined in settings. + Args: dbcon (AvalonMongoDB): MongoDB connection project (dict): Project dict got using gazu. @@ -341,12 +351,17 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) + all_asset_types = gazu.asset.all_asset_types_for_project(project) all_episodes = gazu.shot.all_episodes_for_project(project) all_seqs = gazu.shot.all_sequences_for_project(project) all_shots = gazu.shot.all_shots_for_project(project) all_entities = [ item - for item in all_assets + all_episodes + all_seqs + all_shots + for item in all_assets + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] @@ -401,21 +416,20 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): "data": { "root_of": entity_type, "parents": parent_folders[:i], - "visualParent": direct_parent_doc, + "visualParent": direct_parent_doc.inserted_id + if direct_parent_doc + else None, "tasks": {}, }, } ) # Create - to_insert = [] - to_insert.extend( - [ - create_op_asset(item) - for item in all_entities - if item["id"] not in zou_ids_and_asset_docs.keys() - ] - ) + to_insert = [ + create_op_asset(item) + for item in all_entities + if item["id"] not in zou_ids_and_asset_docs.keys() + ] if to_insert: # Insert doc in DB dbcon.insert_many(to_insert) From a7044dadf7c74260f5c8945cccad196d4de89a1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Jul 2022 17:35:27 +0200 Subject: [PATCH 0347/1030] fix containers varible usage --- openpype/pipeline/load/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index a9aa240ff6..8b12088d3c 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -732,7 +732,7 @@ def filter_containers(containers, project_name): some missing entity in database. Args: - containers (list[dict]): List of containers referenced into scene. + containers (Iterable[dict]): List of containers referenced into scene. project_name (str): Name of project in which context shoud look for versions. @@ -741,6 +741,9 @@ def filter_containers(containers, project_name): 'invalid' and 'not_found' containers. """ + # Make sure containers is list that won't change + containers = list(containers) + outdated_containers = [] uptodate_containers = [] not_found_containers = [] From 02cc2166c1ee165b48d697c876675302c8cb77db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 20 Jul 2022 17:37:19 +0200 Subject: [PATCH 0348/1030] docstring linting line length --- openpype/modules/kitsu/utils/update_op_with_zou.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index e0ff87adf7..cabf4e4d18 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -333,9 +333,9 @@ def sync_all_projects(login: str, password: str): def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): """Update OP project in DB with Zou data. - `root_of` is meant to sort entities by type for a better readability in the data tree. It - puts all shot like (Shot and Episode and Sequence) and asset entities under two different root - folders or hierarchy, defined in settings. + `root_of` is meant to sort entities by type for a better readability in + the data tree. It puts all shot like (Shot and Episode and Sequence) and + asset entities under two different root folders or hierarchy, defined in settings. Args: dbcon (AvalonMongoDB): MongoDB connection From a3144c9d75e3372e1d7a3008f54e2695c81a51d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 20 Jul 2022 17:40:16 +0200 Subject: [PATCH 0349/1030] docstring linting line length --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 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 cabf4e4d18..46e0fa38f3 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -333,7 +333,7 @@ def sync_all_projects(login: str, password: str): def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): """Update OP project in DB with Zou data. - `root_of` is meant to sort entities by type for a better readability in + `root_of` is meant to sort entities by type for a better readability in the data tree. It puts all shot like (Shot and Episode and Sequence) and asset entities under two different root folders or hierarchy, defined in settings. From af45aff844ab2ff28ccc76ec99d3e5a9803c92e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 20 Jul 2022 17:56:37 +0200 Subject: [PATCH 0350/1030] docstring linting line length --- 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 46e0fa38f3..7262d2ee1a 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -335,7 +335,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): `root_of` is meant to sort entities by type for a better readability in the data tree. It puts all shot like (Shot and Episode and Sequence) and - asset entities under two different root folders or hierarchy, defined in settings. + asset entities under two different root folders or hierarchy, defined in + settings. Args: dbcon (AvalonMongoDB): MongoDB connection From 480d1968124ccf4fe2bc70b109145b016292fe25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 20 Jul 2022 17:56:55 +0200 Subject: [PATCH 0351/1030] docstring linting line length --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 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 7262d2ee1a..7bfbd42f6a 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -335,7 +335,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): `root_of` is meant to sort entities by type for a better readability in the data tree. It puts all shot like (Shot and Episode and Sequence) and - asset entities under two different root folders or hierarchy, defined in + asset entities under two different root folders or hierarchy, defined in settings. Args: From 7793ea5580595c95d875b056bdf1a61685f63a1d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 20 Jul 2022 19:26:10 +0100 Subject: [PATCH 0352/1030] 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{ zw%hR5JMUq)1NX-*w~WROf4K}lefJHXdh0QUyTx$uK8K<{nu+cMcf{CxZpO56cj2+e zrsCMsF2*4T?Sp%6z7)%8R9{v@dsJ-@J+>Q!2kyBBFHC<7PrUR2#*cd(?-A%em@^%v zR58PuA+*Q}X1R`j9J(=)DV{{yNF=UsaIQhIY@x>RPYuU44%wJ6=2jFfZ$hWe{dwLMC@-&}!7~>J@4FA4ec%%8JmR-l8SIa12^znA{46F; zdJY-ceKG3z-(rtFdf?vsAH%`FKNY_jJOKCJb{)R?=|}kUa^a_*><@%!w~L~-vJUtD z>o$D!-lrHad@p*F>T%c2w=i9=9nL)e4BF}%P+Jip5c%<+36pWwUq)f)A=~2KF^}Mj zuYSP9|NILdyz?5qSz3*Yue=5~U4Aw`eg8eQFUUq_P8ZZ>cE@v*$04WuD;#;uiD>9J z1Xo@D5A^Tm#U+ebVv$t)p zK4fKoA<%t?DO0B6@0b1=xj8XBHRUN(auL^OXJgW1PvXB9-G=Wz`4G>2G6ny-?{>_3 z>P-Sb7YyD1cewiEb8y=QC(%(6NAI2Y!ygVDiN_xqjW6bXjbNv3=;YX*_A4JQIQuBP z_wgJ&^UB9~Wb9*jf9gYcpFmel?_x5t-2% zJo2w|@TW7*#fsn{Tz&apvG}uTc;d;I(U7qnjyPgCcHOlzCO^y{d9gYAY*n|Lr&9v(LZAm#qGL`IXn=jAM^s58h5V@BEVqo=;%pzDJ;> zDgzUrnubfxI|+H-8cdopowl`1oPGK+^zv3?^0RYr)?Y7T_|Ug_mFjyri#aSTs)D!u zju><2P59u+e__V#w@}Ra?YrM^@W(@T#$}gZj;OzYcGDm=PBK;P`X=sJ2?iIyOPAVN zpi6(5+auZzbdHT~pa2~gRXtgOgh=%0(jDc}IVbJR2nmc}r~_dW{Ar?^7s~eH*1N94 zv}d2j>=|>AO?y)~+JK%tsJfnXCcgP*5hgu485dl961w-y#+1oZF#o5e2zBj_@#80B z%q6#B-WMO^`41=HoD0vu{I7~=Oz(gZe>eu0QI%~f`dLFGDw}*LEi1)jvJy}QJ>}HX z(78i@uEQX@cJ<)qe_eyAGe5(GaZgZn9jB^m1)T-Us%mi9p-1EL%dbQQk6*TU39h;B zYHgn=>U>k9Gz*fkYtXSvN8EhfUwMtyxc06IIN;Dx*lR$0JTc+F`0D3+1jvjoKI2&I zJA4lw-;Qc-7~MmaIPj=Hp{9Tg>Cy|Z=(Fi~a_UQH@b#pcx;J*&wJV-|@l70Z_?ejh z{zrKA=_laNr!$65k4D-dqXKkfbn?w~&mI57mtTI5C!hZicG!S$D%tL@C9 zbSebsCG6F+2mW!<1*EG#e;y1fv`N|3m9oQe!A0LN<4j+Z4+yS`q$_p`n?(-N& zhln?8TO4)d2<*0d4?Oh9(>V07GvK3lb_&6(o*^jGeK($2K=Wac&59xnsVT<2cixE4 zzxWno$4| zaI$sr0(b4yn>I$Ew4zA!0H;2BFYi$vj=yLOo_qRny!z0Uw5Ps+jCMothr@n{14i`6 z6_;OzC~b66(7C~%3`z0RJIt>|`9iQjm;S1?N2D!*bb&6&SlRlTGAvoL)OcO{4BIpA zSN0O-`pO{Cbwyzzo8zS21L zrIpJW*Y+|hN;7DqI}z;}5ixo4Lo~QAL(Vok;=X$)Vf|6lO{Zd4_=sx7oT~O>LG)6X{U>mshB`!hNx&Zg4}~Ra%d+TG;km*E=wFAy_YOX zy5DYn$%5wMffwGT?d=f!^`t{E=b0&3u(XKD!)3VnpEuy?XP!n`Svh{Q=bkwD;Dhk{ zgOBI@`r^C`PeqrmVN9Gj1;r&%45gD}?-6})_vnYwxA&en>aasGZ~j;8TC^C&rRDf` z-Y>Ls`e{f7%FBxJ?|W~#~+)4etr96@)HkX{`~K8(n+VFXU|?ZosNg{iW=N` z=bhMZa0M=km*mHdk75#Uq^|@o$<7CcoT99!@JtHbmw8E)g(s$S%s=78YAU8-=se89BSjupT^ngyLV!CW+Fs?+S`!m9{MG;Ig za2GvUHRX%=uz;MQd#qq&MZ;}l$IJclDofYTe z-KS?@#lrcx=*F`#Y3iex_rqf3W-%Ot_M+VUApU;Id5(j_j|D$1po;t~+P80yBab|i zDr;B#@Wb~6oeF&V`CQCfxDaQac_x1T^do%r@kfZLdKlomsgBX{5Myw&EM>QOwKNLa zd!M~Iw*%GQ==b|*FMJzwUV0vjYw1Qs+RZMhog=<3VZLpx3*$? z_0?CA$uVAhVGhj#KJ@F~6DOZ>IGq_`y!qOPC@Wuxop#wCvtE9S%&eMoJ{!KD^z#cc(g-E~Ar)8ED|YzEv00@)m^jP^u&wi(A3kV{+cNqck9@?v`8 zWMQmkTVt6f6O5)6Lfm9`QFkOr%_q5~1b{{^Vu@)H0FwN(7}=kMn@DR#jdhW{$|%7@ zb3MiWL#q(gttbI9$~guIGO|S({*jf%P?aX;j!NvCrf^bm=0c2V0>6wWjDyUJ03~^z z5iW2)L00zHB!8y~x6DrfPX*~@#mnC^2LT6}n*g+ci7+*EXkt8LZZ@+q2(I#_ zBw!aMD2m)9hgQI_iD`P08tCCOVV(uaku&$#0G(T1MqbF-Eznsw+ISLZ3((awmUU%O zu_?T{^YH(RNBGSiWgH#^UxD)__voy;7v4IH&SI4 zrALzBRn3)(9kM{gbEj+2B*IIIx!B4igh8XV1hkxjZ^pkz01%I#!l^Jh0TBZRrrw;1 zx)=4?ylIqbVO?^f2pN(j){`J|F0vPk4(NHz?ov-r_EQEVe`)amBMU78AZM0q6tL4h z$=8%uJm+d>m244HJVC)<@`^3c zHM__5#io%!xQpd1&w5o5b#<7)v*VI-!`xuaGI&Pc@%VL1Ge)7Ehwjm(pktg(PZsl_F)mY?y0 zB~-<*Ng*swWfbQ=x}YR*Bsk|XC}BhbJSLDfMV*t2aKKU=9CDRf&_w`Uu32(>9Tur% zF3uDyUK3%RAjj|x00k< zH8gN}MX_k%0u1ao2=$Ch6Tn$@lhiJNg}lV3-GbX6lmC8+jD!naVxXcjUFbXl<) zvZy$!Xr~g!;^*fXOt{yTCiXE2SJv8aQWnWLG9<7=7>{27KtYHLUf_s}UCJkUyex4f zK-ZMx03Cta$+Ok6OfAT$lvm1R!2)4&RsIm2B((oUJbX`VPc<4Q5tAYTQk%|UMgj(A zGmOd9$`iiBB2p43C?%5Q2r@2ZppGhye0)Y0Y;9~B=Y0&e&&A3Us3TE3(%RMT{G|1v+(BkQGm<=I)O8pxU_$x zi~_1I8*meum>PkC*F2vCbgq1?jn0D6rjUU9M7uzjpU=E-n!&_0)7%+mW~0lH|7A+f zJHtzg>D*LJXz*@(#(e~z%JLG_R90g9?fRf0N&pfRY_@P@2#0O)F9Exn8YZqgHYxX_ zCH_p*M=u$QBY_~cav=POK$uT^qRNq8lPt$6Yr9-Y`%UN@1=ib+9dDr}X4H^kUH0m@=! zntFSjFThovveum4JzfS^aof#GV2u)RSDaD}HJ!xKt{sEd6?2@ml5(`ewe%hO>4<47 zQB+u{<;pxmcG)u?ZK^{xW54Qasx@}3N(hoH0HZR>3K^GLs8I)= z!MpAmuVcK)vIPsVP4})0!tFptX9`lb7U)u&&bVNpY(r^=nPvhbT-#iWo2Mo%wZ%Dz z`2v)-*~A z(6xo!@bGnk(i-Qlr*>>mMMv{@I_guV+F%Y zoH)9^1Bb*53zxB;S-Aq;fc`tMpraVRQ`Ih4zy?Ju(4{%Hm|eaL4Z3#3f^(kkxDRbFwgS@F14YmPRNoZFI?LTA*7!(4}`H4L?pb z_NRNi-QP-J>y^Oz0bPJ?3F<1#SPjVu7@qsx5Ah4mIS22){iasA*nRh1S^Uzp`dgjy zY8hRNM=OQVrTfrXpi5U`cK=_G1X>StuEnUOGop@tT_rO@+a13D(P#08#*V?Gk4|9D zqaVBOybD-hqgg10<2QBqhgDshJ!}@}em!f?Ug;*3Kb^0=y+Z0>P?v@ z1-ch!&5nQj<>$EKvMbn3*^eCs=$K_>!bDulzRAUOUt87Hs;-;xiMOZvRZ1XDpc5yA zOT`#xo0-WnO)NfJRJ6hX-G`rj7GJ_zZ6}^^g8Dpn-gzf&C1@bdc*k3Y=AefQmmUs!H?w>|b`(yXarrU)m;rIsGEjIQ~!W`XY4 zv-a$jZbAvP2I$D>B!9xqF>n~309|g56bz}u3b&N{xflK|{=>K5vPAc*to$3qPCM;F zpp&FoQ`gM`-8$~A1-ec6@Y_@UdL+*Htb_7+njXlef8FejYp4 zu>#NvIwKV59E@R|vrHXik3IIllEsCluWL%RjntUBB+yl@Ql=?U*CV%zANNFDG z>@G_BT5?*~HmH0Vv=N|n*hI(r^k=S?&lag;>5pQMuoBq%Bp_+1sT@F>Gp!Ej%=+Pf zPK6&)T?_KrY@H?83SCC$4}{|R?K^16rEcB2p+}GIsAQuQk1#rishJ8KZg`0lieyDM z7PhOd5ukI?)e*Fm4{8HwxX9dwLz8!SeGax@v-PbMdqrEc1f(uvOzTvdEylWMErHH~ zIA;$jjw?IH$>`X`inSXHS5S2ouWLc4?(vQt3fRlF2)VgA7{sLhl|>~^DRH;n)0%+J z)T2u#^&35Ty?}0gz-o)Wt#QDIfzCdfRsvQ68$kkT09~pwmH@f} z0-dO?E~E2>a^jh6cPBU&3WgNu%1bL9pleC59Ar(Xt`_Jv!Y#H#S_xPQvAklepy-&>6v&3oA5^j;gCaJC{Jm2G49bQNU)!{rdK2GviwA6t#LC)G|8z6j}*b z32bButP|)A=}LHA^%aAM#V091moLq8B3M!A09{6bM>{P503ZNKL_t(2E1oHx?Ah0~ zfE{-F_aA`T%6bcQ8~Hxk;jIL$1lA&fRso$8mgck~P@r=eT}z+?y{=-Li;&Uf7ZhN? z4g*kM9X0{OtMw{aMrR*TD*-EkjVyun1vKQk#BoPc50qjP~S5{)1)Cr{fQHq?pN)y;%xDRJv{{k1=X_WG>^Ho64b z0Ca9O6RBR3lkGz^Qg6JjQg}0vldl;RYX-X11PhxPvC(g^J%N>gm4H)>WNo>wCTlBE zcvD}QvQ1L+B$Szfa6>iAG&$`KGnwluovoV#UBn45X;IQc%OF`sXP-kW0V{!xEP+-r zy5^Y?NuY}(7>J{`wvq)(Qh-kC%QZ$K$jzhbI)DMgbqqCD)wM;gD*;ieM8;!LmTB^# zilv>JSiQulf~WZEwbe^D@)KZ(w-WdjNMIG9GflbLwP)eYMkx&^W#qJ8iTPEz>0UYJu)o@DSQdv=Z3Z5@;3B zwXCq@ri3*II&F6de`eet$h1JWv2UCm-%7wrp!E`H1JJq5P8nT*jIPQsIs*L+8HC8Lv%Okwuq=9CJsjIQh>V$BSjwZH1W(@U}DM07( z`q&zgd0|aaq^S6#}!uEnOp@W5fa#LLUeQCVKeGzky-_U(sQ)UnS=4?qiaZM#wSC@TRgfwoJa zl|Ywjn^PN|K$bWoGULqf5=O^3I$?A}ciSUgTU(39#weC8U4or<8iq)OHag4b+J2Ml zkyZj$0_!J%G=MH)n-kZnoRy{2L)jTL!BvvcIY76=PQ&62R9ow+DgmDtJ$v?|GeTMz z@(|1D*6(K7qpbw21llHnbb-$Cow`7mo#SPUZ%PVSzo9$EJxuLdwq!AK7-q8LF1ups z(iIwe@;?E(wu!|aWhG!Gu$4(5PO288KALD?O_j8HW`uMB(_}4GSshP;GP;~RW=zED zw8F63==u&G8jo{;2;=AqmoCNLBSxaQxJ)ztH#N{D+m>x*cg$Xnm4KB%`Vt@@q(-=z z6tMP;gpPyI8%-KEw$a$m#7^S|jcwbuolI<}vF)6Be|O!p&YJ)C%%1)1FFzQ-mAhco z$=^Tc)cP-+!o&S!$S4g@UEl8nxYEhuAF;9TtoVlhkUg0eZYV{ar6_hzt}3CaHYC@i z7UprRT7X347e18#X)P))_h{4p-!(MhDqh0o3C5y2>*Rgt(yfndsXC^Y zRsn#WFo=;}T5D2)!J@e&F5A>3Wp%{H2c#FP^~gf{&y!dN=GG>+-0nO2NmWH71f@;z|Ig^|98foWqRf& zBqV>Qd?Dn=tcsd}0}VnQ;Yb$mP*N7A^v3bz4AFUqNhz>rLbP^Xb^mRC!CS;4)&wj_-MlKcxJt zYHMp#i7Lhaq}!G+y8N2cQwSPun?!;E700 zTjlQlBQ`czplXOQfns?E4}(1BLXvRu(0kmdwnTDB&sh~1N?r@8jH`D7rbQHb8t<{J zXrXE6J^e7Z^ws~Fi8`LdSnLtSvrzlWb!`ARx&f6Bw}CA>Cv!^k$|yeDg(V>$8pF)& z?tM_Mk}B#4EGA>-E4Y(~q#ZlezuCC`?^(`LALZvt$ z1^qLbp;elemL0dS+bf1XVnZ#$7R^8v96eerYJ<;=G)LJEE1UF}*D3;shU9<^taH#M zmTc2ovV{-xsr!eULVaHDa`mm#x~jBeqFs4c55-`xV8jyIs(FYp3`TBt=LPRwWaqWa zWfr)XNafV@pnNpyv;A{e_^{ValBojALY-%LX4XO z*$^jFro{*t_p;2Mr)mpCLp!uwYSmdE{vrUu6;5)oR-8>cCx*XXW}2oP5Y4n%#1jzp z8+MKyG7_ft2Zysd~V)l=!tASyzAVSzN#sKiE@ zORDT7gP&YU-73?mmjAy9X)}bz(zD>2g8&CwNJc#(z}K^Tm4^qSsU8}4NOk+ZD=I6u zTBTCmZ?PF@>fuwl$M8s!QJ<6>jiX}RL) zpgEQk9#4i6v(IPAME?Y0I4j4j2O9%YuYpyN(a5lw)H`+qO5tW)qwFiG}kR3_>Zkn%sDg zO&oy>QdFTnG~Z`O2p(GaE@|-ZO()4k>XN8|iJH5U^?dWckYpR@Z-m3y??PB{z#9H& zKoN$XNLq~uQsWVpl!ZTN{$mc!mYDd@#5~{a@?1$vsT&^D>QQ3l>Fh6mW*1dU^4h~? zv_G-|sW}c>jc_a zMqWx}YrbI4kg#*L89n-Hxf?Ia0<@WKH|kp17tDu|OSJ1qF8BmF%HuQWhECB+-`Byl z9+)lNqb9!8nvqF}1S%z!X`E6lBHno!D~Mv!1TkF{N6^gl^R@zjK8iY+SZV*hDBX-D zkhD!iQ(GAIPO(5R&?o#ZXXYnfdpwK8ukMBy4<0NbIUQpj5f}f*7LqH*14j&ogbtgy zLbknlj#m6tQB=E^gp0<#LT~AFi3+Px^uG}?wCNS7h&0)m_WO=1STF7HinLVg`NJ#8 z`!bxS5Cz>FhIs<8LFEZJw**V11VDoEL3!eJ0i5Jgal(>oz9iH_Ohg(=YG2=KeszD8 zl!x;OCp(EV6|z&vrlQZiD$U>wgN<;4*`tL3-QJJ5oy|(?F4H(qw{568ojfloUs;?k zJGa9*YRMI7iPCQ<=nLH)-W;iOtq<+c7=9D`yWlSz=9kEH;BZRAGF}P%b@l#joW%|-Zn$S=OScLw&WW|);-WNP*-o7_(tJKZS(D)mHvRFKJ+ALBqJzQx6Whv-`pl5INH+PZa#Cp3yKz`aFt{K1R?k%_P3O+OV;>Tz5OV{}s zGYsQyz*Iv&d^NAt-Ex&DWLr29} ziuvUQ&SQ@F4e;Nr@6I*rOmGDop&Gt5_1`b)MQc@0_B3c!=U0^>s}Ogg6Z<@o4#|6c zOV)Lv6rkKPdRu-S-EU(LQ8*Aa52mtdVZ=7|ZD=Z8&73j|U?Q|!`HyPX1i6TCR3dZm zZ?<>U!bxwCcjp?ieplJbQY}ZO@jrQkQM74>QEkgkk`5ejYbT)nGDv7X3;8ckV1*oI!2%0k zI0aX*NK`s+rjFtN`@^x1Zk)9`DHi{l0C6GP{DpF?%9E{3;$~9U7a{YVm-Q-W^LQ(x za7mG1aD7hiQy%8`?(7nBnn(j{no+@+m#{goChhglpobP3c)rIUOXscwxqi#U#9i+I zv(6j3Rkt&_LrJg5x9-^GDh=!j?`vEFr)}7d&#Ck$s}JT8*aI~0cbZDup1e95iI^@- zOoXlh^!Op?Du`7KOPhjbjoEJ5s`8Xq3S0Pa!nBA zYyOtfuJm;7m2y2FPot|9g1I0Z>g%bdTq5duzw-^)mP{ zJ2fc(8$Tpet4GVufljUPjqPvVk<yIk=vzztLyW2yyYi6iyQj| zamI-~VNU$S8W;ai)KU+Fd($1=N`q+t9ep@I8ltb!kU7vJvv!!N-(hgje76_oVxar7 z3&FHeZ6af2;C9a3=&aM&ajMmPXMQeP0<*K-r9#>J)8})j0=J&0S|YM*4;gXI*3HB$ z7*y30itypex#@A~13X(j=M(5NU0{e?UpOb4L}UzdVN+G5aZg z>h8jkA%kuUYHJr2R_nN!iy$E&fT1iN4R)df$`8Ga?vK_-X;SoWz9uD`&>{&w^0!Pl z^#vNHU&1WtAtMCv_Dt5jht9g+`XJj;1gXH85B8ZIr9gV4P=~sLK|6dm!U=e?9ILPP zgF7dHl)eZ=y`WY$GkdBuZw1Bo+oN)w0o16h=*kTao@4>{&}|(gbBg_%WM$<%1&!ifrNt7c z!d92lvY%kTD^F7Vh8V(iEE{>zO1Nf3^&LCl^CnC*=FE)b+$k(1dmVmx0&7POL4FN; z4z(&!`k+2jK69HKr*-Bo_D;A)`;%GKxT%2vOy56lq^?D?D1=-^1jdO8dyh@#YSEvl z<51e1jU`|Gi^ zdFEq;q4$A<;T10ng%wBZxm-IO9D zI7!8C)>fWj?t$@YY3taPU=^Yx?dP`@uwn#P_KBhY(t^F`)}3kgn=+cKK+1T{WOZEXykJF!eMBtpyU2J*Bz~|JLyML^u*rwk3sy z(_SFZ{|jMkc{n2TgnJ8(nqm0uJ_ zM6(ppPcJlZ{YD8PHOqZZlb+q{lbXY-e5Y}guvpC=& z;vdnV;vIT9xrus8&}fV*Q!FvA@c0_s)v@2`d^Ck*{J5M4CD<2g`>q9uqi4Oa9_6BQ z-tu3$Hc3GuOkFNuid$UuGO(pQTZs#*?W<8`E;URo`iGoas8~*ucff3H*Kp5tTK##lXfenzdm=fnC3n@g4K{-6$ z3iMt=P7#Pdl}dD*xK9Hz*Eg2%%sNF_3UV`can15AUfBHC@8?pYsCXr$e59k}HODj@ z)SO6CadL_xm`FDel>dSjV31c10n2X6gD^=n$;UXJ_v3g!H8;0&)YaP^oPxDx~Ww z0PeHI0chmb{!FE|w9!0_YDgele<=rgs5UArno(grQ(AQz5UTwP;%2z8~AT^3t!P?ktU+frmuEB_j`#A3IUU*bcvYwzy0qU047$@ zw|_TI1230c1o~pLlYi{bIC%0~w`#_Y+d};) z9glb2cjN9Vta{~?zFR<)^U@T@wV42`J?qodwH`o5 zCcle>mQrb6PVG$PN)aK!rO*gbr$$8&gxe4$xe zG6x-WGM|ecfe?HlRgnh4i(RGt!lIY+2URM4`7oDOI);v&K^C3ad3Ju1g-5kv;2)a4Z{{9VrFT*L?CDU!(o#~tbT+kFi22Dz}=#Zd* z?&c-1U{I-lQyo?Jfwdl-&))F#`^fqLz;M@4ueS%cYnpTCIKo`vVT|&Bf0C4#&9Hu> z@9r>3s?lVs2NkjDMaIL@)Npdl=O=lTOA(n{n4E~nmsOP}Dq*|>8Q>Vz`Llswag)Tw z&?36jN8(xo0Ab1CoK_>!qPBw8P0f21pRBJYMDR1t#q{eEsc~ia4IqmPZ*3m!sm07pjb@ zp1$_;44-`OC;3+sdo(ZX@Sw)LaQ__eW?^zCoit1aK z(SFW+G1UrtmKByJ5vQa7cPR6{qaYgoBPDAZEO&XmXogNejNV}^VBWhmt}v!dIY~xOMKY)sr^2)7-S; zv=L2y6&rVJKpR1mLzILS0X|{CMcABt4EOv(`F93KU@Akoev1Wdd0@dn$*2^R33#7E z^*1>=CXrqPEP52C6nTFV=O1t4BBf6AbihXhxev)VkWZGSLkrbI6+Tw54?p=+|DDlx zV&66%rif)FP`~tJlP?wzf=`jboytZoFsYCvAWQxizIkI%f6K}3D*j24qm!G)Z!qv1 zL}c{8aXO_fJEy*Um9lW9PV)uyk*44%wkA4A>YL?3ThW(Hw(h;D@67rtN>cC1w$N}u zqc@#CJKgV|3XO2lHN28KHd9B!63u)$=!4T=R89c$UowBN96CerRhk-+NdAzBM(3MK=FS_daDbgFVg%X)mrgoDQlRkSM z1b?Z2xk@w!-1eaNYB~R>30DFcL>X>7X6iWFvrPeV$!8`Bl)!qthe?@il#sxddXLQ^eN4nPVO(r%5a_ zqFPBo6*WYeH7(j$F!zd#SVgouEXp-12=+xbrLnW+UY`3pc|}Dbjh7-+i~~^8Wra#7 z_xf4s1X_Yqa>@}AelT!v09%QYi}fXBSx11LnhRz(tP|$l+@-Wxwhw*4uXIT5uRuXX zJw4G#meqr@9m+uZvfq~N{T9n5Rc%|ipDk?qDl$3cl}gp6$u0+2lb=esdVQ!fR1!-Q zpJ^=3_+dZQ^t3{!XtT6*bSe#INAdt@JsI(PlEd=nN<+~GYHFNiHxcDpJwK1Yy>lk# z(2%Jko)jaZEoX!La+$a9IAinnj3n_O*uxT$KH-FJ7CUK^5HqY~hLukP&kDBW`xvP~ z#-g_Cn40U-8wz|%2!5YCa91qD?}PURDsJ?I1*{}dgQf|-3m71ndOpuLsVx_h*0Laj zh|21t_q8HKKe1uv{XJxD_g|rDWPZ6?4mub6Wx0JrMceqVSq;5^-lHfeYJMtbiqLL^ zzsQfv{N+-Rqe#KTmO*U2$S%Q95($#@$Gz8`6y)wIuVn$aBonjw{y`=^bru7(5+f61)4R>R5M(^Snd{$&Bh$5oT$ zfIi)rfB6TyHX*zu#KooWi0x{j*jWw6725>us6|)DT@QTv39t`=aqx(1U@oA{v$19Gp*+F-iVwV$^5BlR3D#xVQ=usV}5Mr)FPkL1$P1FdiIbA_0fgpjdf zLWE>`x^SGAs3>i$?DE3~PyzN?8l*^3=8HJ7@~7NrNA6Mc


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 0353/1030] 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 0354/1030] 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 0355/1030] 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 0356/1030] 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 0357/1030] 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 0358/1030] 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 0359/1030] 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 0360/1030] 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 0361/1030] 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 0362/1030] 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 0363/1030] 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 92b611d76aabb35fefb3f34430a9679b5c9b28da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 09:58:17 +0200 Subject: [PATCH 0364/1030] Raise AttributeError instead of ImportError on missing attribute in 'openpype_interfaces' --- openpype/modules/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index b9ccec13cc..1bd343fd07 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -49,6 +49,7 @@ class _ModuleClass(object): Object of this class can be stored to `sys.modules` and used for storing dynamically imported modules. """ + def __init__(self, name): # Call setattr on super class super(_ModuleClass, self).__setattr__("name", name) @@ -116,12 +117,13 @@ class _InterfacesClass(_ModuleClass): - this is because interfaces must be available even if are missing implementation """ + def __getattr__(self, attr_name): if attr_name not in self.__attributes__: if attr_name in ("__path__", "__file__"): return None - raise ImportError(( + raise AttributeError(( "cannot import name '{}' from 'openpype_interfaces'" ).format(attr_name)) From 635164b00c6573396096c72268e71a4a062688ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:40:39 +0200 Subject: [PATCH 0365/1030] added HiddenCreator description --- website/docs/dev_publishing.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 8ee3b7e85f..c949fa8570 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -66,7 +66,7 @@ Another optional function is **get_current_context**. This function is handy in Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host. #### *BaseCreator* -Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants. +Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. @@ -92,7 +92,7 @@ def collect_instances(self): self._add_instance_to_context(instance) ``` -- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator** and **AutoCreator**. +- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator**, **HiddenCreator** and **AutoCreator**. - **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**. ```python @@ -199,6 +199,20 @@ class RenderLayerCreator(Creator): - **`get_dynamic_data`** (method) - Can be used to extend data for subset templates which may be required in some cases. +#### *HiddenCreator* +Creator which is not showed in UI so artist can't trigger it directly but is available for other creators. This creator is primarily meant for cases when creation should create different types of instances. For example during editorial publishing where input is single edl file but should create 2 or more kind of instances each with different family, attributes and abilities. Arguments for creation were limited to `instance_data` and `source_data`. Data of `instance_data` should follow what is sent to other creators and `source_data` can be used to send custom data defined by main creator. It is expected that `HiddenCreator` has specific main or "parent" creator. + +```python +def create(self, instance_data, source_data): + variant = instance_data["variant"] + task_name = instance_data["task"] + asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(self.project_name, asset_name) + self.get_subset_name( + variant, task_name, asset_doc, self.project_name, self.host_name) +``` + + #### *AutoCreator* Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting all creators. From a8d99a6f91d8fc44bda465a6a5f3f5425931eb75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:41:56 +0200 Subject: [PATCH 0366/1030] changed imports of 'attribute_deffinitions' --- website/docs/dev_publishing.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index c949fa8570..5266ece72c 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -172,11 +172,11 @@ class RenderLayerCreator(Creator): icon = "fa5.building" ``` -- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**. +- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.lib.attribute_definitions`. Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**. - **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**. ```python -from openpype.pipeline import attribute_definitions +from openpype.lib import attribute_definitions class RenderLayerCreator(Creator): @@ -311,7 +311,8 @@ class BulkRenderCreator(Creator): - **`pre_create_attr_defs`** (attr) - Attribute for default implementation of **get_pre_create_attr_defs**. ```python -from openpype.pipeline import Creator, attribute_definitions +from openpype.lib import attribute_definitions +from openpype.pipeline.create import Creator class CreateRender(Creator): @@ -484,10 +485,8 @@ Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_ ```python import pyblish.api -from openpype.pipeline import ( - OpenPypePyblishPluginMixin, - attribute_definitions, -) +from openpype.lib import attribute_definitions +from openpype.pipeline import OpenPypePyblishPluginMixin # Example context plugin From 6d093b92d9db498ab40dbc2b5bdc7a93f7581ebb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:42:12 +0200 Subject: [PATCH 0367/1030] changed queries and access to current session --- website/docs/dev_publishing.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 5266ece72c..f11a2c3047 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -248,14 +248,14 @@ def create(self): # - variant can be filled from settings variant = self._variant_name # Only place where we can look for current context - project_name = io.Session["AVALON_PROJECT"] - asset_name = io.Session["AVALON_ASSET"] - task_name = io.Session["AVALON_TASK"] - host_name = io.Session["AVALON_APP"] + project_name = self.project_name + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] # Create new instance if does not exist yet if existing_instance is None: - asset_doc = 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 ) @@ -278,7 +278,7 @@ def create(self): existing_instance["asset"] != asset_name or existing_instance["task"] != task_name ): - asset_doc = 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 ) From 2209bcf6b10f4e65a25e58b34373178aa8b92648 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 11:31:44 +0200 Subject: [PATCH 0368/1030] check for 'ILoadHost' to call different method on host --- openpype/pipeline/load/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 8b12088d3c..fe5102353d 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -7,6 +7,7 @@ import inspect import collections import numbers +from openpype.host import ILoadHost from openpype.client import ( get_project, get_assets, @@ -719,7 +720,11 @@ def get_outdated_containers(host=None, project_name=None): if project_name is None: project_name = legacy_io.active_project() - containers = host.ls() + + if isinstance(host, ILoadHost): + containers = host.get_containers() + else: + containers = host.ls() return filter_containers(containers, project_name).outdated From dc7856e919d3b7536c1bd5643d1b0e7ccbc8d059 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 11:56:20 +0200 Subject: [PATCH 0369/1030] 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 dfa6328d74fbbd806769d3689ccc1b2f85dc757e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 11:58:42 +0200 Subject: [PATCH 0370/1030] remove metadata from default environment values --- openpype/settings/defaults/system_settings/general.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index a06947ba77..909ffc1ee4 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -2,11 +2,7 @@ "studio_name": "Studio name", "studio_code": "stu", "admin_password": "", - "environment": { - "__environment_keys__": { - "global": [] - } - }, + "environment": {}, "log_to_server": true, "disk_mapping": { "windows": [], From 5d49d9c3d2876bddc4a1856b50e6ef94bf0c90d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Jul 2022 12:29:38 +0200 Subject: [PATCH 0371/1030] 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 0372/1030] 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 0373/1030] 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 0374/1030] 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 0375/1030] 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 78b4bbadc92ec29167af3487e1b597e07a40f35e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 11:39:22 +0200 Subject: [PATCH 0376/1030] add continuos arguments next to each other --- openpype/plugins/publish/extract_review_slate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 2edaf10e6b..28deb360be 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -295,12 +295,14 @@ class ExtractReviewSlate(openpype.api.Extractor): # this will reencode the output if input_audio: fmap = [ + "-filter_complex", "[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]", "-map", '[v]', "-map", '[a]' ] else: fmap = [ + "-filter_complex", "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", "-map", '[v]' ] @@ -308,7 +310,6 @@ class ExtractReviewSlate(openpype.api.Extractor): ffmpeg_path, "-i", slate_v_path, "-i", input_path, - "-filter_complex", ] concat_args.extend(fmap) if offset_timecode: From f99f811ddd4194caadd27a481aef766eae2e5727 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 11:39:37 +0200 Subject: [PATCH 0377/1030] add `-y` into base of ffmpeg arguments --- openpype/plugins/publish/extract_review_slate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 28deb360be..90dad00b97 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -308,6 +308,7 @@ class ExtractReviewSlate(openpype.api.Extractor): ] concat_args = [ ffmpeg_path, + "-y", "-i", slate_v_path, "-i", input_path, ] @@ -319,6 +320,7 @@ class ExtractReviewSlate(openpype.api.Extractor): # - keep format of output if format_args: concat_args.extend(format_args) + # Use arguments from ffmpeg preset source_ffmpeg_cmd = repre.get("ffmpeg_cmd") if source_ffmpeg_cmd: @@ -334,7 +336,7 @@ class ExtractReviewSlate(openpype.api.Extractor): concat_args.append(arg) # assumes arg has one parameter concat_args.append(args[indx + 1]) - concat_args.append("-y") + # add final output path concat_args.append(output_path) From c34a1270a29c6d660b1c7f40dcca259171b1a553 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 13:05:58 +0200 Subject: [PATCH 0378/1030] 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 0379/1030] 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 0380/1030] 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 0381/1030] 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 fcbf46d345bcef7363bc4f590d476136f478b6ce Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 22 Jul 2022 14:20:56 +0200 Subject: [PATCH 0382/1030] Add normaal animation --- website/src/css/custom.css | 4 ++-- website/src/pages/index.js | 16 ++++++++++------ website/static/img/logo_normaal.png | Bin 0 -> 13468 bytes 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 website/static/img/logo_normaal.png diff --git a/website/src/css/custom.css b/website/src/css/custom.css index e8dd86256b..58c9305bc7 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -196,12 +196,12 @@ html[data-theme='dark'] .header-github-link::before { padding: 20px } -.showcase .client { +.showcase .studio { display: flex; justify-content: space-between; } -.showcase .client img { +.showcase .studio img { max-height: 110px; padding: 20px; max-width: 160px; diff --git a/website/src/pages/index.js b/website/src/pages/index.js index ae7119e928..52302ec285 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -65,13 +65,17 @@ const collab = [ image: '/img/clothcat.png', infoLink: 'https://www.clothcatanimation.com/' }, { - title: 'Ellipse Studio', - image: '/img/ellipse-studio.png', - infoLink: 'http://www.dargaudmedia.com' + title: 'Ellipse Animation', + image: '/img/ellipse_animation.svg', + infoLink: 'http://www.ellipseanimation.com' }, { title: 'J Cube Inc', image: '/img/jcube_logo_bw.png', infoLink: 'https://j-cube.jp' + }, { + title: 'Normaal Animation', + image: '/img/logo_normaal.png', + infoLink: 'https://j-cube.jp' } ]; @@ -191,10 +195,10 @@ function Service({imageUrl, title, description}) { ); } -function Client({title, image, infoLink}) { +function Studio({title, image, infoLink}) { const imgUrl = useBaseUrl(image); return ( - + ); @@ -490,7 +494,7 @@ function Home() {

    Studios using openPype

    {studios.map((props, idx) => ( - + ))}
    diff --git a/website/static/img/logo_normaal.png b/website/static/img/logo_normaal.png new file mode 100644 index 0000000000000000000000000000000000000000..711847c9f2f95d77d46d4ab98a9287e5f0ef771d GIT binary patch literal 13468 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 mxrUV7^Rp0{u+ Date: Fri, 22 Jul 2022 14:21:06 +0200 Subject: [PATCH 0383/1030] update Ellipse animation after re-brand --- website/static/img/ellipse_animation.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 website/static/img/ellipse_animation.svg diff --git a/website/static/img/ellipse_animation.svg b/website/static/img/ellipse_animation.svg new file mode 100644 index 0000000000..c1caaa6726 --- /dev/null +++ b/website/static/img/ellipse_animation.svg @@ -0,0 +1,9 @@ + + + + + + + + + From abfe580eeed15293d929cce4170bb41862a33868 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 14:27:32 +0200 Subject: [PATCH 0384/1030] 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 69246a76b4d6e494c563c828bdfb203fd0e80c44 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 22 Jul 2022 15:01:31 +0200 Subject: [PATCH 0385/1030] fixing the host condition --- 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 cfaff4067b..1ddb694f85 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -229,7 +229,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): skip = False for item in self.skip_host_families: - if item["host"] != host_name: + if host_name not in item["host"]: continue families = set(item["families"]) From abc5c9e69b6cbdd0627d229e7e8294c159cef0e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 15:37:10 +0200 Subject: [PATCH 0386/1030] adding codec args for keeping continuity even wtih audio stream. --- openpype/plugins/publish/extract_review_slate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 90dad00b97..69043ee261 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -321,6 +321,9 @@ class ExtractReviewSlate(openpype.api.Extractor): if format_args: concat_args.extend(format_args) + if codec_args: + concat_args.extend(codec_args) + # Use arguments from ffmpeg preset source_ffmpeg_cmd = repre.get("ffmpeg_cmd") if source_ffmpeg_cmd: From f0ca08b4959dde095b5ae4599cdee76fd8ac86f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 16:30:50 +0200 Subject: [PATCH 0387/1030] 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 0388/1030] 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 4ac8da4ca047363005b1f0638c29584f847c8590 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 18:14:26 +0200 Subject: [PATCH 0389/1030] remove hosts filter on integrator plugins --- openpype/plugins/publish/integrate.py | 4 ---- openpype/plugins/publish/integrate_legacy.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 1ddb694f85..8532691e61 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -104,10 +104,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", - "hiero", "houdini", "nuke", "photoshop", "resolve", - "standalonepublisher", "traypublisher", "tvpaint", "unreal", - "webpublisher"] families = ["workfile", "pointcache", "camera", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 34e81a3839..b90b61f587 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -72,10 +72,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): label = "Integrate Asset (legacy)" # Make sure it happens after new integrator order = pyblish.api.IntegratorOrder + 0.00001 - hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", - "hiero", "houdini", "nuke", "photoshop", "resolve", - "standalonepublisher", "traypublisher", "tvpaint", "unreal", - "webpublisher"] families = ["workfile", "pointcache", "camera", From 5c0f0f260365423128e410712c18c1938e83777b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 22 Jul 2022 18:22:48 +0200 Subject: [PATCH 0390/1030] :heavy_minus_sign: remove invalid submodules --- vendor/powershell/BurntToast | 1 - vendor/powershell/PSWriteColor | 1 - 2 files changed, 2 deletions(-) delete mode 160000 vendor/powershell/BurntToast delete mode 160000 vendor/powershell/PSWriteColor diff --git a/vendor/powershell/BurntToast b/vendor/powershell/BurntToast deleted file mode 160000 index ae0acdd870..0000000000 --- a/vendor/powershell/BurntToast +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ae0acdd870a2fd8d9f0d147de22dc36d6c5e399e diff --git a/vendor/powershell/PSWriteColor b/vendor/powershell/PSWriteColor deleted file mode 160000 index 12eda384eb..0000000000 --- a/vendor/powershell/PSWriteColor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 12eda384ebd7a7954e15855e312215c009c97114 From e69d8e3ac65ee273e7d9a23c4bbd5f741baf01c9 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 23 Jul 2022 03:53:23 +0000 Subject: [PATCH 0391/1030] [Automated] Bump version --- CHANGELOG.md | 35 ++++++++++++++++++----------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8da885473..ec880b9c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ # Changelog -## [3.12.2-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...HEAD) +### 📖 Documentation + +- Update website with more studios [\#3554](https://github.com/pypeclub/OpenPype/pull/3554) +- Documentation: Update publishing dev docs [\#3549](https://github.com/pypeclub/OpenPype/pull/3549) + **🚀 Enhancements** +- 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) @@ -20,8 +26,15 @@ **🐛 Bug fixes** +- 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) +- 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) +- 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) @@ -31,8 +44,12 @@ **🔀 Refactored code** +- Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) - General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) +- 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) - TimersManager: Use query functions [\#3495](https://github.com/pypeclub/OpenPype/pull/3495) +- Deadline: Use query functions [\#3466](https://github.com/pypeclub/OpenPype/pull/3466) ## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13) @@ -57,7 +74,6 @@ - Windows installer: Clean old files and add version subfolder [\#3445](https://github.com/pypeclub/OpenPype/pull/3445) - Blender: Bugfix - Set fps properly on open [\#3426](https://github.com/pypeclub/OpenPype/pull/3426) - Hiero: Add custom scripts menu [\#3425](https://github.com/pypeclub/OpenPype/pull/3425) -- Blender: pre pyside install for all platforms [\#3400](https://github.com/pypeclub/OpenPype/pull/3400) **🐛 Bug fixes** @@ -95,34 +111,19 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) -### 📖 Documentation - -- Fix typo in documentation: pyenv on mac [\#3417](https://github.com/pypeclub/OpenPype/pull/3417) -- Linux: update OIIO package [\#3401](https://github.com/pypeclub/OpenPype/pull/3401) - **🚀 Enhancements** - Webserver: Added CORS middleware [\#3422](https://github.com/pypeclub/OpenPype/pull/3422) -- Attribute Defs UI: Files widget show what is allowed to drop in [\#3411](https://github.com/pypeclub/OpenPype/pull/3411) **🐛 Bug fixes** - NewPublisher: Fix subset name change on change of creator plugin [\#3420](https://github.com/pypeclub/OpenPype/pull/3420) - Bug: fix invalid avalon import [\#3418](https://github.com/pypeclub/OpenPype/pull/3418) -- Nuke: Fix keyword argument in query function [\#3414](https://github.com/pypeclub/OpenPype/pull/3414) -- Houdini: fix loading and updating vbd/bgeo sequences [\#3408](https://github.com/pypeclub/OpenPype/pull/3408) -- Nuke: Collect representation files based on Write [\#3407](https://github.com/pypeclub/OpenPype/pull/3407) -- General: Filter representations before integration start [\#3398](https://github.com/pypeclub/OpenPype/pull/3398) -- Maya: look collector typo [\#3392](https://github.com/pypeclub/OpenPype/pull/3392) **🔀 Refactored code** - Unreal: Use client query functions [\#3421](https://github.com/pypeclub/OpenPype/pull/3421) - General: Move editorial lib to pipeline [\#3419](https://github.com/pypeclub/OpenPype/pull/3419) -- Kitsu: renaming to plural func sync\_all\_projects [\#3397](https://github.com/pypeclub/OpenPype/pull/3397) -- Houdini: Use client query functions [\#3395](https://github.com/pypeclub/OpenPype/pull/3395) -- Hiero: Use client query functions [\#3393](https://github.com/pypeclub/OpenPype/pull/3393) -- Nuke: Use client query functions [\#3391](https://github.com/pypeclub/OpenPype/pull/3391) ## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20) diff --git a/openpype/version.py b/openpype/version.py index dd5ad97449..9dda1eacce 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.2-nightly.2" +__version__ = "3.12.2-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 9552242694..eebc8a5600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.2-nightly.2" # OpenPype +version = "3.12.2-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5e31967310836d0ef1ad3a1590fa1f3c7d9c682d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 10:53:07 +0200 Subject: [PATCH 0392/1030] implemented helper function to escape html symbols --- openpype/tools/utils/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index ea1362945f..2169cf8ef1 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -37,6 +37,19 @@ def center_window(window): window.move(geo.topLeft()) +def html_escape(text): + """Basic escape of html syntax symbols in text.""" + + return ( + text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("'", "'") + ) + + def set_style_property(widget, property_name, property_value): """Set widget's property that may affect style. From 307612d86878f3d098cc3d5c90f9d3464fb87b5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 10:53:24 +0200 Subject: [PATCH 0393/1030] replace implemented functions instead of 'html' --- openpype/tools/publisher/publish_report_viewer/model.py | 4 ++-- openpype/tools/publisher/widgets/card_view_widgets.py | 4 ++-- openpype/tools/publisher/widgets/list_view_widgets.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py index bd03376c55..704feeb4bd 100644 --- a/openpype/tools/publisher/publish_report_viewer/model.py +++ b/openpype/tools/publisher/publish_report_viewer/model.py @@ -1,9 +1,9 @@ import uuid -import html from Qt import QtCore, QtGui import pyblish.api +from openpype.tools.utils.lib import html_escape from .constants import ( ITEM_ID_ROLE, ITEM_IS_GROUP_ROLE, @@ -46,7 +46,7 @@ class InstancesModel(QtGui.QStandardItemModel): all_removed = True for instance_item in instance_items: item = QtGui.QStandardItem(instance_item.label) - instance_label = html.escape(instance_item.label) + instance_label = html_escape(instance_item.label) item.setData(instance_label, ITEM_LABEL_ROLE) item.setData(instance_item.errored, ITEM_ERRORED_ROLE) item.setData(instance_item.id, ITEM_ID_ROLE) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index bd591138f4..fa391f4ba0 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -22,13 +22,13 @@ Only one item can be selected at a time. import re import collections -import html from Qt import QtWidgets, QtCore from openpype.widgets.nice_checkbox import NiceCheckbox from openpype.tools.utils import BaseClickableFrame +from openpype.tools.utils.lib import html_escape from .widgets import ( AbstractInstanceView, ContextWarningLabel, @@ -308,7 +308,7 @@ class InstanceCardWidget(CardWidget): self._last_variant = variant self._last_subset_name = subset_name # Make `variant` bold - label = html.escape(self.instance.label) + label = html_escape(self.instance.label) found_parts = set(re.findall(variant, label, re.IGNORECASE)) if found_parts: for part in found_parts: diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 3e4fd5b72d..6e31ba635b 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -23,12 +23,12 @@ selection can be enabled disabled using checkbox or keyboard key presses: ``` """ import collections -import html from Qt import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors from openpype.widgets.nice_checkbox import NiceCheckbox +from openpype.tools.utils.lib import html_escape from .widgets import AbstractInstanceView from ..constants import ( INSTANCE_ID_ROLE, @@ -114,7 +114,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): self.instance = instance - instance_label = html.escape(instance.label) + instance_label = html_escape(instance.label) subset_name_label = QtWidgets.QLabel(instance_label, self) subset_name_label.setObjectName("ListViewSubsetName") @@ -181,7 +181,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): # Check subset name label = self.instance.label if label != self._instance_label_widget.text(): - self._instance_label_widget.setText(html.escape(label)) + self._instance_label_widget.setText(html_escape(label)) # Check active state self.set_active(self.instance["active"]) # Check valid states From c5258fb295df6c65d81a77a13063034a3b733db0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 12:28:43 +0200 Subject: [PATCH 0394/1030] nuke: refactory Missing frames validator --- .../publish/help/validate_rendered_frames.xml | 17 +++++++ .../publish/validate_rendered_frames.py | 48 ++++++++++++------- 2 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml b/openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml new file mode 100644 index 0000000000..434081c269 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml @@ -0,0 +1,17 @@ + + + + Rendered Frames + +## Missing Rendered Frames + +Render node "{node_name}" is set to "Use existing frames", but frames are missing. + +### How to repair? + +1. Use Repair button. +2. Set different target. +2. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index af5e8e9d27..f8e128cd26 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.api import ValidationException import clique +from openpype.pipeline import PublishXmlValidationError @pyblish.api.log @@ -36,7 +36,7 @@ class RepairActionBase(pyblish.api.Action): class RepairCollectionActionToLocal(RepairActionBase): - label = "Repair > rerender with `Local` machine" + label = "Repair - rerender with \"Local\"" def process(self, context, plugin): instances = self.get_instance(context, plugin) @@ -44,7 +44,7 @@ class RepairCollectionActionToLocal(RepairActionBase): class RepairCollectionActionToFarm(RepairActionBase): - label = "Repair > rerender `On farm` with remote machines" + label = "Repair - rerender with \"On farm\"" def process(self, context, plugin): instances = self.get_instance(context, plugin) @@ -63,6 +63,10 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): def process(self, instance): + f_data = { + "node_name": instance[0]["name"].value() + } + for repre in instance.data["representations"]: if not repre.get("files"): @@ -71,7 +75,8 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): "Check properties of write node (group) and" "select 'Local' option in 'Publish' dropdown.") self.log.error(msg) - raise ValidationException(msg) + raise PublishXmlValidationError( + self, msg, formatting_data=f_data) if isinstance(repre["files"], str): return @@ -82,30 +87,33 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): collection = collections[0] - fstartH = instance.data["frameStartHandle"] - fendH = instance.data["frameEndHandle"] + f_start_h = instance.data["frameStartHandle"] + f_end_h = instance.data["frameEndHandle"] - frame_length = int(fendH - fstartH + 1) + frame_length = int(f_end_h - f_start_h + 1) if frame_length != 1: if len(collections) != 1: msg = "There are multiple collections in the folder" self.log.error(msg) - raise ValidationException(msg) + raise PublishXmlValidationError( + self, msg, formatting_data=f_data) if not collection.is_contiguous(): msg = "Some frames appear to be missing" self.log.error(msg) - raise ValidationException(msg) + raise PublishXmlValidationError( + self, msg, formatting_data=f_data) - collected_frames_len = int(len(collection.indexes)) + collected_frames_len = len(collection.indexes) coll_start = min(collection.indexes) coll_end = max(collection.indexes) self.log.info("frame_length: {}".format(frame_length)) self.log.info("collected_frames_len: {}".format( collected_frames_len)) - self.log.info("fstartH-fendH: {}-{}".format(fstartH, fendH)) + self.log.info("f_start_h-f_end_h: {}-{}".format( + f_start_h, f_end_h)) self.log.info( "coll_start-coll_end: {}-{}".format(coll_start, coll_end)) @@ -116,13 +124,19 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): if ("slate" in instance.data["families"]) \ and (frame_length != collected_frames_len): collected_frames_len -= 1 - fstartH += 1 + f_start_h += 1 - assert ((collected_frames_len >= frame_length) - and (coll_start <= fstartH) - and (coll_end >= fendH)), ( - "{} missing frames. Use repair to render all frames" - ).format(__name__) + if ( + collected_frames_len >= frame_length + and coll_start <= f_start_h + and coll_end >= f_end_h + ): + raise PublishXmlValidationError( + self, ( + "{} missing frames. Use repair to " + "render all frames" + ).format(__name__), formatting_data=f_data + ) instance.data["collection"] = collection From 7905d18e6713b1307c2ea33fc3f4cb07577999b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:40:03 +0200 Subject: [PATCH 0395/1030] check if instance have representations as first thing --- openpype/plugins/publish/extract_thumbnail.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 7933595b89..789b6c75bc 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -29,7 +29,17 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ffmpeg_args = None def process(self, instance): - self.log.info("subset {}".format(instance.data['subset'])) + subset_name = instance.data["subset"] + instance_repres = instance.data.get("representations") + if not instance_repres: + self.log.debug(( + "Instance {} does not have representations. Skipping" + ).format(subset_name)) + return + + self.log.info( + "Processing instance with subset name {}".format(subset_name) + ) # skip crypto passes. # TODO: This is just a quick fix and has its own side-effects - it is From 89705a69d1df6ba69792290d56220e2f2bd317f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:43:10 +0200 Subject: [PATCH 0396/1030] move instance review key check earlier and move the logic to method --- openpype/plugins/publish/extract_thumbnail.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 789b6c75bc..839618bcdd 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -41,6 +41,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "Processing instance with subset name {}".format(subset_name) ) + # Skip if instance does not have review + if not self._is_review_instance(instance): + self.log.info("Skipping - no review set on instance.") + return + # skip crypto passes. # TODO: This is just a quick fix and has its own side-effects - it is # affecting every subset name with `crypto` in its name. @@ -51,11 +56,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.info("Skipping crypto passes.") return - # Skip if review not set. - if not instance.data.get("review", True): - self.log.info("Skipping - no review set on instance.") - return - if self._already_has_thumbnail(instance): self.log.info("Thumbnail representation already present.") return @@ -116,6 +116,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # There is no need to create more then one thumbnail break + def _is_review_instance(self, instance): + # TODO: We should probably handle "not creating" of thumbnail + # other way then checking for "review" key on instance data? + if instance.data.get("review", True): + return True + return False + def _already_has_thumbnail(self, instance): for repre in instance.data.get("representations", []): self.log.info("repre {}".format(repre)) From f82c97dc6aae62007de239c30fe8304b561a2a3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:45:44 +0200 Subject: [PATCH 0397/1030] check existing thumbnail before crypto check --- openpype/plugins/publish/extract_thumbnail.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 839618bcdd..51624a3cc7 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -41,11 +41,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "Processing instance with subset name {}".format(subset_name) ) - # Skip if instance does not have review + # Skip if instance have 'review' key in data set to 'False' if not self._is_review_instance(instance): self.log.info("Skipping - no review set on instance.") return + # Check if already has thumbnail created + if self._already_has_thumbnail(instance_repres): + self.log.info("Thumbnail representation already present.") + return + # skip crypto passes. # TODO: This is just a quick fix and has its own side-effects - it is # affecting every subset name with `crypto` in its name. @@ -56,9 +61,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.info("Skipping crypto passes.") return - if self._already_has_thumbnail(instance): - self.log.info("Thumbnail representation already present.") - return filtered_repres = self._get_filtered_repres(instance) for repre in filtered_repres: @@ -123,12 +125,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return True return False - def _already_has_thumbnail(self, instance): - for repre in instance.data.get("representations", []): + def _already_has_thumbnail(self, repres): + for repre in repres: self.log.info("repre {}".format(repre)) if repre["name"] == "thumbnail": return True - return False def _get_filtered_repres(self, instance): From 239414ffff9d9ec0e03c6b5239e7206317d5b9fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:49:52 +0200 Subject: [PATCH 0398/1030] try to create thumbnail from all filtered representations --- openpype/plugins/publish/extract_thumbnail.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 51624a3cc7..cb1af12586 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -63,6 +63,14 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): filtered_repres = self._get_filtered_repres(instance) + if not filtered_repres: + self.log.info(( + "Instance don't have representations" + " that can be used as source for thumbnail. Skipping" + )) + return + + thumbnail_created = False for repre in filtered_repres: repre_files = repre["files"] if not isinstance(repre_files, (list, tuple)): @@ -81,7 +89,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): jpeg_file = filename + "jpg" full_output_path = os.path.join(stagingdir, jpeg_file) - thumbnail_created = False # Try to use FFMPEG if OIIO is not supported (for cases when # oiiotool isn't available) if not is_oiio_supported(): @@ -96,10 +103,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.info("Converting with FFMPEG because input can't be read by OIIO.") # noqa thumbnail_created = self.create_thumbnail_ffmpeg(full_input_path, full_output_path) # noqa - # Skip the rest of the process if the thumbnail wasn't created + # Skip representation and try next one if wasn't created if not thumbnail_created: - self.log.warning("Thumbanil has not been created.") - return + continue new_repre = { "name": "thumbnail", @@ -118,6 +124,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # There is no need to create more then one thumbnail break + if not thumbnail_created: + self.log.warning("Thumbanil has not been created.") + def _is_review_instance(self, instance): # TODO: We should probably handle "not creating" of thumbnail # other way then checking for "review" key on instance data? From 6a018364b2961d21dab66657b12613838ba54750 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:53:27 +0200 Subject: [PATCH 0399/1030] create custom staging dir for thumbnail representation --- openpype/plugins/publish/extract_thumbnail.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index cb1af12586..2715aa4db4 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -1,4 +1,5 @@ import os +import tempfile import pyblish.api from openpype.lib import ( @@ -8,8 +9,6 @@ from openpype.lib import ( run_subprocess, path_to_subprocess_arg, - - execute, ) @@ -57,11 +56,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # This must be solved properly, maybe using tags on # representation that can be determined much earlier and # with better precision. - if 'crypto' in instance.data['subset'].lower(): + if "crypto" in subset_name.lower(): self.log.info("Skipping crypto passes.") return - filtered_repres = self._get_filtered_repres(instance) if not filtered_repres: self.log.info(( @@ -70,6 +68,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): )) return + # Create temp directory for thumbnail + # - this is to avoid "override" of source file + dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + self.log.debug( + "Create temp directory {} for thumbnail".formap(dst_staging) + ) + # Store new staging to cleanup paths + instance.context.data["cleanupFullPaths"].append(dst_staging) + thumbnail_created = False for repre in filtered_repres: repre_files = repre["files"] @@ -79,15 +86,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): file_index = int(float(len(repre_files)) * 0.5) input_file = repre_files[file_index] - stagingdir = os.path.normpath(repre["stagingDir"]) - - full_input_path = os.path.join(stagingdir, input_file) + src_staging = os.path.normpath(repre["stagingDir"]) + full_input_path = os.path.join(src_staging, input_file) self.log.info("input {}".format(full_input_path)) filename = os.path.splitext(input_file)[0] - if not filename.endswith('.'): - filename += "." - jpeg_file = filename + "jpg" - full_output_path = os.path.join(stagingdir, jpeg_file) + jpeg_file = filename + ".jpg" + full_output_path = os.path.join(dst_staging, jpeg_file) # Try to use FFMPEG if OIIO is not supported (for cases when # oiiotool isn't available) @@ -111,7 +115,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "name": "thumbnail", "ext": "jpg", "files": jpeg_file, - "stagingDir": stagingdir, + "stagingDir": dst_staging, "thumbnail": True, "tags": ["thumbnail"] } From d1987eed02ba4ca842cd820ef0947adbb4240d2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:53:58 +0200 Subject: [PATCH 0400/1030] removed unneeded f string --- openpype/plugins/publish/extract_thumbnail.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 2715aa4db4..d944c341e5 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -167,12 +167,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def create_thumbnail_oiio(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [oiio_tool_path, "-a", - src_path, "-o", - dst_path - ] - subprocess_exr = " ".join(oiio_cmd) - self.log.info(f"running: {subprocess_exr}") + oiio_cmd = [ + oiio_tool_path, + "-a", src_path, + "-o", dst_path + ] + self.log.info("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) return True From adc9b9303ba090dd1125f83b5d619306d230c5b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 12:55:29 +0200 Subject: [PATCH 0401/1030] reduced order of thumbnail creation to 2 conditions --- openpype/plugins/publish/extract_thumbnail.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index d944c341e5..c1eee71376 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -78,6 +78,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): instance.context.data["cleanupFullPaths"].append(dst_staging) thumbnail_created = False + oiio_supported = is_oiio_supported() for repre in filtered_repres: repre_files = repre["files"] if not isinstance(repre_files, (list, tuple)): @@ -93,19 +94,26 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): jpeg_file = filename + ".jpg" full_output_path = os.path.join(dst_staging, jpeg_file) - # Try to use FFMPEG if OIIO is not supported (for cases when - # oiiotool isn't available) - if not is_oiio_supported(): - thumbnail_created = self.create_thumbnail_ffmpeg(full_input_path, full_output_path) # noqa - else: + if oiio_supported: + self.log.info("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg - self.log.info("Trying to convert with OIIO") # noqa - thumbnail_created = self.create_thumbnail_oiio(full_input_path, full_output_path) # noqa + thumbnail_created = self.create_thumbnail_oiio( + full_input_path, full_output_path + ) - if not thumbnail_created: - self.log.info("Converting with FFMPEG because input can't be read by OIIO.") # noqa - thumbnail_created = self.create_thumbnail_ffmpeg(full_input_path, full_output_path) # noqa + # Try to use FFMPEG if OIIO is not supported or for cases when + # oiiotool isn't available + if not thumbnail_created: + if oiio_supported: + self.log.info(( + "Converting with FFMPEG because input" + " can't be read by OIIO." + )) + + thumbnail_created = self.create_thumbnail_ffmpeg( + full_input_path, full_output_path + ) # Skip representation and try next one if wasn't created if not thumbnail_created: From a96bfc45ad05c14267ef7d6cd968ab412cf00172 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Jul 2022 13:49:15 +0200 Subject: [PATCH 0402/1030] Fix - method expects dict not id --- openpype/hosts/harmony/plugins/load/load_background.py | 2 +- openpype/hosts/harmony/plugins/load/load_imagesequence.py | 2 +- openpype/hosts/harmony/plugins/load/load_template.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/plugins/load/load_background.py b/openpype/hosts/harmony/plugins/load/load_background.py index 9e9fcbfa32..c28a87791e 100644 --- a/openpype/hosts/harmony/plugins/load/load_background.py +++ b/openpype/hosts/harmony/plugins/load/load_background.py @@ -300,7 +300,7 @@ class BackgroundLoader(load.LoaderPlugin): print(container) - is_latest = is_representation_from_latest(representation["parent"]) + is_latest = is_representation_from_latest(representation) for layer in sorted(layers): file_to_import = [ os.path.join(bg_folder, layer).replace("\\", "/") diff --git a/openpype/hosts/harmony/plugins/load/load_imagesequence.py b/openpype/hosts/harmony/plugins/load/load_imagesequence.py index 8d6421a6aa..1b64aff595 100644 --- a/openpype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/openpype/hosts/harmony/plugins/load/load_imagesequence.py @@ -109,7 +109,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) # Colour node. - if is_representation_from_latest(representation["parent"]): + if is_representation_from_latest(representation): harmony.send( { "function": "PypeHarmony.setColor", diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index 8ddd3934f7..f3c69a9104 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -83,7 +83,7 @@ class TemplateLoader(load.LoaderPlugin): self_name = self.__class__.__name__ update_and_replace = False - if is_representation_from_latest(representation["parent"]): + if is_representation_from_latest(representation): self._set_green(node) else: self._set_red(node) From d7917b1950c6d5cd79e2b4d297de99d8a9ce2c82 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 25 Jul 2022 15:30:03 +0300 Subject: [PATCH 0403/1030] Handle locked attributes for playblast capture. --- openpype/vendor/python/common/capture.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 71b86a5f1a..86c1c60e56 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -665,7 +665,10 @@ def _applied_camera_options(options, panel): _iteritems = getattr(options, "iteritems", options.items) for opt, value in _iteritems(): - _safe_setAttr(camera + "." + opt, value) + if cmds.getAttr(camera + "." + opt, lock=True): + continue + else: + _safe_setAttr(camera + "." + opt, value) try: yield @@ -673,7 +676,11 @@ def _applied_camera_options(options, panel): if old_options: _iteritems = getattr(old_options, "iteritems", old_options.items) for opt, value in _iteritems(): - _safe_setAttr(camera + "." + opt, value) + # + if cmds.getAttr(camera + "." + opt, lock=True): + continue + else: + _safe_setAttr(camera + "." + opt, value) @contextlib.contextmanager From 649ddf19c9ca49b5d0838045626a47c41ac17767 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 14:40:53 +0200 Subject: [PATCH 0404/1030] query representation using query function --- openpype/plugins/publish/integrate.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8532691e61..597ed9844e 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -10,6 +10,9 @@ from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api import openpype.api +from openpype.client import ( + get_representations, +) from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io @@ -274,6 +277,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return filtered_repres def register(self, instance, file_transactions, filtered_repres): + project_name = legacy_io.active_project() + instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: self.log.info(( @@ -295,13 +300,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Get existing representations (if any) existing_repres_by_name = { - repres["name"].lower(): repres for repres in legacy_io.find( - { - "parent": version["_id"], - "type": "representation" - }, - # Only care about id and name of existing representations - projection={"_id": True, "name": True} + repre_doc["name"].lower(): repre_doc + for repre_doc in get_representations( + project_name, + version_ids=version["_id"], + fields=["_id", "name"] ) } From e7c937bdc086e214fc05a5344b516257ee11751a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 14:42:02 +0200 Subject: [PATCH 0405/1030] use query function to query subset document --- openpype/plugins/publish/integrate.py | 31 +++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 597ed9844e..5ac5680cfa 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -12,6 +12,7 @@ import pyblish.api import openpype.api from openpype.client import ( get_representations, + get_subset_by_name, ) from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction @@ -294,7 +295,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) - subset, subset_writes = self.prepare_subset(instance) + subset, subset_writes = self.prepare_subset(instance, project_name) version, version_writes = self.prepare_version(instance, subset) instance.data["versionEntity"] = version @@ -429,17 +430,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.info("Registered {} representations" "".format(len(prepared_representations))) - def prepare_subset(self, instance): - asset = instance.data.get("assetEntity") + def prepare_subset(self, instance, project_name): + asset_doc = instance.data.get("assetEntity") subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) # Get existing subset if it exists - subset = legacy_io.find_one({ - "type": "subset", - "parent": asset["_id"], - "name": subset_name - }) + subset_doc = get_subset_by_name( + project_name, subset_name, asset_doc["_id"] + ) # Define subset data data = { @@ -451,33 +450,33 @@ class IntegrateAsset(pyblish.api.InstancePlugin): data["subsetGroup"] = subset_group bulk_writes = [] - if subset is None: + if subset_doc is None: # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset = { + subset_doc = { "_id": ObjectId(), "schema": "openpype:subset-3.0", "type": "subset", "name": subset_name, "data": data, - "parent": asset["_id"] + "parent": asset_doc["_id"] } - bulk_writes.append(InsertOne(subset)) + bulk_writes.append(InsertOne(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["data"].update(data) + subset_doc["data"].update(data) bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset["_id"]}, + {"type": "subset", "_id": subset_doc["_id"]}, {"$set": { - "data": subset["data"] + "data": subset_doc["data"] }} )) self.log.info("Prepared subset: {}".format(subset_name)) - return subset, bulk_writes + return subset_doc, bulk_writes def prepare_version(self, instance, subset): From 1a5258b2fe7d0dd2ec642b3648ba2e17707105b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 14:43:08 +0200 Subject: [PATCH 0406/1030] use query function to get version document --- openpype/plugins/publish/integrate.py | 31 ++++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 5ac5680cfa..6236724228 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -13,6 +13,7 @@ import openpype.api from openpype.client import ( get_representations, get_subset_by_name, + get_version_by_name, ) from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction @@ -478,40 +479,40 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.info("Prepared subset: {}".format(subset_name)) return subset_doc, bulk_writes - def prepare_version(self, instance, subset): - + def prepare_version(self, instance, subset_doc, project_name): version_number = instance.data["version"] - version = { + version_doc = { "schema": "openpype:version-3.0", "type": "version", - "parent": subset["_id"], + "parent": subset_doc["_id"], "name": version_number, "data": self.create_version_data(instance) } - existing_version = legacy_io.find_one({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number - }, projection={"_id": True}) + existing_version = get_version_by_name( + project_name, + version_number, + subset_doc["_id"], + fields=["_id"] + ) if existing_version: self.log.debug("Updating existing version ...") - version["_id"] = existing_version["_id"] + version_doc["_id"] = existing_version["_id"] else: self.log.debug("Creating new version ...") - version["_id"] = ObjectId() + version_doc["_id"] = ObjectId() bulk_writes = [ReplaceOne( - filter={"_id": version["_id"]}, - replacement=version, + filter={"_id": version_doc["_id"]}, + replacement=version_doc, upsert=True )] - self.log.info("Prepared version: v{0:03d}".format(version["name"])) + self.log.info("Prepared version: v{0:03d}".format(version_doc["name"])) - return version, bulk_writes + return version_doc, bulk_writes def prepare_representation(self, repre, template_name, From e8bfbf4292979f54ac82114b967394392eefa0a1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Jul 2022 16:44:34 +0200 Subject: [PATCH 0407/1030] Added validator for old containers for AfterEffects --- openpype/plugins/publish/validate_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index 7732ec5ea9..b2a3ed9b79 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin): label = "Validate Containers" order = pyblish.api.ValidatorOrder - hosts = ["maya", "houdini", "nuke", "harmony", "photoshop"] + hosts = ["maya", "houdini", "nuke", "harmony", "photoshop", "aftereffects"] optional = True actions = [ShowInventory] From d3e982ebcf757a07d731760734ac51315454449b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 16:49:11 +0200 Subject: [PATCH 0408/1030] nuke: [wip] validate script attributes --- .../help/validate_script_attributes.xml | 18 ++ .../nuke/plugins/publish/validate_script.py | 156 ------------------ .../publish/validate_script_attributes.py | 129 +++++++++++++++ .../plugins/publish/validate_write_nodes.py | 2 - 4 files changed, 147 insertions(+), 158 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml delete mode 100644 openpype/hosts/nuke/plugins/publish/validate_script.py create mode 100644 openpype/hosts/nuke/plugins/publish/validate_script_attributes.py diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml b/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml new file mode 100644 index 0000000000..96f8ab5d38 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml @@ -0,0 +1,18 @@ + + + + Script attributes + +## Invalid Script attributes + +Following script root attributes need to be fixed: +{missing_attributes} + +### How to repair? + +1. Either use Repair or Select button. +2. If you chose Select then rename asset knob to correct name. +3. Hit Reload button on the publisher. + + + \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_script.py b/openpype/hosts/nuke/plugins/publish/validate_script.py deleted file mode 100644 index b8d7494b9d..0000000000 --- a/openpype/hosts/nuke/plugins/publish/validate_script.py +++ /dev/null @@ -1,156 +0,0 @@ -import pyblish.api - -from openpype.client import get_project, get_asset_by_id, get_asset_by_name -from openpype.pipeline import legacy_io - - -@pyblish.api.log -class ValidateScript(pyblish.api.InstancePlugin): - """ Validates file output. """ - - order = pyblish.api.ValidatorOrder + 0.1 - families = ["workfile"] - label = "Check script settings" - hosts = ["nuke"] - optional = True - - def process(self, instance): - ctx_data = instance.context.data - project_name = legacy_io.active_project() - asset_name = ctx_data["asset"] - # TODO repace query with using 'instance.data["assetEntity"]' - asset = get_asset_by_name(project_name, asset_name) - asset_data = asset["data"] - - # These attributes will be checked - attributes = [ - "fps", - "frameStart", - "frameEnd", - "resolutionWidth", - "resolutionHeight", - "handleStart", - "handleEnd" - ] - - # Value of these attributes can be found on parents - hierarchical_attributes = [ - "fps", - "resolutionWidth", - "resolutionHeight", - "pixelAspect", - "handleStart", - "handleEnd" - ] - - missing_attributes = [] - asset_attributes = {} - for attr in attributes: - if attr in asset_data: - asset_attributes[attr] = asset_data[attr] - - elif attr in hierarchical_attributes: - # TODO this should be probably removed - # Hierarchical attributes is not a thing since Pype 2? - - # Try to find attribute on parent - parent_id = asset['parent'] - parent_type = "project" - if asset_data['visualParent'] is not None: - parent_type = "asset" - parent_id = asset_data['visualParent'] - - value = self.check_parent_hierarchical( - project_name, parent_type, parent_id, attr - ) - if value is None: - missing_attributes.append(attr) - else: - asset_attributes[attr] = value - else: - missing_attributes.append(attr) - - # Raise error if attributes weren't found on asset in database - if len(missing_attributes) > 0: - atr = ", ".join(missing_attributes) - msg = 'Missing attributes "{}" in asset "{}"' - message = msg.format(atr, asset_name) - raise ValueError(message) - - # Get handles from database, Default is 0 (if not found) - handle_start = 0 - handle_end = 0 - if "handleStart" in asset_attributes: - handle_start = asset_attributes["handleStart"] - if "handleEnd" in asset_attributes: - handle_end = asset_attributes["handleEnd"] - - asset_attributes["fps"] = float("{0:.4f}".format( - asset_attributes["fps"])) - - # Get values from nukescript - script_attributes = { - "handleStart": ctx_data["handleStart"], - "handleEnd": ctx_data["handleEnd"], - "fps": float("{0:.4f}".format(ctx_data["fps"])), - "frameStart": ctx_data["frameStart"], - "frameEnd": ctx_data["frameEnd"], - "resolutionWidth": ctx_data["resolutionWidth"], - "resolutionHeight": ctx_data["resolutionHeight"], - "pixelAspect": ctx_data["pixelAspect"] - } - - # Compare asset's values Nukescript X Database - not_matching = [] - for attr in attributes: - self.log.debug("asset vs script attribute \"{}\": {}, {}".format( - attr, asset_attributes[attr], script_attributes[attr]) - ) - if asset_attributes[attr] != script_attributes[attr]: - not_matching.append(attr) - - # Raise error if not matching - if len(not_matching) > 0: - msg = "Attributes '{}' are not set correctly" - # Alert user that handles are set if Frame start/end not match - if ( - (("frameStart" in not_matching) or ("frameEnd" in not_matching)) and - ((handle_start > 0) or (handle_end > 0)) - ): - msg += " (`handle_start` are set to {})".format(handle_start) - msg += " (`handle_end` are set to {})".format(handle_end) - message = msg.format(", ".join(not_matching)) - raise ValueError(message) - - def check_parent_hierarchical( - self, project_name, parent_type, parent_id, attr - ): - if parent_id is None: - return None - - doc = None - if parent_type == "project": - doc = get_project(project_name) - elif parent_type == "asset": - doc = get_asset_by_id(project_name, parent_id) - - if not doc: - return None - - doc_data = doc["data"] - if attr in doc_data: - self.log.info(attr) - return doc_data[attr] - - if parent_type == "project": - return None - - parent_id = doc_data.get("visualParent") - new_parent_type = "asset" - if parent_id is None: - parent_id = doc["parent"] - new_parent_type = "project" - - return self.check_parent_hierarchical( - project_name, new_parent_type, parent_id, attr - ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py new file mode 100644 index 0000000000..2411c7fe4e --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -0,0 +1,129 @@ +from pprint import pformat +import pyblish.api + +from openpype.client import get_project, get_asset_by_id, get_asset_by_name +from openpype.pipeline import legacy_io +from openpype.pipeline import PublishXmlValidationError +import nuke + + +@pyblish.api.log +class ValidateScriptAttributes(pyblish.api.InstancePlugin): + """ Validates file output. """ + + order = pyblish.api.ValidatorOrder + 0.1 + families = ["workfile"] + label = "Validatte script attributes" + hosts = ["nuke"] + optional = True + + def process(self, instance): + ctx_data = instance.context.data + project_name = legacy_io.active_project() + asset_name = ctx_data["asset"] + asset = get_asset_by_name(project_name, asset_name) + asset_data = asset["data"] + + # These attributes will be checked + attributes = [ + "fps", + "frameStart", + "frameEnd", + "resolutionWidth", + "resolutionHeight", + "handleStart", + "handleEnd" + ] + + asset_attributes = { + attr: asset_data[attr] + for attr in attributes + if attr in asset_data + } + + self.log.debug(pformat( + asset_attributes + )) + + handle_start = asset_attributes["handleStart"] + handle_end = asset_attributes["handleEnd"] + asset_attributes["fps"] = float("{0:.4f}".format( + asset_attributes["fps"])) + + root = nuke.root() + # Get values from nukescript + script_attributes = { + "handleStart": ctx_data["handleStart"], + "handleEnd": ctx_data["handleEnd"], + "fps": float("{0:.4f}".format(ctx_data["fps"])), + "frameStart": int(root["first_frame"].getValue()), + "frameEnd": int(root["last_frame"].getValue()), + "resolutionWidth": ctx_data["resolutionWidth"], + "resolutionHeight": ctx_data["resolutionHeight"], + "pixelAspect": ctx_data["pixelAspect"] + } + self.log.debug(pformat( + script_attributes + )) + # Compare asset's values Nukescript X Database + not_matching = [] + for attr in attributes: + self.log.debug( + "Asset vs Script attribute \"{}\": {}, {}".format( + attr, + asset_attributes[attr], + script_attributes[attr] + ) + ) + if asset_attributes[attr] != script_attributes[attr]: + not_matching.append({ + "name": attr, + "expected": asset_attributes[attr], + "actual": script_attributes[attr] + }) + + # Raise error if not matching + if not_matching: + msg = "Attributes '{}' are not set correctly" + # Alert user that handles are set if Frame start/end not match + message = msg.format(", ".join( + [at["name"] for at in not_matching])) + raise PublishXmlValidationError( + self, message, + formatting_data={ + "missing_attributes": not_matching + } + ) + + def check_parent_hierarchical( + self, project_name, parent_type, parent_id, attr + ): + if parent_id is None: + return None + + doc = None + if parent_type == "project": + doc = get_project(project_name) + elif parent_type == "asset": + doc = get_asset_by_id(project_name, parent_id) + + if not doc: + return None + + doc_data = doc["data"] + if attr in doc_data: + self.log.info(attr) + return doc_data[attr] + + if parent_type == "project": + return None + + parent_id = doc_data.get("visualParent") + new_parent_type = "asset" + if parent_id is None: + parent_id = doc["parent"] + new_parent_type = "project" + + return self.check_parent_hierarchical( + project_name, new_parent_type, parent_id, attr + ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index f0a7f01dfb..48dce623a9 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,10 +1,8 @@ import pyblish.api from openpype.api import get_errored_instances_from_context -import openpype.hosts.nuke.api.lib as nlib from openpype.hosts.nuke.api.lib import ( get_write_node_template_attr, set_node_knobs_from_settings - ) from openpype.pipeline import PublishXmlValidationError From 441009fe5f8dbd5422b47624a23ef06ccac2af7d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 17:18:02 +0200 Subject: [PATCH 0409/1030] general: removing exclude family filtering from integrator --- openpype/plugins/publish/integrate.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8532691e61..06909f0ec3 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -156,7 +156,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] + default_template_name = "publish" # Representation context keys that should always be written to @@ -190,14 +190,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ).format(instance.data["family"])) return - # Exclude instances that also contain families from exclude families - families = set(get_instance_families(instance)) - exclude = families & set(self.exclude_families) - if exclude: - self.log.debug("Instance not integrated due to exclude " - "families found: {}".format(", ".join(exclude))) - return - file_transactions = FileTransaction(log=self.log) try: self.register(instance, file_transactions, filtered_repres) From 66d283ecdf0ac881624929fc76cbe48e1398b2d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 17:34:05 +0200 Subject: [PATCH 0410/1030] nuke: add `farm` key to instance data if render.farm --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 4b3b70fa12..b0da94c4ce 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -94,6 +94,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False + instance.data["farm"] = True families.append("{}.farm".format(family)) family = families_ak.lower() From 53e430ee50eaa9d33157573342c2ffb4020121b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 25 Jul 2022 17:49:18 +0200 Subject: [PATCH 0411/1030] :bug: fix active pane loss --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 ++ openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 233a0b60c2..54ef09e060 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -128,8 +128,10 @@ class ExtractPlayblast(openpype.api.Extractor): # Update preset with current panel setting # if override_viewport_options is turned off if not override_viewport_options: + panel = cmds.getPanel(with_focus=True) panel_preset = capture.parse_active_view() preset.update(panel_preset) + cmds.setFocus(panel) path = capture.capture(**preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 4f28aa167c..d1f43b61be 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -110,8 +110,10 @@ class ExtractThumbnail(openpype.api.Extractor): # Update preset with current panel setting # if override_viewport_options is turned off if not override_viewport_options: + panel = cmds.getPanel(with_focus=True) panel_preset = capture.parse_active_view() preset.update(panel_preset) + cmds.setFocus(panel) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From b4b4725b5790b5ffee3b258375ef1bbfe7d17d45 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 17:57:13 +0200 Subject: [PATCH 0412/1030] nuke: getting testing values from script dirrectly --- .../publish/validate_script_attributes.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index 2411c7fe4e..d9b9a35ece 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -4,6 +4,9 @@ import pyblish.api from openpype.client import get_project, get_asset_by_id, get_asset_by_name from openpype.pipeline import legacy_io from openpype.pipeline import PublishXmlValidationError +from openpype.hosts.nuke.api.lib import ( + get_avalon_knob_data +) import nuke @@ -45,22 +48,32 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): asset_attributes )) - handle_start = asset_attributes["handleStart"] - handle_end = asset_attributes["handleEnd"] asset_attributes["fps"] = float("{0:.4f}".format( asset_attributes["fps"])) root = nuke.root() + knob_data = get_avalon_knob_data(root) + + # Get frame range + first_frame = int(root["first_frame"].getValue()) + last_frame = int(root["last_frame"].getValue()) + + handle_start = int(knob_data["handleStart"]) + handle_end = int(knob_data["handleEnd"]) + + # Get format + _format = root["format"].value() + # Get values from nukescript script_attributes = { - "handleStart": ctx_data["handleStart"], - "handleEnd": ctx_data["handleEnd"], - "fps": float("{0:.4f}".format(ctx_data["fps"])), - "frameStart": int(root["first_frame"].getValue()), - "frameEnd": int(root["last_frame"].getValue()), - "resolutionWidth": ctx_data["resolutionWidth"], - "resolutionHeight": ctx_data["resolutionHeight"], - "pixelAspect": ctx_data["pixelAspect"] + "handleStart": handle_start, + "handleEnd": handle_end, + "fps": float("{0:.4f}".format(root['fps'].value())), + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "resolutionWidth": _format.width(), + "resolutionHeight": _format.height(), + "pixelAspect": _format.pixelAspect() } self.log.debug(pformat( script_attributes From 62a7b1f713e636d9ffbf66de6d33d9ea0c6c32a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 18:04:01 +0200 Subject: [PATCH 0413/1030] nuke: adding repair action to script attribute validator --- .../publish/validate_script_attributes.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index d9b9a35ece..605145149d 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -4,12 +4,27 @@ import pyblish.api from openpype.client import get_project, get_asset_by_id, get_asset_by_name from openpype.pipeline import legacy_io from openpype.pipeline import PublishXmlValidationError +from openpype.api import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( - get_avalon_knob_data + get_avalon_knob_data, + WorkfileSettings ) import nuke +@pyblish.api.log +class RepairScriptAttributes(pyblish.api.Action): + label = "Repair" + on = "failed" + icon = "wrench" + + def process(self, context, plugin): + instances = get_errored_instances_from_context(context) + + self.log.debug(instances) + WorkfileSettings().set_context_settings() + + @pyblish.api.log class ValidateScriptAttributes(pyblish.api.InstancePlugin): """ Validates file output. """ @@ -19,6 +34,7 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): label = "Validatte script attributes" hosts = ["nuke"] optional = True + actions = [RepairScriptAttributes] def process(self, instance): ctx_data = instance.context.data From ede691c3e90244fa46a388ad4280bf22b0c50d31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:20:57 +0200 Subject: [PATCH 0414/1030] fix missing arg --- openpype/plugins/publish/integrate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6236724228..0193d136c2 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -297,7 +297,9 @@ 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) + version, version_writes = self.prepare_version( + instance, subset, project_name + ) instance.data["versionEntity"] = version # Get existing representations (if any) From 6ae84ca5e6dec80990d13950d4226a7e66a99e66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:23:41 +0200 Subject: [PATCH 0415/1030] fix passed argument to get_representations --- 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 0193d136c2..8048ce3ab9 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -307,7 +307,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): repre_doc["name"].lower(): repre_doc for repre_doc in get_representations( project_name, - version_ids=version["_id"], + version_ids=[version["_id"]], fields=["_id", "name"] ) } From d4f96ae720c258c7ec6895d5398ee2a0c3e96812 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Jul 2022 18:30:23 +0200 Subject: [PATCH 0416/1030] 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 0417/1030] 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 0418/1030] 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 0419/1030] 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 0420/1030] 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 0421/1030] 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 907500c9e9354c47d945a1abd785f5520be26bc2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 21:26:13 +0200 Subject: [PATCH 0422/1030] nuke: validate script attributes finish --- .../help/validate_script_attributes.xml | 8 +- .../publish/validate_script_attributes.py | 107 ++++++++---------- 2 files changed, 49 insertions(+), 66 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml b/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml index 96f8ab5d38..871fc629ce 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml @@ -6,13 +6,13 @@ ## Invalid Script attributes Following script root attributes need to be fixed: -{missing_attributes} + +{failed_attributes} ### How to repair? -1. Either use Repair or Select button. -2. If you chose Select then rename asset knob to correct name. -3. Hit Reload button on the publisher. +1. Use Repair. +2. Hit Reload button on the publisher. \ No newline at end of file diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index 605145149d..ef89d71c5b 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -37,11 +37,18 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): actions = [RepairScriptAttributes] def process(self, instance): - ctx_data = instance.context.data + root = nuke.root() + knob_data = get_avalon_knob_data(root) project_name = legacy_io.active_project() - asset_name = ctx_data["asset"] - asset = get_asset_by_name(project_name, asset_name) - asset_data = asset["data"] + asset = get_asset_by_name( + project_name, + instance.context.data["asset"] + ) + # get asset data frame values + frame_start = asset["data"]["frameStart"] + frame_end = asset["data"]["frameEnd"] + handle_start = asset["data"]["handleStart"] + handle_end = asset["data"]["handleEnd"] # These attributes will be checked attributes = [ @@ -54,39 +61,36 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): "handleEnd" ] + # get only defined attributes from asset data asset_attributes = { - attr: asset_data[attr] + attr: asset["data"][attr] for attr in attributes - if attr in asset_data + if attr in asset["data"] } + # fix float to max 4 digints (only for evaluating) + fps_data = float("{0:.4f}".format( + asset_attributes["fps"])) + # fix frame values to include handles + asset_attributes.update({ + "frameStart": frame_start - handle_start, + "frameEnd": frame_end + handle_end, + "fps": fps_data + }) self.log.debug(pformat( asset_attributes )) - asset_attributes["fps"] = float("{0:.4f}".format( - asset_attributes["fps"])) - - root = nuke.root() - knob_data = get_avalon_knob_data(root) - - # Get frame range - first_frame = int(root["first_frame"].getValue()) - last_frame = int(root["last_frame"].getValue()) - - handle_start = int(knob_data["handleStart"]) - handle_end = int(knob_data["handleEnd"]) - # Get format _format = root["format"].value() # Get values from nukescript script_attributes = { - "handleStart": handle_start, - "handleEnd": handle_end, + "handleStart": int(knob_data["handleStart"]), + "handleEnd": int(knob_data["handleEnd"]), "fps": float("{0:.4f}".format(root['fps'].value())), - "frameStart": first_frame + handle_start, - "frameEnd": last_frame - handle_end, + "frameStart": int(root["first_frame"].getValue()), + "frameEnd": int(root["last_frame"].getValue()), "resolutionWidth": _format.width(), "resolutionHeight": _format.height(), "pixelAspect": _format.pixelAspect() @@ -94,6 +98,7 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): self.log.debug(pformat( script_attributes )) + # Compare asset's values Nukescript X Database not_matching = [] for attr in attributes: @@ -113,46 +118,24 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): # Raise error if not matching if not_matching: - msg = "Attributes '{}' are not set correctly" - # Alert user that handles are set if Frame start/end not match - message = msg.format(", ".join( - [at["name"] for at in not_matching])) + msg = "Following attributes are not set correctly: \n{}" + attrs_wrong_str = "\n".join([ + ( + "`{0}` is set to `{1}`, " + "but should be set to `{2}`" + ).format(at["name"], at["actual"], at["expected"]) + for at in not_matching + ]) + attrs_wrong_html = "
    ".join([ + ( + "-- __{0}__ is set to __{1}__, " + "but should be set to __{2}__" + ).format(at["name"], at["actual"], at["expected"]) + for at in not_matching + ]) raise PublishXmlValidationError( - self, message, + self, msg.format(attrs_wrong_str), formatting_data={ - "missing_attributes": not_matching + "failed_attributes": attrs_wrong_html } ) - - def check_parent_hierarchical( - self, project_name, parent_type, parent_id, attr - ): - if parent_id is None: - return None - - doc = None - if parent_type == "project": - doc = get_project(project_name) - elif parent_type == "asset": - doc = get_asset_by_id(project_name, parent_id) - - if not doc: - return None - - doc_data = doc["data"] - if attr in doc_data: - self.log.info(attr) - return doc_data[attr] - - if parent_type == "project": - return None - - parent_id = doc_data.get("visualParent") - new_parent_type = "asset" - if parent_id is None: - parent_id = doc["parent"] - new_parent_type = "project" - - return self.check_parent_hierarchical( - project_name, new_parent_type, parent_id, attr - ) From 06f338a95bf33644d84e365d01a3c4f6a68ac344 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 21:47:47 +0200 Subject: [PATCH 0423/1030] nuke: making validator code nicer --- .../publish/validate_script_attributes.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index ef89d71c5b..d16660f272 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -1,10 +1,10 @@ from pprint import pformat import pyblish.api -from openpype.client import get_project, get_asset_by_id, get_asset_by_name +import openpype.api +from openpype.client import get_asset_by_name from openpype.pipeline import legacy_io from openpype.pipeline import PublishXmlValidationError -from openpype.api import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( get_avalon_knob_data, WorkfileSettings @@ -12,19 +12,6 @@ from openpype.hosts.nuke.api.lib import ( import nuke -@pyblish.api.log -class RepairScriptAttributes(pyblish.api.Action): - label = "Repair" - on = "failed" - icon = "wrench" - - def process(self, context, plugin): - instances = get_errored_instances_from_context(context) - - self.log.debug(instances) - WorkfileSettings().set_context_settings() - - @pyblish.api.log class ValidateScriptAttributes(pyblish.api.InstancePlugin): """ Validates file output. """ @@ -34,7 +21,7 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): label = "Validatte script attributes" hosts = ["nuke"] optional = True - actions = [RepairScriptAttributes] + actions = [openpype.api.RepairAction] def process(self, instance): root = nuke.root() @@ -139,3 +126,8 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): "failed_attributes": attrs_wrong_html } ) + + @classmethod + def repair(cls, instance): + cls.log.debug("__ repairing instance: {}".format(instance)) + WorkfileSettings().set_context_settings() From d2e1fe84456feda9c3a8432d665715c5408c2d57 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Jul 2022 22:16:06 +0200 Subject: [PATCH 0424/1030] 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 84eb91acb7d4fe41a122543493372b7dea6012a0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Jul 2022 08:34:41 +0800 Subject: [PATCH 0425/1030] bugfix/OP-3356_Maya-Review-Image-plane-attribute --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 4f28aa167c..7885c1ebc9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -116,7 +116,11 @@ class ExtractThumbnail(openpype.api.Extractor): path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) - _, thumbnail = os.path.split(playblast) + image_plane = instance.data.get("imagePlane") + if image_plane: + _, thumbnail = os.path.split(playblast) + else: + return self.log.info("file list {}".format(thumbnail)) From 3755c5bf05d352de26647f05b5c2940d4022c30f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 10:45:35 +0200 Subject: [PATCH 0426/1030] 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 0427/1030] 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 0428/1030] 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 0429/1030] 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 0430/1030] 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 34601a6243dc39e09ab208a9e7e859a8e84e5d67 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Jul 2022 17:05:39 +0800 Subject: [PATCH 0431/1030] fix the bug of loading image plane --- .../maya/plugins/publish/extract_thumbnail.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 7885c1ebc9..47e9a907a0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -100,6 +100,13 @@ class ExtractThumbnail(openpype.api.Extractor): # camera. if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] + + #Show or Hide Image Plane + image_plane = instance.data.get("imagePlane", True) + if "viewport_options" in preset: + preset["viewport_options"]["imagePlane"] = image_plane + else: + preset["viewport_options"] = {"imagePlane": image_plane} with lib.maintained_time(): # Force viewer to False in call to capture because we have our own @@ -116,11 +123,8 @@ class ExtractThumbnail(openpype.api.Extractor): path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) - image_plane = instance.data.get("imagePlane") - if image_plane: - _, thumbnail = os.path.split(playblast) - else: - return + _, thumbnail = os.path.split(playblast) + self.log.info("file list {}".format(thumbnail)) From cd87b8ba2a5ae9a008123f999e958fe7c1562b54 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Jul 2022 17:11:07 +0800 Subject: [PATCH 0432/1030] bugfix-maya-review-image-plane_attribute --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 47e9a907a0..0d537810c0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -101,7 +101,7 @@ class ExtractThumbnail(openpype.api.Extractor): if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] - #Show or Hide Image Plane + # Show or Hide Image Plane image_plane = instance.data.get("imagePlane", True) if "viewport_options" in preset: preset["viewport_options"]["imagePlane"] = image_plane From 642d6ef407630ef2a9dad37551b6725569a7b4d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:15:48 +0200 Subject: [PATCH 0433/1030] fix typo --- openpype/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index c1eee71376..c154275322 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -72,7 +72,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # - this is to avoid "override" of source file dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") self.log.debug( - "Create temp directory {} for thumbnail".formap(dst_staging) + "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths instance.context.data["cleanupFullPaths"].append(dst_staging) From 2823cc2d1545adebca84c32aae9fb1e6f83db9d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:40:02 +0200 Subject: [PATCH 0434/1030] removed unused git progress --- openpype/lib/git_progress.py | 86 ------------------------------------ 1 file changed, 86 deletions(-) delete mode 100644 openpype/lib/git_progress.py diff --git a/openpype/lib/git_progress.py b/openpype/lib/git_progress.py deleted file mode 100644 index 331b7b6745..0000000000 --- a/openpype/lib/git_progress.py +++ /dev/null @@ -1,86 +0,0 @@ -import git -from tqdm import tqdm - - -class _GitProgress(git.remote.RemoteProgress): - """ Class handling displaying progress during git operations. - - This is using **tqdm** for showing progress bars. As **GitPython** - is parsing progress directly from git command, it is somehow unreliable - as in some operations it is difficult to get total count of iterations - to display meaningful progress bar. - - """ - _t = None - _code = 0 - _current_status = '' - _current_max = '' - - _description = { - 256: "Checking out files", - 4: "Counting objects", - 128: "Finding sources", - 32: "Receiving objects", - 64: "Resolving deltas", - 16: "Writing objects" - } - - def __init__(self): - super().__init__() - - def __del__(self): - if self._t is not None: - self._t.close() - - def _detroy_tqdm(self): - """ Used to close tqdm when operation ended. - - """ - if self._t is not None: - self._t.close() - self._t = None - - def _check_mask(self, opcode: int) -> bool: - """" Add meaningful description to **GitPython** opcodes. - - :param opcode: OP_MASK opcode - :type opcode: int - :return: String description of opcode - :rtype: str - - .. seealso:: For opcodes look at :class:`git.RemoteProgress` - - """ - if opcode & self.COUNTING: - return self._description.get(self.COUNTING) - elif opcode & self.CHECKING_OUT: - return self._description.get(self.CHECKING_OUT) - elif opcode & self.WRITING: - return self._description.get(self.WRITING) - elif opcode & self.RECEIVING: - return self._description.get(self.RECEIVING) - elif opcode & self.RESOLVING: - return self._description.get(self.RESOLVING) - elif opcode & self.FINDING_SOURCES: - return self._description.get(self.FINDING_SOURCES) - else: - return "Processing" - - def update(self, op_code, cur_count, max_count=None, message=''): - """ Called when git operation update progress. - - .. seealso:: For more details see - :func:`git.objects.submodule.base.Submodule.update` - `Documentation `_ - - """ - code = self._check_mask(op_code) - if self._current_status != code or self._current_max != max_count: - self._current_max = max_count - self._current_status = code - self._detroy_tqdm() - self._t = tqdm(total=max_count) - self._t.set_description(" . {}".format(code)) - - self._t.update(cur_count) From 297aaa6ee1a265119783b6f9355054d443e3af27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:41:15 +0200 Subject: [PATCH 0435/1030] removed unused function 'timeit' from log lib --- openpype/lib/__init__.py | 4 +--- openpype/lib/log.py | 22 ---------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index fb52a9aca7..c2fa9f0acb 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -63,7 +63,7 @@ from .execute import ( path_to_subprocess_arg, CREATE_NO_WINDOW ) -from .log import PypeLogger, timeit +from .log import PypeLogger from .path_templates import ( merge_dict, @@ -375,8 +375,6 @@ __all__ = [ "validate_mongo_connection", "OpenPypeMongoConnection", - "timeit", - "is_overlapping_otio_ranges", "otio_range_with_handles", "convert_to_padded_path", diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 2cdb7ec8e4..33d3f5c409 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -483,25 +483,3 @@ class PypeLogger: cls.initialize() return OpenPypeMongoConnection.get_mongo_client() - - -def timeit(method): - """Print time in function. - - For debugging. - - """ - log = logging.getLogger() - - def timed(*args, **kw): - ts = time.time() - result = method(*args, **kw) - te = time.time() - if 'log_time' in kw: - name = kw.get('log_name', method.__name__.upper()) - kw['log_time'][name] = int((te - ts) * 1000) - else: - log.debug('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) - print('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) - return result - return timed From 31a3911d4e0e756d5a3d62e957f771cd3a77aece Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:48:24 +0200 Subject: [PATCH 0436/1030] move functions from openpype.lib.config to openpype.lib.dateutils --- openpype/lib/__init__.py | 3 +- openpype/lib/config.py | 103 ++++++------------ openpype/lib/dateutils.py | 95 ++++++++++++++++ .../event_handlers_user/action_delivery.py | 4 +- openpype/plugins/load/delivery.py | 4 +- .../plugins/publish/collect_datetime_data.py | 4 +- 6 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 openpype/lib/dateutils.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index c2fa9f0acb..2d99efbe28 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -83,8 +83,9 @@ from .anatomy import ( Anatomy ) -from .config import ( +from .dateutils import ( get_datetime_data, + get_timestamp, get_formatted_current_time ) diff --git a/openpype/lib/config.py b/openpype/lib/config.py index 57e8efa57d..26822649e4 100644 --- a/openpype/lib/config.py +++ b/openpype/lib/config.py @@ -1,82 +1,41 @@ -# -*- coding: utf-8 -*- -"""Get configuration data.""" -import datetime +import warnings +import functools -def get_datetime_data(datetime_obj=None): - """Returns current datetime data as dictionary. +class ConfigDeprecatedWarning(DeprecationWarning): + pass - Args: - datetime_obj (datetime): Specific datetime object - Returns: - dict: prepared date & time data +def deprecated(func): + """Mark functions as deprecated. - Available keys: - "d" - in shortest possible way. - "dd" - with 2 digits. - "ddd" - shortened week day. e.g.: `Mon`, ... - "dddd" - full name of week day. e.g.: `Monday`, ... - "m" - in shortest possible way. e.g.: `1` if January - "mm" - with 2 digits. - "mmm" - shortened month name. e.g.: `Jan`, ... - "mmmm" - full month name. e.g.: `January`, ... - "yy" - shortened year. e.g.: `19`, `20`, ... - "yyyy" - full year. e.g.: `2019`, `2020`, ... - "H" - shortened hours. - "HH" - with 2 digits. - "h" - shortened hours. - "hh" - with 2 digits. - "ht" - AM or PM. - "M" - shortened minutes. - "MM" - with 2 digits. - "S" - shortened seconds. - "SS" - with 2 digits. + It will result in a warning being emitted when the function is used. """ - if not datetime_obj: - datetime_obj = datetime.datetime.now() - - year = datetime_obj.strftime("%Y") - - month = datetime_obj.strftime("%m") - month_name_full = datetime_obj.strftime("%B") - month_name_short = datetime_obj.strftime("%b") - day = datetime_obj.strftime("%d") - - weekday_full = datetime_obj.strftime("%A") - weekday_short = datetime_obj.strftime("%a") - - hours = datetime_obj.strftime("%H") - hours_midday = datetime_obj.strftime("%I") - hour_midday_type = datetime_obj.strftime("%p") - minutes = datetime_obj.strftime("%M") - seconds = datetime_obj.strftime("%S") - - return { - "d": str(int(day)), - "dd": str(day), - "ddd": weekday_short, - "dddd": weekday_full, - "m": str(int(month)), - "mm": str(month), - "mmm": month_name_short, - "mmmm": month_name_full, - "yy": str(year[2:]), - "yyyy": str(year), - "H": str(int(hours)), - "HH": str(hours), - "h": str(int(hours_midday)), - "hh": str(hours_midday), - "ht": hour_midday_type, - "M": str(int(minutes)), - "MM": str(minutes), - "S": str(int(seconds)), - "SS": str(seconds), - } + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.simplefilter("always", ConfigDeprecatedWarning) + warnings.warn( + ( + "Deprecated import of function '{}'." + " Class was moved to 'openpype.lib.dateutils.{}'." + " Please change your imports." + ).format(func.__name__), + category=ConfigDeprecatedWarning + ) + return func(*args, **kwargs) + return new_func +@deprecated +def get_datetime_data(datetime_obj=None): + from .dateutils import get_datetime_data + + return get_datetime_data(datetime_obj) + + +@deprecated def get_formatted_current_time(): - return datetime.datetime.now().strftime( - "%Y%m%dT%H%M%SZ" - ) + from .dateutils import get_formatted_current_time + + return get_formatted_current_time() diff --git a/openpype/lib/dateutils.py b/openpype/lib/dateutils.py new file mode 100644 index 0000000000..68cd1d1c5b --- /dev/null +++ b/openpype/lib/dateutils.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +"""Get configuration data.""" +import datetime + + +def get_datetime_data(datetime_obj=None): + """Returns current datetime data as dictionary. + + Args: + datetime_obj (datetime): Specific datetime object + + Returns: + dict: prepared date & time data + + Available keys: + "d" - in shortest possible way. + "dd" - with 2 digits. + "ddd" - shortened week day. e.g.: `Mon`, ... + "dddd" - full name of week day. e.g.: `Monday`, ... + "m" - in shortest possible way. e.g.: `1` if January + "mm" - with 2 digits. + "mmm" - shortened month name. e.g.: `Jan`, ... + "mmmm" - full month name. e.g.: `January`, ... + "yy" - shortened year. e.g.: `19`, `20`, ... + "yyyy" - full year. e.g.: `2019`, `2020`, ... + "H" - shortened hours. + "HH" - with 2 digits. + "h" - shortened hours. + "hh" - with 2 digits. + "ht" - AM or PM. + "M" - shortened minutes. + "MM" - with 2 digits. + "S" - shortened seconds. + "SS" - with 2 digits. + """ + + if not datetime_obj: + datetime_obj = datetime.datetime.now() + + year = datetime_obj.strftime("%Y") + + month = datetime_obj.strftime("%m") + month_name_full = datetime_obj.strftime("%B") + month_name_short = datetime_obj.strftime("%b") + day = datetime_obj.strftime("%d") + + weekday_full = datetime_obj.strftime("%A") + weekday_short = datetime_obj.strftime("%a") + + hours = datetime_obj.strftime("%H") + hours_midday = datetime_obj.strftime("%I") + hour_midday_type = datetime_obj.strftime("%p") + minutes = datetime_obj.strftime("%M") + seconds = datetime_obj.strftime("%S") + + return { + "d": str(int(day)), + "dd": str(day), + "ddd": weekday_short, + "dddd": weekday_full, + "m": str(int(month)), + "mm": str(month), + "mmm": month_name_short, + "mmmm": month_name_full, + "yy": str(year[2:]), + "yyyy": str(year), + "H": str(int(hours)), + "HH": str(hours), + "h": str(int(hours_midday)), + "hh": str(hours_midday), + "ht": hour_midday_type, + "M": str(int(minutes)), + "MM": str(minutes), + "S": str(int(seconds)), + "SS": str(seconds), + } + + +def get_timestamp(datetime_obj=None): + """Get standardized timestamp from datetime object. + + Args: + datetime_obj (datetime.datetime): Object of datetime. Current time + is used if not passed. + """ + + if datetime_obj is None: + datetime_obj = datetime.datetime.now() + return datetime_obj.strftime( + "%Y%m%dT%H%M%SZ" + ) + + +def get_formatted_current_time(): + return get_timestamp() diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index ad82af39a3..eec245070c 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -16,7 +16,7 @@ from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from openpype_modules.ftrack.lib.custom_attributes import ( query_custom_attributes ) -from openpype.lib import config +from openpype.lib.dateutils import get_datetime_data from openpype.lib.delivery import ( path_from_representation, get_format_dict, @@ -555,7 +555,7 @@ class Delivery(BaseAction): format_dict = get_format_dict(anatomy, location_path) - datetime_data = config.get_datetime_data() + datetime_data = get_datetime_data() for repre in repres_to_deliver: source_path = repre.get("data", {}).get("path") debug_msg = "Processing representation {}".format(repre["_id"]) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 7585ea4c59..f6e1d4f06b 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -4,10 +4,10 @@ from collections import defaultdict from Qt import QtWidgets, QtCore, QtGui from openpype.client import get_representations -from openpype.lib import config from openpype.pipeline import load, Anatomy from openpype import resources, style +from openpype.lib.dateutils import get_datetime_data from openpype.lib.delivery import ( sizeof_fmt, path_from_representation, @@ -160,7 +160,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): selected_repres = self._get_selected_repres() - datetime_data = config.get_datetime_data() + datetime_data = get_datetime_data() template_name = self.dropdown.currentText() format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) for repre in self._representations: diff --git a/openpype/plugins/publish/collect_datetime_data.py b/openpype/plugins/publish/collect_datetime_data.py index 1675ae1a98..f46d616fb3 100644 --- a/openpype/plugins/publish/collect_datetime_data.py +++ b/openpype/plugins/publish/collect_datetime_data.py @@ -5,7 +5,7 @@ Provides: """ import pyblish.api -from openpype.api import config +from openpype.lib.dateutils import get_datetime_data class CollectDateTimeData(pyblish.api.ContextPlugin): @@ -15,4 +15,4 @@ class CollectDateTimeData(pyblish.api.ContextPlugin): def process(self, context): key = "datetimeData" if key not in context.data: - context.data[key] = config.get_datetime_data() + context.data[key] = get_datetime_data() From 09001afa223baacd6748e0a44a6823199a289300 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:57:36 +0200 Subject: [PATCH 0437/1030] reduced 'Pype' from class names in logger --- openpype/lib/log.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 33d3f5c409..aaacb7b004 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -41,13 +41,13 @@ except ImportError: USE_UNICODE = hasattr(__builtins__, "unicode") -class PypeStreamHandler(logging.StreamHandler): +class LogStreamHandler(logging.StreamHandler): """ StreamHandler class designed to handle utf errors in python 2.x hosts. """ def __init__(self, stream=None): - super(PypeStreamHandler, self).__init__(stream) + super(LogStreamHandler, self).__init__(stream) self.enabled = True def enable(self): @@ -56,7 +56,6 @@ class PypeStreamHandler(logging.StreamHandler): Used to silence output """ self.enabled = True - pass def disable(self): """ Disable StreamHandler @@ -107,13 +106,13 @@ class PypeStreamHandler(logging.StreamHandler): self.handleError(record) -class PypeFormatter(logging.Formatter): +class LogFormatter(logging.Formatter): DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ]' default_formatter = logging.Formatter(DFT) def __init__(self, formats): - super(PypeFormatter, self).__init__() + super(LogFormatter, self).__init__() self.formatters = {} for loglevel in formats: self.formatters[loglevel] = logging.Formatter(formats[loglevel]) @@ -141,7 +140,7 @@ class PypeFormatter(logging.Formatter): return out -class PypeMongoFormatter(logging.Formatter): +class MongoFormatter(logging.Formatter): DEFAULT_PROPERTIES = logging.LogRecord( '', '', '', '', '', '', '', '').__dict__.keys() @@ -239,7 +238,7 @@ class PypeLogger: for handler in logger.handlers: if isinstance(handler, MongoHandler): add_mongo_handler = False - elif isinstance(handler, PypeStreamHandler): + elif isinstance(handler, LogStreamHandler): add_console_handler = False if add_console_handler: @@ -292,7 +291,7 @@ class PypeLogger: "username": components["username"], "password": components["password"], "capped": True, - "formatter": PypeMongoFormatter() + "formatter": MongoFormatter() } if components["port"] is not None: kwargs["port"] = int(components["port"]) @@ -303,10 +302,10 @@ class PypeLogger: @classmethod def _get_console_handler(cls): - formatter = PypeFormatter(cls.FORMAT_FILE) - console_handler = PypeStreamHandler() + formatter = LogFormatter(cls.FORMAT_FILE) + console_handler = LogStreamHandler() - console_handler.set_name("PypeStreamHandler") + console_handler.set_name("LogStreamHandler") console_handler.setFormatter(formatter) return console_handler @@ -417,9 +416,9 @@ class PypeLogger: def get_process_name(cls): """Process name that is like "label" of a process. - Pype's logging can be used from pype itseld of from hosts. Even in Pype - it's good to know if logs are from Pype tray or from pype's event - server. This should help to identify that information. + OpenPype's logging can be used from OpenPyppe itself of from hosts. + Even in OpenPype process it's good to know if logs are from tray or + from other cli commands. This should help to identify that information. """ if cls._process_name is not None: return cls._process_name From 14224407261d89b19424b4ac3c6608b10796cb01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Jul 2022 11:58:35 +0200 Subject: [PATCH 0438/1030] make main class 'Logger' and keep 'PypeLogger' with commented deprecation log --- openpype/api.py | 3 +-- openpype/lib/__init__.py | 7 ++++++- openpype/lib/log.py | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index fac2ae572b..c2227c1a52 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -9,6 +9,7 @@ from .settings import ( ) from .lib import ( PypeLogger, + Logger, Anatomy, config, execute, @@ -58,8 +59,6 @@ from .action import ( RepairContextAction ) -# for backward compatibility with Pype 2 -Logger = PypeLogger __all__ = [ "get_system_settings", diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 2d99efbe28..31cd5e7510 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -63,7 +63,10 @@ from .execute import ( path_to_subprocess_arg, CREATE_NO_WINDOW ) -from .log import PypeLogger +from .log import ( + Logger, + PypeLogger, +) from .path_templates import ( merge_dict, @@ -371,7 +374,9 @@ __all__ = [ "get_datetime_data", "get_formatted_current_time", + "Logger", "PypeLogger", + "get_default_components", "validate_mongo_connection", "OpenPypeMongoConnection", diff --git a/openpype/lib/log.py b/openpype/lib/log.py index aaacb7b004..dc030a6430 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -160,7 +160,7 @@ class MongoFormatter(logging.Formatter): 'method': record.funcName, 'lineNumber': record.lineno } - document.update(PypeLogger.get_process_data()) + document.update(Logger.get_process_data()) # Standard document decorated with exception info if record.exc_info is not None: @@ -180,7 +180,7 @@ class MongoFormatter(logging.Formatter): return document -class PypeLogger: +class Logger: DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ] ' DBG = " - { %(name)s }: [ %(message)s ] " INF = ">>> [ %(message)s ] " @@ -482,3 +482,15 @@ class PypeLogger: cls.initialize() return OpenPypeMongoConnection.get_mongo_client() + + +class PypeLogger(Logger): + @classmethod + def get_logger(cls, *args, **kwargs): + logger = Logger.get_logger(*args, **kwargs) + # TODO uncomment when replaced most of places + # logger.warning(( + # "'openpype.lib.PypeLogger' is deprecated class." + # " Please use 'openpype.lib.Logger' instead." + # )) + return logger From 2657ff27f186bdcf8098f8f7878947fc36bec1f5 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 13:32:00 +0300 Subject: [PATCH 0439/1030] 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 0440/1030] 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 0441/1030] 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 0442/1030] 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 0443/1030] 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 0444/1030] 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 0445/1030] 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 0446/1030] 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 0447/1030] 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 0448/1030] 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 0449/1030] 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 0450/1030] 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 0451/1030] 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 0452/1030] 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 0453/1030] 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 0454/1030] 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 0455/1030] 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 0456/1030] 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 0457/1030] 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 0458/1030] 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 0459/1030] 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 0460/1030] 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 0461/1030] 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 955423c9eb379cd9f90662672d8b91f3c96bb85a Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 27 Jul 2022 04:03:52 +0000 Subject: [PATCH 0462/1030] [Automated] Bump version --- CHANGELOG.md | 38 ++++++++++++-------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec880b9c61..133be18f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.12.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.2-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...HEAD) @@ -11,21 +11,22 @@ **🚀 Enhancements** +- 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) - Ftrack: add source into Note [\#3509](https://github.com/pypeclub/OpenPype/pull/3509) -- Ftrack: Trigger custom ftrack topic of project structure creation [\#3506](https://github.com/pypeclub/OpenPype/pull/3506) -- Settings UI: Add extract to file action on project view [\#3505](https://github.com/pypeclub/OpenPype/pull/3505) - Add pack and unpack convenience scripts [\#3502](https://github.com/pypeclub/OpenPype/pull/3502) - General: Event system [\#3499](https://github.com/pypeclub/OpenPype/pull/3499) - 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) -- Migrate basic families to the new Tray Publisher [\#3469](https://github.com/pypeclub/OpenPype/pull/3469) **🐛 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) - General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) - Settings: Clean default values of environments [\#3550](https://github.com/pypeclub/OpenPype/pull/3550) @@ -44,13 +45,19 @@ **🔀 Refactored code** +- 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) -- TimersManager: Use query functions [\#3495](https://github.com/pypeclub/OpenPype/pull/3495) - Deadline: Use query functions [\#3466](https://github.com/pypeclub/OpenPype/pull/3466) +**Merged pull requests:** + +- Maya: fix active pane loss [\#3566](https://github.com/pypeclub/OpenPype/pull/3566) + ## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.1-nightly.6...3.12.1) @@ -59,10 +66,6 @@ - Docs: Added minimal permissions for MongoDB [\#3441](https://github.com/pypeclub/OpenPype/pull/3441) -**🆕 New features** - -- Maya: Add VDB to Arnold loader [\#3433](https://github.com/pypeclub/OpenPype/pull/3433) - **🚀 Enhancements** - TrayPublisher: Added more options for grouping of instances [\#3494](https://github.com/pypeclub/OpenPype/pull/3494) @@ -72,8 +75,6 @@ - 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) -- Blender: Bugfix - Set fps properly on open [\#3426](https://github.com/pypeclub/OpenPype/pull/3426) -- Hiero: Add custom scripts menu [\#3425](https://github.com/pypeclub/OpenPype/pull/3425) **🐛 Bug fixes** @@ -92,7 +93,6 @@ - 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) -- Nuke: Slate frame is integrated [\#3427](https://github.com/pypeclub/OpenPype/pull/3427) **🔀 Refactored code** @@ -111,20 +111,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) -**🚀 Enhancements** - -- Webserver: Added CORS middleware [\#3422](https://github.com/pypeclub/OpenPype/pull/3422) - -**🐛 Bug fixes** - -- NewPublisher: Fix subset name change on change of creator plugin [\#3420](https://github.com/pypeclub/OpenPype/pull/3420) -- Bug: fix invalid avalon import [\#3418](https://github.com/pypeclub/OpenPype/pull/3418) - -**🔀 Refactored code** - -- Unreal: Use client query functions [\#3421](https://github.com/pypeclub/OpenPype/pull/3421) -- General: Move editorial lib to pipeline [\#3419](https://github.com/pypeclub/OpenPype/pull/3419) - ## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.11.1-nightly.1...3.11.1) diff --git a/openpype/version.py b/openpype/version.py index 9dda1eacce..9388d4219e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.2-nightly.3" +__version__ = "3.12.2-nightly.4" diff --git a/pyproject.toml b/pyproject.toml index eebc8a5600..0a9c02834a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.2-nightly.3" # OpenPype +version = "3.12.2-nightly.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 078bcb3b8e027948b8951920821e63392378c787 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 27 Jul 2022 07:58:18 +0000 Subject: [PATCH 0463/1030] [Automated] Release --- CHANGELOG.md | 5 ++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 133be18f68..e4fc1d59ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.12.2-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [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...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...3.12.2) ### 📖 Documentation @@ -17,7 +17,6 @@ - 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) -- General: Event system [\#3499](https://github.com/pypeclub/OpenPype/pull/3499) - 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) diff --git a/openpype/version.py b/openpype/version.py index 9388d4219e..5c39e9e630 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.2-nightly.4" +__version__ = "3.12.2" diff --git a/pyproject.toml b/pyproject.toml index 0a9c02834a..175e72be24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.2-nightly.4" # OpenPype +version = "3.12.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From b5fb016331e3a86397f6337f44e4c885caf9cff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:10:08 +0200 Subject: [PATCH 0464/1030] moved abstract template loader into openpype/pipeline/workfile --- openpype/{lib => pipeline/workfile}/abstract_template_loader.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/abstract_template_loader.py (100%) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py similarity index 100% rename from openpype/lib/abstract_template_loader.py rename to openpype/pipeline/workfile/abstract_template_loader.py From b1f2831868001431ab5b949cf2a85729a9adfb04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:12:04 +0200 Subject: [PATCH 0465/1030] moved 'get_loaders_by_name' to load utils --- openpype/lib/avalon_context.py | 15 --------------- openpype/pipeline/load/__init__.py | 2 ++ openpype/pipeline/load/utils.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 316c8ad67e..86902cac56 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -943,21 +943,6 @@ def collect_last_version_repres(asset_entities): return output -@with_pipeline_io -def get_loaders_by_name(): - from openpype.pipeline import discover_loader_plugins - - 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 {} !".format(loader_name) - ) - loaders_by_name[loader_name] = loader - return loaders_by_name - - class BuildWorkfile: """Wrapper for build workfile process. diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e46d9f152b..b6bdd13d50 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -16,6 +16,7 @@ from .utils import ( switch_container, get_loader_identifier, + get_loaders_by_name, get_representation_path_from_context, get_representation_path, @@ -61,6 +62,7 @@ __all__ = ( "switch_container", "get_loader_identifier", + "get_loaders_by_name", "get_representation_path_from_context", "get_representation_path", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index fe5102353d..9945e1fce4 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -369,6 +369,20 @@ def get_loader_identifier(loader): return loader.__name__ +def get_loaders_by_name(): + from .plugins import discover_loader_plugins + + 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 {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + def _get_container_loader(container): """Return the Loader corresponding to the container""" from .plugins import discover_loader_plugins From b2b6ffe0e4290840fc1ca1b5c98174f2bdfcbfaf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:13:56 +0200 Subject: [PATCH 0466/1030] updated 'collect_last_version_repres' with latest develop --- openpype/lib/avalon_context.py | 68 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 86902cac56..4b552d13ed 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -847,7 +847,7 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): @with_pipeline_io -def collect_last_version_repres(asset_entities): +def collect_last_version_repres(asset_docs): """Collect subsets, versions and representations for asset_entities. Args: @@ -880,64 +880,56 @@ def collect_last_version_repres(asset_entities): ``` """ - if not asset_entities: - return {} + output = {} + if not asset_docs: + return output - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) + 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} - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) + 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() + ) - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = last_version_docs_by_id[version_id] - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) + subset_id = version_doc["parent"] + subset_doc = subset_entity_by_ids[subset_id] - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_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, + "asset_entity": asset_doc, "subsets": {} } if subset_id not in output[asset_id]["subsets"]: output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, + "subset_entity": subset_doc, "version": { - "version_entity": version, + "version_entity": version_doc, "repres": [] } } output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre + repre_doc ) return output From 9b4b44ef3bdf490fca2a4df0f3451143a09e555c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:17:26 +0200 Subject: [PATCH 0467/1030] moved build template code into workfile --- openpype/{lib => pipeline/workfile}/build_template.py | 0 openpype/{lib => pipeline/workfile}/build_template_exceptions.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/build_template.py (100%) rename openpype/{lib => pipeline/workfile}/build_template_exceptions.py (100%) diff --git a/openpype/lib/build_template.py b/openpype/pipeline/workfile/build_template.py similarity index 100% rename from openpype/lib/build_template.py rename to openpype/pipeline/workfile/build_template.py diff --git a/openpype/lib/build_template_exceptions.py b/openpype/pipeline/workfile/build_template_exceptions.py similarity index 100% rename from openpype/lib/build_template_exceptions.py rename to openpype/pipeline/workfile/build_template_exceptions.py From 6462bf15d04ad53eaed484069e70f2c2312f0a2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:16 +0200 Subject: [PATCH 0468/1030] fixed imports --- .../workfile/abstract_template_loader.py | 24 ++++++++++--------- openpype/pipeline/workfile/build_template.py | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e296e3207f..e95b89b518 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -4,23 +4,25 @@ from abc import ABCMeta, abstractmethod import traceback import six - -from openpype.settings import get_project_settings -from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name -from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io, load - +import logging from functools import reduce -from openpype.lib.build_template_exceptions import ( +from openpype.settings import get_project_settings +from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline.load import ( + get_loaders_by_name, + get_representation_context, + load_with_repre_context, +) + +from .build_template_exceptions import ( TemplateAlreadyImported, TemplateLoadingFailed, TemplateProfileNotFound, TemplateNotFound ) -import logging - log = logging.getLogger(__name__) @@ -289,8 +291,8 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - repre = load.get_representation_context(last_representation) - return load.load_with_repre_context( + repre = get_representation_context(last_representation) + return load_with_repre_context( loaders_by_name[placeholder.loader], repre, options=parse_loader_args(placeholder.data['loader_args'])) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index 7f749cbec2..f4b57218fb 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,6 +1,6 @@ -from openpype.pipeline import registered_host -from openpype.lib import classes_from_module from importlib import import_module +from openpype.lib import classes_from_module +from openpype.pipeline import registered_host from .abstract_template_loader import ( AbstractPlaceholder, From 5dfb12a217f24e5551ec3f4a982823254efdb00e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:44 +0200 Subject: [PATCH 0469/1030] logger is created dynamically on demand and is using class name --- openpype/pipeline/workfile/abstract_template_loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e95b89b518..27823479cf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -72,6 +72,7 @@ class AbstractTemplateLoader: """ def __init__(self, placeholder_class): + self._log = None self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] @@ -91,8 +92,6 @@ class AbstractTemplateLoader: .get("type") ) - self.log = Logger().get_logger("BUILD TEMPLATE") - self.log.info( "BUILDING ASSET FROM TEMPLATE :\n" "Starting templated build for {asset} in {project}\n\n" @@ -112,6 +111,12 @@ class AbstractTemplateLoader: "There is no registered loaders. No assets will be loaded") return + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + def template_already_imported(self, err_msg): """In case template was already loaded. Raise the error as a default action. From 764207d033fc049f6726f901a99732c928595768 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:25:04 +0200 Subject: [PATCH 0470/1030] fix missing import 'get_loaders_by_name' --- openpype/lib/avalon_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 4b552d13ed..e60dbb9e8f 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -992,6 +992,9 @@ class BuildWorkfile: ... }] """ + + from openpype.pipeline.load import get_loaders_by_name + # Get current asset name and entity project_name = legacy_io.active_project() current_asset_name = legacy_io.Session["AVALON_ASSET"] From fe38df50bff954993570cd113371044dde4a5e43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:26:18 +0200 Subject: [PATCH 0471/1030] removed 'get_loaders_by_name' from openpype lib init file --- openpype/lib/__init__.py | 2 -- openpype/pipeline/workfile/__init__.py | 0 2 files changed, 2 deletions(-) create mode 100644 openpype/pipeline/workfile/__init__.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index f4efffd726..fb52a9aca7 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -135,7 +135,6 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, - get_loaders_by_name, BuildWorkfile, @@ -307,7 +306,6 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", - "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From c9ac330e2ebedc6e9900e0d2e6207a20326d0139 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:31:18 +0200 Subject: [PATCH 0472/1030] fixed imports in maya --- openpype/hosts/maya/api/menu.py | 6 +++--- openpype/hosts/maya/api/template_loader.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c0bad7092f..833fbae881 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -8,12 +8,12 @@ import maya.cmds as cmds from openpype.api import BuildWorkfile -from openpype.lib.build_template import ( +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.pipeline.workfile.build_template import ( build_workfile_template, update_workfile_template ) -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index c7946b6ad3..6b225442e7 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,11 +1,13 @@ from maya import cmds from openpype.pipeline import legacy_io -from openpype.lib.abstract_template_loader import ( +from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, AbstractTemplateLoader ) -from openpype.lib.build_template_exceptions import TemplateAlreadyImported +from openpype.pipeline.workfile.build_template_exceptions import ( + TemplateAlreadyImported +) PLACEHOLDER_SET = 'PLACEHOLDERS_SET' From 1e8cf2a6ea87ded1131d5d3012cdd5980dc2f183 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:36:04 +0200 Subject: [PATCH 0473/1030] make sure '_log' attribute is available before abc init --- openpype/pipeline/workfile/abstract_template_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 27823479cf..3d942a0bdd 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -71,9 +71,9 @@ class AbstractTemplateLoader: as placeholders. Depending on current host """ - def __init__(self, placeholder_class): - self._log = None + _log = None + def __init__(self, placeholder_class): self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] self.project_name = legacy_io.Session["AVALON_PROJECT"] From 361ba53f26d89e94758ff8f32e48444ba1715771 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:54:57 +0200 Subject: [PATCH 0474/1030] 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 0475/1030] 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 0476/1030] 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 a3a839181b0fa94d5696a53c8a4d52cc8aed4119 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 11:21:20 +0200 Subject: [PATCH 0477/1030] global, flame, hiero, resolve, sp: implementing `newAssetPublishing` --- .../plugins/publish/collect_timeline_instances.py | 3 ++- .../hiero/plugins/publish/precollect_instances.py | 3 ++- .../resolve/plugins/publish/precollect_instances.py | 3 ++- .../plugins/publish/collect_editorial_instances.py | 3 ++- openpype/plugins/publish/integrate.py | 11 ++++++++++- openpype/plugins/publish/validate_asset_docs.py | 4 ++++ 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 5db89a0ab9..992db62c75 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -136,7 +136,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): "tasks": { task["name"]: {"type": task["type"]} for task in self.add_tasks}, - "representations": [] + "representations": [], + "newAssetPublishing": True }) self.log.debug("__ inst_data: {}".format(pformat(inst_data))) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 2d0ec6fc99..0c7dbc1f22 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -109,7 +109,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "clipAnnotations": annotations, # add all additional tags - "tags": phiero.get_track_item_tags(track_item) + "tags": phiero.get_track_item_tags(track_item), + "newAssetPublishing": True }) # otio clip data diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 8f1a13a4e5..ee51998c0d 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -70,7 +70,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "publish": resolve.get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, - "handleEnd": handle_end + "handleEnd": handle_end, + "newAssetPublishing": True }) # otio clip data diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 3237fbbe12..75c260bad7 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -170,7 +170,8 @@ class CollectInstances(pyblish.api.InstancePlugin): "frameStart": frame_start, "frameEnd": frame_end, "frameStartH": frame_start - handle_start, - "frameEndH": frame_end + handle_end + "frameEndH": frame_end + handle_end, + "newAssetPublishing": True } for data_key in instance_data_filter: diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8ab508adc9..a4378bf58d 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -12,6 +12,7 @@ import pyblish.api import openpype.api from openpype.client import ( get_representations, + get_asset_by_name, get_subset_by_name, get_version_by_name, ) @@ -273,6 +274,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def register(self, instance, file_transactions, filtered_repres): project_name = legacy_io.active_project() + # making sure editorial instances have its `assetEntity` + if instance.data.get("newAssetPublishing"): + asset_doc = get_asset_by_name( + project_name, + instance.data["asset"] + ) + instance.data["assetEntity"] = asset_doc + instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: self.log.info(( @@ -426,7 +435,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "".format(len(prepared_representations))) def prepare_subset(self, instance, project_name): - asset_doc = instance.data.get("assetEntity") + asset_doc = instance.data["assetEntity"] subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index bc1f9b9e6c..9a1ca5b8de 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 instance.data.get("newAssetPublishing"): + # 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 e8a8f86cdf387e777914ae833ea7f469bc63b11c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 12:32:09 +0200 Subject: [PATCH 0478/1030] global: removing changes from integrate --- openpype/plugins/publish/integrate.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index a4378bf58d..74227fdb40 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -274,14 +274,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def register(self, instance, file_transactions, filtered_repres): project_name = legacy_io.active_project() - # making sure editorial instances have its `assetEntity` - if instance.data.get("newAssetPublishing"): - asset_doc = get_asset_by_name( - project_name, - instance.data["asset"] - ) - instance.data["assetEntity"] = asset_doc - instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: self.log.info(( From fac4529e4df877bdf5f774907430f9b5662636eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 12:32:44 +0200 Subject: [PATCH 0479/1030] global: integrate hierarchy is fixing avalonData and avalonEntity --- .../publish/extract_hierarchy_avalon.py | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 8d447ba595..967381b02e 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -30,9 +30,15 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) self.project = None - self.import_to_avalon(project_name, hierarchy_context) + self.import_to_avalon(context, project_name, hierarchy_context) - def import_to_avalon(self, project_name, input_data, parent=None): + def import_to_avalon( + self, + context, + project_name, + input_data, + parent=None, + ): for name in input_data: self.log.info("input_data[name]: {}".format(input_data[name])) entity_data = input_data[name] @@ -133,6 +139,9 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): # Unarchive if entity was archived entity = self.unarchive_entity(unarchive_entity, data) + # make sure all relative instances have correct avalon data + self._set_avalon_data_to_relative_instances(context, entity) + if update_data: # Update entity data with input data legacy_io.update_many( @@ -142,7 +151,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if "childs" in entity_data: self.import_to_avalon( - project_name, entity_data["childs"], entity + context, project_name, entity_data["childs"], entity ) def unarchive_entity(self, entity, data): @@ -159,20 +168,43 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): {"_id": entity["_id"]}, new_entity ) + return new_entity - def create_avalon_asset(self, project_name, name, data): - item = { + def create_avalon_asset(self, name, data): + asset_doc = { "schema": "openpype:asset-3.0", "name": name, "parent": self.project["_id"], "type": "asset", "data": data } - self.log.debug("Creating asset: {}".format(item)) - entity_id = legacy_io.insert_one(item).inserted_id + self.log.debug("Creating asset: {}".format(asset_doc)) + asset_doc["_id"] = legacy_io.insert_one(asset_doc).inserted_id - return get_asset_by_id(project_name, entity_id) + return asset_doc + + def _set_avalon_data_to_relative_instances(self, context, asset_doc): + for instance in context: + asset_name = asset_doc["name"] + inst_asset_name = instance.data["asset"] + + if asset_name == inst_asset_name: + instance.data["assetEntity"] = asset_doc + + # get parenting data + parents = asset_doc["data"].get("parents") or list() + + # equire only relative parent + if parents: + parent_name = parents[-1] + + # update avalon data on instance + instance.data["avalonData"].update({ + "hierarchy": "/".join(parents), + "task": {}, + "parent": parent_name + }) def _get_active_assets(self, context): """ Returns only asset dictionary. From 9b14e486579e209f2ff100842c081fc938406c8c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 12:38:14 +0200 Subject: [PATCH 0480/1030] fixing avalonData to anatomyData --- openpype/plugins/publish/extract_hierarchy_avalon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 967381b02e..01dc80d6ee 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -200,7 +200,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): parent_name = parents[-1] # update avalon data on instance - instance.data["avalonData"].update({ + instance.data["anatomyData"].update({ "hierarchy": "/".join(parents), "task": {}, "parent": parent_name From 5af77fe04caf1b38313ce09b182aa4f3eea2946f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 27 Jul 2022 12:59:41 +0200 Subject: [PATCH 0481/1030] Update openpype/plugins/publish/extract_hierarchy_avalon.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/extract_hierarchy_avalon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 01dc80d6ee..37ca42e4cc 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -186,6 +186,9 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): def _set_avalon_data_to_relative_instances(self, context, asset_doc): for instance in context: + # Skip instance if has filled asset entity + if instance.data.get("assetEntity"): + continue asset_name = asset_doc["name"] inst_asset_name = instance.data["asset"] From e9e00831f03d69776a380d826e6a971e44855bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 27 Jul 2022 13:00:14 +0200 Subject: [PATCH 0482/1030] Update openpype/plugins/publish/extract_hierarchy_avalon.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/extract_hierarchy_avalon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 37ca42e4cc..ec01ab4e8f 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -199,6 +199,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): parents = asset_doc["data"].get("parents") or list() # equire only relative parent + parent_name = project_name if parents: parent_name = parents[-1] From 203048bcf814a5ab8e05f769ce19d52fd19937db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 27 Jul 2022 13:00:21 +0200 Subject: [PATCH 0483/1030] Update openpype/plugins/publish/integrate.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/integrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 74227fdb40..cac212b7e2 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -12,7 +12,6 @@ import pyblish.api import openpype.api from openpype.client import ( get_representations, - get_asset_by_name, get_subset_by_name, get_version_by_name, ) From 86d9d0134ad57ebb1a07cdf3dd6d6ef13d466d0d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 13:02:45 +0200 Subject: [PATCH 0484/1030] fixing missing project_name --- .../plugins/publish/extract_hierarchy_avalon.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index ec01ab4e8f..d765755eee 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -140,7 +140,11 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): entity = self.unarchive_entity(unarchive_entity, data) # make sure all relative instances have correct avalon data - self._set_avalon_data_to_relative_instances(context, entity) + self._set_avalon_data_to_relative_instances( + context, + project_name, + entity + ) if update_data: # Update entity data with input data @@ -184,7 +188,12 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): return asset_doc - def _set_avalon_data_to_relative_instances(self, context, asset_doc): + def _set_avalon_data_to_relative_instances( + self, + context, + project_name, + asset_doc + ): for instance in context: # Skip instance if has filled asset entity if instance.data.get("assetEntity"): From a0149c36ffd80d1dcc5a2b08c5c09d37062de621 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Jul 2022 13:14:35 +0200 Subject: [PATCH 0485/1030] fixing problem with more function argumets --- openpype/plugins/publish/extract_hierarchy_avalon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index d765755eee..6b4e5f48c5 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -133,7 +133,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if unarchive_entity is None: # Create entity if doesn"t exist entity = self.create_avalon_asset( - project_name, name, data + name, data ) else: # Unarchive if entity was archived From 2e0fe9335151c6b7cdc9d25011216ca3b2705f5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 13:16:46 +0200 Subject: [PATCH 0486/1030] 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 0487/1030] 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 0488/1030] 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 0489/1030] 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 0490/1030] 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 0491/1030] 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 0492/1030] 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 0493/1030] 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 0494/1030] 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 0495/1030] 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 0496/1030] 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 0497/1030] 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 0498/1030] 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 0499/1030] 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 0500/1030] 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 0501/1030] 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 0502/1030] 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 0503/1030] 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 0504/1030] :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 0505/1030] 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 0506/1030] 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 0507/1030] 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 0508/1030] 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 0509/1030] 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 6bb28d16df22e4d5c4cf6e763a85a545ba6da833 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Jul 2022 11:53:51 +0200 Subject: [PATCH 0510/1030] fix build template and added few comments --- .../workfile/abstract_template_loader.py | 30 +++++++++++++------ openpype/pipeline/workfile/build_template.py | 9 +++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 3d942a0bdd..00bc8f15a7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,6 +7,7 @@ import six import logging from functools import reduce +from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import get_linked_assets, PypeLogger as Logger from openpype.pipeline import legacy_io, Anatomy @@ -74,18 +75,25 @@ class AbstractTemplateLoader: _log = None def __init__(self, placeholder_class): + # TODO template loader should expect host as and argument + # - host have all responsibility for most of code (also provide + # placeholder class) + # - also have responsibility for current context + # - this won't work in DCCs where multiple workfiles with + # different contexts can be opened at single time + # - template loader should have ability to change context + project_name = legacy_io.active_project() + asset_name = legacy_io.Session["AVALON_ASSET"] + self.loaders_by_name = get_loaders_by_name() - self.current_asset = legacy_io.Session["AVALON_ASSET"] - self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.current_asset = asset_name + self.project_name = project_name self.host_name = legacy_io.Session["AVALON_APP"] self.task_name = legacy_io.Session["AVALON_TASK"] self.placeholder_class = placeholder_class - self.current_asset_docs = legacy_io.find_one({ - "type": "asset", - "name": self.current_asset - }) + self.current_asset_doc = get_asset_by_name(project_name, asset_name) self.task_type = ( - self.current_asset_docs + self.current_asset_doc .get("data", {}) .get("tasks", {}) .get(self.task_name, {}) @@ -218,7 +226,7 @@ class AbstractTemplateLoader: loaders_by_name = self.loaders_by_name current_asset = self.current_asset linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_docs)] + in get_linked_assets(self.current_asset_doc)] ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -270,7 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets): + self, placeholder, current_asset, linked_assets + ): + # TODO This approach must be changed. Placeholders should return + # already prepared data and not query them here. + # - this is impossible to handle using query functions placeholder_db_filters = placeholder.convert_to_db_filters( current_asset, linked_assets) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index f4b57218fb..df6fe3514a 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,5 +1,6 @@ from importlib import import_module from openpype.lib import classes_from_module +from openpype.host import HostBase from openpype.pipeline import registered_host from .abstract_template_loader import ( @@ -35,7 +36,13 @@ def update_workfile_template(args): def build_template_loader(): - host_name = registered_host().__name__.partition('.')[2] + # TODO refactor to use advantage of 'HostBase' and don't import dynamically + # - hosts should have methods that gives option to return builders + host = registered_host() + if isinstance(host, HostBase): + host_name = host.name + else: + host_name = host.__name__.partition('.')[2] module_path = _module_path_format.format(host=host_name) module = import_module(module_path) if not module: From 03767d28912b65a47b66826cc359a6db0baf4533 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 28 Jul 2022 13:03:37 +0300 Subject: [PATCH 0511/1030] 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 0512/1030] 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 0513/1030] 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 0514/1030] 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 0515/1030] 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 0516/1030] 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 0517/1030] 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 628833be97308401e3279929a9866da03c6d8d9d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 14:48:37 +0200 Subject: [PATCH 0518/1030] flame: adding timewarp effect scraping --- openpype/hosts/flame/api/lib.py | 153 ++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index d59308ad6c..02481a1d2e 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1,14 +1,16 @@ import sys import os import re +import sys import json import pickle import clique import tempfile +import traceback import itertools import contextlib import xml.etree.cElementTree as cET -from copy import deepcopy +from copy import deepcopy, copy from xml.etree import ElementTree as ET from pprint import pformat from .constants import ( @@ -266,7 +268,7 @@ def get_current_sequence(selection): def rescan_hooks(): import flame try: - flame.execute_shortcut('Rescan Python Hooks') + flame.execute_shortcut("Rescan Python Hooks") except Exception: pass @@ -1082,21 +1084,21 @@ class MediaInfoFile(object): xml_data (ET.Element): clip data """ try: - for out_track in xml_data.iter('track'): - for out_feed in out_track.iter('feed'): + for out_track in xml_data.iter("track"): + for out_feed in out_track.iter("feed"): # start frame out_feed_nb_ticks_obj = out_feed.find( - 'startTimecode/nbTicks') + "startTimecode/nbTicks") self.start_frame = out_feed_nb_ticks_obj.text # fps out_feed_fps_obj = out_feed.find( - 'startTimecode/rate') + "startTimecode/rate") self.fps = out_feed_fps_obj.text # drop frame mode out_feed_drop_mode_obj = out_feed.find( - 'startTimecode/dropMode') + "startTimecode/dropMode") self.drop_mode = out_feed_drop_mode_obj.text break except Exception as msg: @@ -1118,8 +1120,143 @@ class MediaInfoFile(object): tree = cET.ElementTree(xml_element_data) tree.write( fpath, xml_declaration=True, - method='xml', encoding='UTF-8' + method="xml", encoding="UTF-8" ) except IOError as error: raise IOError( "Not able to write data to file: {}".format(error)) + + +class TimeEffectMetadata(object): + log = log + temp_setup_path = "/var/tmp/temp_timewarp_setup.timewarp_node" + _data = {} + _retime_modes = { + 0: "speed", + 1: "timewarp", + 2: "duration" + } + + def __init__(self, segment=None, logger=None): + if logger: + self.log = logger + if segment: + self._data = self._get_metadata(segment) + + def _get_metadata(self, segment): + effects = segment.effects or [] + for effect in effects: + if effect.type == "Timewarp": + effect.save_setup(self.temp_setup_path) + + self._data = self._get_attributes_from_xml() + os.remove(self.temp_setup_path) + + def _get_attributes_from_xml(self): + with open(self.temp_setup_path, "r") as tw_setup_file: + tw_setup_string = tw_setup_file.read() + tw_setup_file.close() + + tw_setup_xml = ET.fromstring(tw_setup_string) + tw_setup = self._dictify(tw_setup_xml) + # pprint(tw_setup) + try: + tw_setup_state = tw_setup["Setup"]["State"][0] + mode = int( + tw_setup_state["TW_RetimerMode"][0]["_text"] + ) + r_data = { + "type": self._retime_modes[mode], + "effectStart": int( + tw_setup["Setup"]["Base"][0]["Range"][0]["Start"]), + "effectEnd": int( + tw_setup["Setup"]["Base"][0]["Range"][0]["End"]) + } + + if mode == 0: # speed + r_data[self._retime_modes[mode]] = int( + tw_setup_state["TW_Speed"] + [0]["Channel"][0]["Value"][0]["_text"] + ) / 100 + elif mode == 1: # timewarp + print("timing") + r_data[self._retime_modes[mode]] = self._get_anim_keys( + tw_setup_state["TW_Timing"] + ) + elif mode == 2: # duration + r_data[self._retime_modes[mode]] = { + "start": { + "source": int( + tw_setup_state["TW_DurationTiming"][0]["Channel"] + [0]["KFrames"][0]["Key"][0]["Value"][0]["_text"] + ), + "timeline": int( + tw_setup_state["TW_DurationTiming"][0]["Channel"] + [0]["KFrames"][0]["Key"][0]["Frame"][0]["_text"] + ) + }, + "end": { + "source": int( + tw_setup_state["TW_DurationTiming"][0]["Channel"] + [0]["KFrames"][0]["Key"][1]["Value"][0]["_text"] + ), + "timeline": int( + tw_setup_state["TW_DurationTiming"][0]["Channel"] + [0]["KFrames"][0]["Key"][1]["Frame"][0]["_text"] + ) + } + } + except Exception: + lines = traceback.format_exception(*sys.exc_info()) + self.log.error("\n".join(lines)) + return + + return r_data + + def _get_anim_keys(self, setup_cat, index=None): + return_data = { + "extrapolation": ( + setup_cat[0]["Channel"][0]["Extrap"][0]["_text"] + ), + "animKeys": [] + } + for key in setup_cat[0]["Channel"][0]["KFrames"][0]["Key"]: + if index and int(key["Index"]) != index: + continue + key_data = { + "source": float(key["Value"][0]["_text"]), + "timeline": float(key["Frame"][0]["_text"]), + "index": int(key["Index"]), + "curveMode": key["CurveMode"][0]["_text"], + "curveOrder": key["CurveOrder"][0]["_text"] + } + if key.get("TangentMode"): + key_data["tangentMode"] = key["TangentMode"][0]["_text"] + + return_data["animKeys"].append(key_data) + + return return_data + + def _dictify(self, xml_, root=True): + """ Convert xml object to dictionary + + Args: + xml_ (xml.etree.ElementTree.Element): xml data + root (bool, optional): is root available. Defaults to True. + + Returns: + dict: dictionarized xml + """ + + if root: + return {xml_.tag: self._dictify(xml_, False)} + + d = copy(xml_.attrib) + if xml_.text: + d["_text"] = xml_.text + + for x in xml_.findall("./*"): + if x.tag not in d: + d[x.tag] = [] + d[x.tag].append(self._dictify(x, False)) + return d From 2998253832daf43f62fc901de6dd11eccb2708fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 14:58:43 +0200 Subject: [PATCH 0519/1030] flame: adding property to return data --- openpype/hosts/flame/api/lib.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 02481a1d2e..a02acd85a7 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1129,7 +1129,6 @@ class MediaInfoFile(object): class TimeEffectMetadata(object): log = log - temp_setup_path = "/var/tmp/temp_timewarp_setup.timewarp_node" _data = {} _retime_modes = { 0: "speed", @@ -1137,23 +1136,34 @@ class TimeEffectMetadata(object): 2: "duration" } - def __init__(self, segment=None, logger=None): + def __init__(self, segment, logger=None): if logger: self.log = logger - if segment: - self._data = self._get_metadata(segment) + + self._data = self._get_metadata(segment) + + @property + def data(self): + """ Returns timewarp effect data + + Returns: + dict: retime data + """ + return self._data def _get_metadata(self, segment): effects = segment.effects or [] for effect in effects: if effect.type == "Timewarp": - effect.save_setup(self.temp_setup_path) + with maintained_temp_file_path(".timewarp_node") as tmp_path: + self.log.info("Temp File: {}".format(tmp_path)) + effect.save_setup(tmp_path) + return self._get_attributes_from_xml(tmp_path) - self._data = self._get_attributes_from_xml() - os.remove(self.temp_setup_path) + return {} - def _get_attributes_from_xml(self): - with open(self.temp_setup_path, "r") as tw_setup_file: + def _get_attributes_from_xml(self, tmp_path): + with open(tmp_path, "r") as tw_setup_file: tw_setup_string = tw_setup_file.read() tw_setup_file.close() From 7f9948eaad87d144db2fc58c5083798ebf34482f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 15:09:06 +0200 Subject: [PATCH 0520/1030] flame: adding timewarp class to api --- openpype/hosts/flame/api/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 2c461e5f16..76c1c93379 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -30,7 +30,8 @@ from .lib import ( maintained_temp_file_path, get_clip_segment, get_batch_group_from_desktop, - MediaInfoFile + MediaInfoFile, + TimeEffectMetadata ) from .utils import ( setup, @@ -107,6 +108,7 @@ __all__ = [ "get_clip_segment", "get_batch_group_from_desktop", "MediaInfoFile", + "TimeEffectMetadata", # pipeline "install", From 42fa3dd2097cf7d1b9c9442b042600981be64bb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 15:09:25 +0200 Subject: [PATCH 0521/1030] flame: implementing timewarpmetadata class --- openpype/hosts/flame/otio/flame_export.py | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 1e4ef866ed..a111176e29 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -275,7 +275,7 @@ def create_otio_reference(clip_data, fps=None): def create_otio_clip(clip_data): - from openpype.hosts.flame.api import MediaInfoFile + from openpype.hosts.flame.api import MediaInfoFile, TimeEffectMetadata segment = clip_data["PySegment"] @@ -284,14 +284,27 @@ def create_otio_clip(clip_data): media_timecode_start = media_info.start_frame media_fps = media_info.fps + # Timewarp metadata + tw_data = TimeEffectMetadata(segment, logger=log).data + log.debug("__ tw_data: {}".format(tw_data)) + # define first frame first_frame = media_timecode_start or utils.get_frame_from_filename( clip_data["fpath"]) or 0 _clip_source_in = int(clip_data["source_in"]) _clip_source_out = int(clip_data["source_out"]) + _clip_source_duration = clip_data["source_duration"] + _clip_record_in = clip_data["record_in"] + _clip_record_out = clip_data["record_out"] _clip_record_duration = int(clip_data["record_duration"]) + log.debug("_ first_frame: {}".format(first_frame)) + log.debug("_ _clip_source_in: {}".format(_clip_source_in)) + log.debug("_ _clip_source_out: {}".format(_clip_source_out)) + log.debug("_ _clip_record_in: {}".format(_clip_record_in)) + log.debug("_ _clip_record_out: {}".format(_clip_record_out)) + # first solve if the reverse timing speed = 1 if clip_data["source_in"] > clip_data["source_out"]: @@ -307,13 +320,17 @@ def create_otio_clip(clip_data): # secondly check if any change of speed if source_duration != _clip_record_duration: retime_speed = float(source_duration) / float(_clip_record_duration) - log.debug("_ retime_speed: {}".format(retime_speed)) + log.debug("_ calculated speed: {}".format(retime_speed)) speed *= retime_speed - log.debug("_ source_in: {}".format(source_in)) - log.debug("_ source_out: {}".format(source_out)) + # get speed from metadata if available + if tw_data.get("speed"): + speed = tw_data["speed"] + log.debug("_ metadata speed: {}".format(speed)) + log.debug("_ speed: {}".format(speed)) log.debug("_ source_duration: {}".format(source_duration)) + log.debug("_ _clip_source_duration: {}".format(_clip_source_duration)) log.debug("_ _clip_record_duration: {}".format(_clip_record_duration)) # create media reference From 009d7fc1fb765f18cadf1782bd66a5c3b95c38ee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 15:18:07 +0200 Subject: [PATCH 0522/1030] flame: speed should be float --- openpype/hosts/flame/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index a02acd85a7..a5ae3c4468 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1184,7 +1184,7 @@ class TimeEffectMetadata(object): } if mode == 0: # speed - r_data[self._retime_modes[mode]] = int( + r_data[self._retime_modes[mode]] = float( tw_setup_state["TW_Speed"] [0]["Channel"][0]["Value"][0]["_text"] ) / 100 From 8eb5c1ccb30c6fb7bfb6cddd2eb82d3697a652c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 15:37:16 +0200 Subject: [PATCH 0523/1030] flame: more frame debug printing --- openpype/hosts/flame/otio/flame_export.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index a111176e29..6d6b33d2a1 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -289,16 +289,20 @@ def create_otio_clip(clip_data): log.debug("__ tw_data: {}".format(tw_data)) # define first frame - first_frame = media_timecode_start or utils.get_frame_from_filename( - clip_data["fpath"]) or 0 + file_first_frame = utils.get_frame_from_filename( + clip_data["fpath"]) + if file_first_frame: + file_first_frame = int(file_first_frame) + + first_frame = media_timecode_start or file_first_frame or 0 _clip_source_in = int(clip_data["source_in"]) _clip_source_out = int(clip_data["source_out"]) - _clip_source_duration = clip_data["source_duration"] _clip_record_in = clip_data["record_in"] _clip_record_out = clip_data["record_out"] _clip_record_duration = int(clip_data["record_duration"]) + log.debug("_ file_first_frame: {}".format(file_first_frame)) log.debug("_ first_frame: {}".format(first_frame)) log.debug("_ _clip_source_in: {}".format(_clip_source_in)) log.debug("_ _clip_source_out: {}".format(_clip_source_out)) @@ -315,6 +319,15 @@ def create_otio_clip(clip_data): source_in = _clip_source_in - int(first_frame) source_out = _clip_source_out - int(first_frame) + log.debug("_ source_in: {}".format(source_in)) + log.debug("_ source_out: {}".format(source_out)) + + if file_first_frame: + log.debug("_ file_source_in: {}".format( + file_first_frame + source_in)) + log.debug("_ file_source_in: {}".format( + file_first_frame + source_out)) + source_duration = (source_out - source_in + 1) # secondly check if any change of speed @@ -330,7 +343,6 @@ def create_otio_clip(clip_data): log.debug("_ speed: {}".format(speed)) log.debug("_ source_duration: {}".format(source_duration)) - log.debug("_ _clip_source_duration: {}".format(_clip_source_duration)) log.debug("_ _clip_record_duration: {}".format(_clip_record_duration)) # create media reference From bc2cec540c8b7962d3b0c0fc8dabe5f6cf54fb36 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 16:29:50 +0200 Subject: [PATCH 0524/1030] 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 0525/1030] 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 4a18e50352cda46e3b8de09bd7a40df15ea1384d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 28 Jul 2022 17:42:52 +0200 Subject: [PATCH 0526/1030] Update openpype/hosts/nuke/plugins/publish/validate_script_attributes.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../nuke/plugins/publish/validate_script_attributes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index d16660f272..3907f40991 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -26,11 +26,7 @@ class ValidateScriptAttributes(pyblish.api.InstancePlugin): def process(self, instance): root = nuke.root() knob_data = get_avalon_knob_data(root) - project_name = legacy_io.active_project() - asset = get_asset_by_name( - project_name, - instance.context.data["asset"] - ) + asset = instance.data["assetEntity"] # get asset data frame values frame_start = asset["data"]["frameStart"] frame_end = asset["data"]["frameEnd"] From 6a51b4e7891829761b2c49353d17a7fc3423edcc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Jul 2022 17:49:29 +0200 Subject: [PATCH 0527/1030] hound suggestions --- .../hosts/nuke/plugins/publish/validate_output_resolution.py | 2 +- .../hosts/nuke/plugins/publish/validate_script_attributes.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index 710adde069..fc07e9b83b 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -89,4 +89,4 @@ class ValidateOutputResolution(pyblish.api.InstancePlugin): if cls.resolution_msg == invalid: reformat = cls.get_reformat(instance) reformat["format"].setValue(nuke.root()["format"].value()) - cls.log.info("I am fixing reformat to root.format") \ No newline at end of file + cls.log.info("I am fixing reformat to root.format") diff --git a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py index 3907f40991..106d7a2524 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -2,8 +2,6 @@ from pprint import pformat import pyblish.api import openpype.api -from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io from openpype.pipeline import PublishXmlValidationError from openpype.hosts.nuke.api.lib import ( get_avalon_knob_data, From 037c5a13cddc3e9426ebfe2c46ec6abc82eb559f Mon Sep 17 00:00:00 2001 From: kaamaurice Date: Thu, 28 Jul 2022 18:46:47 +0200 Subject: [PATCH 0528/1030] 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 0529/1030] 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 0530/1030] 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 0531/1030] 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 0532/1030] 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 0533/1030] 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 0534/1030] 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 0535/1030] 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 0536/1030] 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 0537/1030] 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 2d601d051a9b59509c6af159c06f8424591af444 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 16:42:54 +0200 Subject: [PATCH 0538/1030] give ability to query by representation context and regex --- openpype/client/entities.py | 108 +++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index dd5d831ecf..57c38784b0 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -7,6 +7,7 @@ that has project name as a context (e.g. on 'ProjectEntity'?). """ import os +import re import collections import six @@ -1035,17 +1036,70 @@ def get_representation_by_name( return conn.find_one(query_filter, _prepare_fields(fields)) +def _flatten_dict(data): + flatten_queue = collections.deque() + flatten_queue.append(data) + output = {} + while flatten_queue: + item = flatten_queue.popleft() + for key, value in item.items(): + if not isinstance(value, dict): + output[key] = value + continue + + tmp = {} + for subkey, subvalue in value.items(): + new_key = "{}.{}".format(key, subkey) + tmp[new_key] = subvalue + flatten_queue.append(tmp) + return output + + +def _regex_filters(filters): + output = [] + for key, value in filters.items(): + regexes = [] + a_values = [] + if isinstance(value, re.Pattern): + regexes.append(value) + elif isinstance(value, (list, tuple, set)): + for item in value: + if isinstance(item, re.Pattern): + regexes.append(item) + else: + a_values.append(item) + else: + a_values.append(value) + + key_filters = [] + if len(a_values) == 1: + key_filters.append({key: a_values[0]}) + elif a_values: + key_filters.append({key: {"$in": a_values}}) + + for regex in regexes: + key_filters.append({key: {"$regex": regex}}) + + if len(key_filters) == 1: + output.append(key_filters[0]) + else: + output.append({"$or": key_filters}) + + return output + + def _get_representations( project_name, representation_ids, representation_names, version_ids, - extensions, + context_filters, names_by_version_ids, standard, archived, fields ): + default_output = [] repre_types = [] if standard: repre_types.append("representation") @@ -1053,7 +1107,7 @@ def _get_representations( repre_types.append("archived_representation") if not repre_types: - return [] + return default_output if len(repre_types) == 1: query_filter = {"type": repre_types[0]} @@ -1063,25 +1117,21 @@ def _get_representations( if representation_ids is not None: representation_ids = _convert_ids(representation_ids) if not representation_ids: - return [] + return default_output query_filter["_id"] = {"$in": representation_ids} if representation_names is not None: if not representation_names: - return [] + return default_output query_filter["name"] = {"$in": list(representation_names)} if version_ids is not None: version_ids = _convert_ids(version_ids) if not version_ids: - return [] + return default_output query_filter["parent"] = {"$in": version_ids} - if extensions is not None: - if not extensions: - return [] - query_filter["context.ext"] = {"$in": list(extensions)} - + or_queries = [] if names_by_version_ids is not None: or_query = [] for version_id, names in names_by_version_ids.items(): @@ -1091,8 +1141,35 @@ def _get_representations( "name": {"$in": list(names)} }) if not or_query: + return default_output + or_queries.append(or_query) + + if context_filters is not None: + if not context_filters: return [] - query_filter["$or"] = or_query + _flatten_filters = _flatten_dict(context_filters) + flatten_filters = {} + for key, value in _flatten_filters.items(): + if not key.startswith("context"): + key = "context.{}".format(key) + flatten_filters[key] = value + + for item in _regex_filters(flatten_filters): + for key, value in item.items(): + if key == "$or": + or_queries.append(value) + else: + query_filter[key] = value + + if len(or_queries) == 1: + query_filter["$or"] = or_queries[0] + elif or_queries: + and_query = [] + for or_query in or_queries: + if isinstance(or_query, list): + or_query = {"$or": or_query} + and_query.append(or_query) + query_filter["$and"] = and_query conn = get_project_connection(project_name) @@ -1104,7 +1181,7 @@ def get_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, + context_filters=None, names_by_version_ids=None, archived=False, standard=True, @@ -1122,8 +1199,8 @@ def get_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). + context_filters (Dict[str, List[str, re.Pattern]]): Filter by + representation context fields. names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering using version ids and list of names under the version. archived (bool): Output will also contain archived representations. @@ -1140,6 +1217,7 @@ def get_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, archived=archived, @@ -1153,6 +1231,7 @@ def get_archived_representations( representation_names=None, version_ids=None, extensions=None, + context_filters=None, names_by_version_ids=None, fields=None ): @@ -1185,6 +1264,7 @@ def get_archived_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, archived=True, From d63d0cfb6f40ba7a0cc5b6a0cb3cc8d3057da6ba Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 29 Jul 2022 17:56:39 +0300 Subject: [PATCH 0539/1030] 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 0540/1030] 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 c65dd9747f5197868a9153fc109915ed654122ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:15:13 +0200 Subject: [PATCH 0541/1030] added new method 'get_representations' to get representations from placeholder --- .../workfile/abstract_template_loader.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 00bc8f15a7..0a422f5cca 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,8 +456,25 @@ class AbstractPlaceholder: @abstractmethod def clean(self): - """Clean placeholder from hierarchy after loading assets. + """Clean placeholder from hierarchy after loading assets.""" + + pass + + @abstractmethod + def get_representations(self, current_asset, linked_assets): + """Query representations based on placeholder data. + + Args: + current_asset (str): Name of current + context asset. + linked_assets (List[str]): Names of assets + linked to current context asset. + + Returns: + Iterable[Dict[str, Any]]: Representations that are matching + placeholder filters. """ + pass @abstractmethod From da8e25f4a1b7ea89bf9c7cac62c8a3ea10fbb9e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:16:23 +0200 Subject: [PATCH 0542/1030] use 'get_representations' instead of 'convert_to_db_filters' --- .../workfile/abstract_template_loader.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0a422f5cca..a2505c061e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -280,19 +280,16 @@ class AbstractTemplateLoader: def get_placeholder_representations( self, placeholder, current_asset, linked_assets ): - # TODO This approach must be changed. Placeholders should return - # already prepared data and not query them here. - # - this is impossible to handle using query functions - placeholder_db_filters = placeholder.convert_to_db_filters( + placeholder_representations = placeholder.get_representations( current_asset, - linked_assets) - # get representation by assets - for db_filter in placeholder_db_filters: - placeholder_representations = list(legacy_io.find(db_filter)) - for representation in reduce(update_representations, - placeholder_representations, - dict()).values(): - yield representation + linked_assets + ) + for repre_doc in reduce( + update_representations, + placeholder_representations, + dict() + ).values(): + yield repre_doc def load_data_is_incorrect( self, placeholder, last_representation, ignored_ids): From c944ae35c9848045cfb73ccfc1b93f30f7af2989 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:17:03 +0200 Subject: [PATCH 0543/1030] 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 0e0cec5e0146a3001a4a349360324346fd0ab961 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:19:47 +0200 Subject: [PATCH 0544/1030] pass asset documents instead of just names --- .../workfile/abstract_template_loader.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a2505c061e..96012eba36 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -223,10 +223,10 @@ class AbstractTemplateLoader: Returns: None """ + loaders_by_name = self.loaders_by_name - current_asset = self.current_asset - linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_doc)] + current_asset_doc = self.current_asset_doc + linked_assets = get_linked_assets(current_asset_doc) ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -239,7 +239,7 @@ class AbstractTemplateLoader: )) placeholder_representations = self.get_placeholder_representations( placeholder, - current_asset, + current_asset_doc, linked_assets ) @@ -278,11 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets + self, placeholder, current_asset_doc, linked_asset_docs ): placeholder_representations = placeholder.get_representations( - current_asset, - linked_assets + current_asset_doc, + linked_asset_docs ) for repre_doc in reduce( update_representations, @@ -458,13 +458,13 @@ class AbstractPlaceholder: pass @abstractmethod - def get_representations(self, current_asset, linked_assets): + def get_representations(self, current_asset_doc, linked_asset_docs): """Query representations based on placeholder data. Args: - current_asset (str): Name of current + current_asset_doc (Dict[str, Any]): Document of current context asset. - linked_assets (List[str]): Names of assets + linked_asset_docs (List[Dict[str, Any]]): Documents of assets linked to current context asset. Returns: From ef674857f85f360954b4d6e2c6f6c0c4acf3f711 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:29:34 +0200 Subject: [PATCH 0545/1030] implemented get_representations for maya placeholder --- openpype/hosts/maya/api/template_loader.py | 80 +++++++++++----------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 6b225442e7..f553730186 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,5 +1,7 @@ +import re from maya import cmds +from openpype.client import get_representations from openpype.pipeline import legacy_io from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, @@ -191,48 +193,48 @@ class MayaPlaceholder(AbstractPlaceholder): cmds.hide(node) cmds.setAttr(node + '.hiddenInOutliner', True) - def convert_to_db_filters(self, current_asset, linked_asset): - if self.data['builder_type'] == "context_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": current_asset, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + def get_representations(self, current_asset_doc, linked_asset_docs): + project_name = legacy_io.active_project() - elif self.data['builder_type'] == "linked_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": asset_name, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } for asset_name in linked_asset - ] + builder_type = self.data["builder_type"] + if builder_type == "context_asset": + context_filters = { + "asset": [current_asset_doc["name"]], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representations": [self.data["representation"]], + "family": [self.data["family"]] + } + + elif builder_type != "linked_asset": + context_filters = { + "asset": [re.compile(self.data["asset"])], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]] + } else: - return [ - { - "type": "representation", - "context.asset": {"$regex": self.data['asset']}, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + asset_regex = re.compile(self.data["asset"]) + linked_asset_names = [] + for asset_doc in linked_asset_docs: + asset_name = asset_doc["name"] + if asset_regex.match(asset_name): + linked_asset_names.append(asset_name) + + context_filters = { + "asset": linked_asset_names, + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]], + } + + return list(get_representations( + project_name, + context_filters=context_filters + )) def err_message(self): return ( From a6406f72d36d8eb748404af8fc6e6d61c6c6b451 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:39:57 +0200 Subject: [PATCH 0546/1030] added logger to placeholder --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 96012eba36..d934c50daf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -397,8 +397,19 @@ class AbstractPlaceholder: optional_attributes = {} def __init__(self, node): + self._log = None self.get_data(node) + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(repr(self)) + return self._log + + def __repr__(self): + return "< {} {} >".format(self.__class__.__name__, self.name) + + def order(self): """Get placeholder order. Order is used to sort them by priority @@ -436,9 +447,9 @@ class AbstractPlaceholder: Bool: True if every attributes are a key of data """ if set(self.attributes).issubset(self.data.keys()): - print("Valid placeholder : {}".format(self.data["node"])) + self.log.debug("Valid placeholder: {}".format(self.data["node"])) return True - print("Placeholder is not valid : {}".format(self.data["node"])) + self.log.info("Placeholder is not valid: {}".format(self.data["node"])) return False @abstractmethod From 292d071f442a494cabd2161512012b13e391a9f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:39:59 +0200 Subject: [PATCH 0547/1030] 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 8b7531b97775d3facefde41682dd19e9dd3e11f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:44:03 +0200 Subject: [PATCH 0548/1030] added helper attributes to placeholder so there is no need to access it's 'data' --- .../workfile/abstract_template_loader.py | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index d934c50daf..5ecc154ea4 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,11 +231,11 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.data['node'] for placeholder in placeholders] + [placeholder.name] for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( - placeholder.data['node'] + placeholder.name )) placeholder_representations = self.get_placeholder_representations( placeholder, @@ -246,7 +246,7 @@ class AbstractTemplateLoader: if not placeholder_representations: self.log.info( "There's no representation for this placeholder: " - "{}".format(placeholder.data['node']) + "{}".format(placeholder.name) ) continue @@ -264,8 +264,8 @@ class AbstractTemplateLoader: "Loader arguments used : {}".format( representation['context']['asset'], representation['context']['subset'], - placeholder.loader, - placeholder.data['loader_args'])) + placeholder.loader_name, + placeholder.loader_args)) try: container = self.load( @@ -307,19 +307,22 @@ class AbstractTemplateLoader: def load(self, placeholder, loaders_by_name, last_representation): repre = get_representation_context(last_representation) return load_with_repre_context( - loaders_by_name[placeholder.loader], + loaders_by_name[placeholder.loader_name], repre, - options=parse_loader_args(placeholder.data['loader_args'])) + options=parse_loader_args(placeholder.loader_args)) def load_succeed(self, placeholder, container): placeholder.parent_in_hierarchy(container) def load_failed(self, placeholder, last_representation): - self.log.warning("Got error trying to load {}:{} with {}\n\n" - "{}".format(last_representation['context']['asset'], - last_representation['context']['subset'], - placeholder.loader, - traceback.format_exc())) + self.log.warning( + "Got error trying to load {}:{} with {}".format( + last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader_name + ), + exc_info=True + ) def postload(self, placeholder): placeholder.clean() @@ -398,6 +401,7 @@ class AbstractPlaceholder: def __init__(self, node): self._log = None + self._name = node self.get_data(node) @property @@ -409,6 +413,17 @@ class AbstractPlaceholder: def __repr__(self): return "< {} {} >".format(self.__class__.__name__, self.name) + @property + def name(self): + return self._name + + @property + def loader_args(self): + return self.data["loader_args"] + + @property + def builder_type(self): + return self.data["builder_type"] def order(self): """Get placeholder order. @@ -423,12 +438,15 @@ class AbstractPlaceholder: return self.data.get('order') @property - def loader(self): - """Return placeholder loader type + def loader_name(self): + """Return placeholder loader type. + Returns: - string: Loader name + str: Loader name that will be used to load placeholder + representations. """ - return self.data.get('loader') + + return self.data["loader"] @property def is_context(self): From 2d7910a26410936f1d23282b9011780cccfc8680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:46:14 +0200 Subject: [PATCH 0549/1030] renamed 'attributes' to 'required_keys' and 'optional_attributes' to 'optional_keys' --- openpype/hosts/maya/api/template_loader.py | 8 +++-- .../workfile/abstract_template_loader.py | 35 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index f553730186..ecffafc93d 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -98,11 +98,11 @@ class MayaPlaceholder(AbstractPlaceholder): """Concrete implementation of AbstractPlaceholder for maya """ - optional_attributes = {'asset', 'subset', 'hierarchy'} + optional_keys = {'asset', 'subset', 'hierarchy'} def get_data(self, node): user_data = dict() - for attr in self.attributes.union(self.optional_attributes): + for attr in self.required_keys.union(self.optional_keys): attribute_name = '{}.{}'.format(node, attr) if not cmds.attributeQuery(attr, node=node, exists=True): print("{} not found".format(attribute_name)) @@ -112,7 +112,9 @@ class MayaPlaceholder(AbstractPlaceholder): asString=True) user_data['parent'] = ( cmds.getAttr(node + '.parent', asString=True) - or node.rpartition('|')[0] or "") + or node.rpartition('|')[0] + or "" + ) user_data['node'] = node if user_data['parent']: siblings = cmds.listRelatives(user_data['parent'], children=True) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5ecc154ea4..56fb31fa0c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -377,15 +377,17 @@ class AbstractTemplateLoader: @six.add_metaclass(ABCMeta) class AbstractPlaceholder: - """Abstraction of placeholders logic + """Abstraction of placeholders logic. + Properties: - attributes: A list of mandatory attribute to decribe placeholder + required_keys: A list of mandatory keys to decribe placeholder and assets to load. - optional_attributes: A list of optional attribute to decribe + optional_keys: A list of optional keys to decribe placeholder and assets to load loader: Name of linked loader to use while loading assets is_context: Is placeholder linked to context asset (or to linked assets) + Methods: is_repres_valid: loader: @@ -395,9 +397,15 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'family', 'representation', - 'order', 'loader', 'loader_args'} - optional_attributes = {} + required_keys = { + "builder_type", + "family", + "representation", + "order", + "loader", + "loader_args" + } + optional_keys = {} def __init__(self, node): self._log = None @@ -459,15 +467,18 @@ class AbstractPlaceholder: return self.data.get('builder_type') == 'context_asset' def is_valid(self): - """Test validity of placeholder - i.e.: every attributes exists in placeholder data + """Test validity of placeholder. + + i.e.: every required key exists in placeholder data + Returns: - Bool: True if every attributes are a key of data + bool: True if every key is in data """ - if set(self.attributes).issubset(self.data.keys()): - self.log.debug("Valid placeholder: {}".format(self.data["node"])) + + if set(self.required_keys).issubset(self.data.keys()): + self.log.debug("Valid placeholder : {}".format(self.name)) return True - self.log.info("Placeholder is not valid: {}".format(self.data["node"])) + self.log.info("Placeholder is not valid : {}".format(self.name)) return False @abstractmethod From 736123d1c2496df1604d1b0c84df5a2646cc51f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:48:47 +0200 Subject: [PATCH 0550/1030] modified 'is_context' property --- .../pipeline/workfile/abstract_template_loader.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 56fb31fa0c..a1d188ea6c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -458,14 +458,22 @@ class AbstractPlaceholder: @property def is_context(self): - """Return placeholder type + """Check if is placeholder context type. + context_asset: For loading current asset linked_asset: For loading linked assets + + Question: + There seems to be more build options and this property is not used, + should be removed? + Returns: bool: true if placeholder is a context placeholder """ - return self.data.get('builder_type') == 'context_asset' + return self.builder_type == "context_asset" + + @property def is_valid(self): """Test validity of placeholder. From 0f5ec0f0c4cbd4db8c4968db75f6375b6bdf7f59 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Jul 2022 17:54:51 +0200 Subject: [PATCH 0551/1030] 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 0552/1030] 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 0553/1030] 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 0554/1030] 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 0555/1030] 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 0556/1030] 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 0557/1030] 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 0558/1030] 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 0559/1030] 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 0560/1030] 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 0561/1030] 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 0562/1030] 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 0563/1030] 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 0564/1030] 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 0565/1030] 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 0566/1030] [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 0567/1030] 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 0568/1030] 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 0569/1030] 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 0570/1030] 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 0571/1030] 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 0572/1030] 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 0573/1030] 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 0574/1030] 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 0575/1030] 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 0576/1030] :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 0577/1030] :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 0578/1030] :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 0579/1030] :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 0580/1030] :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 0581/1030] 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 0582/1030] 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 0583/1030] 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 0584/1030] 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 0585/1030] 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 0586/1030] 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 0587/1030] 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 0588/1030] :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 0589/1030] 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 0590/1030] 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 0591/1030] 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 0592/1030] 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 0593/1030] 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 0594/1030] 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 0595/1030] [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 0596/1030] 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 0597/1030] 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 0598/1030] :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 0599/1030] 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 0600/1030] 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 0601/1030] 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 0602/1030] 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 0603/1030] 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 0604/1030] :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 0605/1030] 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 0606/1030] 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 0607/1030] 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 0608/1030] 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 0609/1030] 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 0610/1030] 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 0611/1030] 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 0612/1030] 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 0613/1030] 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 0614/1030] "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 0615/1030] 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 0616/1030] 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 0617/1030] 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 0618/1030] 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 0619/1030] :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 0620/1030] :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 0621/1030] :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 0622/1030] 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 0623/1030] 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 0624/1030] :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 0625/1030] :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 0626/1030] :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 0627/1030] :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 0628/1030] :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 0629/1030] 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 0630/1030] 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 0631/1030] 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 0632/1030] 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 0633/1030] 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 0634/1030] 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 0635/1030] 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 0636/1030] 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 49799c2d8871a52fb1fd8210b31a1e51fd5f3f2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 18:26:38 +0200 Subject: [PATCH 0637/1030] fix merge conflict --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1d188ea6c..5d8d79397a 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,7 +231,7 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.name] for placeholder in placeholders] + [placeholder.name for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( 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 0638/1030] :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 7d1f1bb064190873beee61c0a4eb4df598747c88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 09:50:11 +0200 Subject: [PATCH 0639/1030] remove extensions arguments --- openpype/client/entities.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 57c38784b0..a3fcd01f80 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1216,7 +1216,6 @@ def get_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, @@ -1230,7 +1229,6 @@ def get_archived_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, context_filters=None, names_by_version_ids=None, fields=None @@ -1247,8 +1245,6 @@ def get_archived_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. fields (Iterable[str]): Fields that should be returned. All fields are @@ -1263,7 +1259,6 @@ def get_archived_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, From 08a9cb207385a0906cc56d063c19de3aa88eb51d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 10:08:07 +0200 Subject: [PATCH 0640/1030] 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 0641/1030] 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 0642/1030] 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 0643/1030] 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 0644/1030] 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 0645/1030] :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 0646/1030] :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 0647/1030] :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 0648/1030] [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 0649/1030] 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 0650/1030] 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 0651/1030] 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 0652/1030] 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 0653/1030] 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 8db8ada9642bcdf2c5f364fbf78c902344b1613e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:46:32 +0200 Subject: [PATCH 0654/1030] changed 'node' variable to 'identifier' and added it's docstrings --- .../workfile/abstract_template_loader.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5d8d79397a..16287bbd4e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -384,17 +384,11 @@ class AbstractPlaceholder: and assets to load. optional_keys: A list of optional keys to decribe placeholder and assets to load - loader: Name of linked loader to use while loading assets - is_context: Is placeholder linked - to context asset (or to linked assets) + loader_name: Name of linked loader to use while loading assets - Methods: - is_repres_valid: - loader: - order: - is_valid: - get_data: - parent_in_hierachy: + Args: + identifier (str): Placeholder identifier. Should be possible to be + used as identifier in "a scene" (e.g. unique node name). """ required_keys = { @@ -407,10 +401,10 @@ class AbstractPlaceholder: } optional_keys = {} - def __init__(self, node): + def __init__(self, identifier): self._log = None - self._name = node - self.get_data(node) + self._name = identifier + self.get_data(identifier) @property def log(self): From 5d0cd42a8133bcf7d65bbcef0c7b093ef058d7b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:47:01 +0200 Subject: [PATCH 0655/1030] renamed 'order' method to 'get_order' --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 16287bbd4e..fe1f15c140 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -336,7 +336,7 @@ class AbstractTemplateLoader: placeholders = map(placeholder_class, self.get_template_nodes()) valid_placeholders = filter(placeholder_class.is_valid, placeholders) sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.order) + key=placeholder_class.get_order) return sorted_placeholders @abstractmethod @@ -427,17 +427,24 @@ class AbstractPlaceholder: def builder_type(self): return self.data["builder_type"] + @property def order(self): - """Get placeholder order. + return self.data["order"] + + def get_order(self): + """Placeholder order. + Order is used to sort them by priority Priority is lowset first, highest last (ex: 1: First to load 100: Last to load) + Returns: - Int: Order priority + int: Order priority """ - return self.data.get('order') + + return self.order @property def loader_name(self): From 7e8e61c0e4d51334d6de0d1f9cd672fa0dae5313 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:14 +0200 Subject: [PATCH 0656/1030] changed 'get_data' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index fe1f15c140..66943eafe7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -537,10 +537,12 @@ class AbstractPlaceholder: pass @abstractmethod - def get_data(self, node): - """ - Collect placeholders information. + def get_data(self, identifier): + """Collect information about placeholder by identifier. + Args: - node (AnyNode): A unique node decided by Placeholder implementation + identifier (str): A unique placeholder identifier defined by + implementation. """ + pass From a1cd1890d6db952e4feee357e204444aed0015ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:28 +0200 Subject: [PATCH 0657/1030] modified 'parent_in_hierarchy' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 66943eafe7..a1629d9b79 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -491,13 +491,13 @@ class AbstractPlaceholder: return False @abstractmethod - def parent_in_hierarchy(self, containers): - """Place container in correct hierarchy - given by placeholder + def parent_in_hierarchy(self, container): + """Place loaded container in correct hierarchy given by placeholder + Args: - containers (String): Container name returned back by - placeholder's loader. + container (Dict[str, Any]): Loaded container created by loader. """ + pass @abstractmethod From 56150d4abb72d8b0025a7724e002eab792aa34a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:48 +0200 Subject: [PATCH 0658/1030] removed unused method 'convert_to_db_filters' --- .../pipeline/workfile/abstract_template_loader.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1629d9b79..c36e489017 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -523,19 +523,6 @@ class AbstractPlaceholder: pass - @abstractmethod - def convert_to_db_filters(self, current_asset, linked_asset): - """map current placeholder data as a db filter - args: - current_asset (String): Name of current asset in context - linked asset (list[String]) : Names of assets linked to - current asset in context - Returns: - dict: a dictionnary describing a filter to look for asset in - a database - """ - pass - @abstractmethod def get_data(self, identifier): """Collect information about placeholder by identifier. From 56bbbdbd583b51ba07bac08e753fb5a2050a768f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:49:20 +0200 Subject: [PATCH 0659/1030] removed unused import --- openpype/pipeline/workfile/abstract_template_loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index c36e489017..725ab1dab3 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -1,8 +1,6 @@ import os from abc import ABCMeta, abstractmethod -import traceback - import six import logging from functools import reduce From 26572719c9eb82dc6f818665c2544ef376d6769a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 8 Aug 2022 17:01:40 +0100 Subject: [PATCH 0660/1030] 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 0661/1030] 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 0662/1030] 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 0663/1030] 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 0664/1030] 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 0665/1030] [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 0666/1030] [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 0667/1030] 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 0668/1030] 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 0669/1030] 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 0670/1030] 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 0671/1030] 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 0672/1030] 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 0673/1030] 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 0674/1030] 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 0675/1030] 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 0676/1030] 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 0677/1030] 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 0678/1030] 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 0679/1030] 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 0680/1030] 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 0681/1030] 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 0682/1030] 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 0683/1030] 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 0684/1030] 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 0685/1030] 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 0686/1030] 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 0687/1030] 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 0688/1030] 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 0689/1030] 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 0690/1030] 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 0691/1030] 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 0692/1030] 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 0693/1030] [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 0694/1030] 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 0695/1030] 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 0696/1030] 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 0697/1030] 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 0698/1030] 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 0699/1030] 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 0700/1030] 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 0701/1030] 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 02edebad41f26680f0f7ceb3b2b21fe6cfebebab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 18:33:03 +0200 Subject: [PATCH 0702/1030] fix import string --- openpype/pipeline/workfile/build_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index df6fe3514a..e6396578c5 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -15,7 +15,7 @@ from .build_template_exceptions import ( MissingTemplateLoaderClass ) -_module_path_format = 'openpype.{host}.template_loader' +_module_path_format = 'openpype.hosts.{host}.api.template_loader' def build_workfile_template(*args): From bbf113cac4c8dd0dde7cca18646641107a505b44 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 11:54:07 +0200 Subject: [PATCH 0703/1030] 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 0704/1030] 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 4f9d1c34e22d22729fc99fc92abcfaeb16ca253b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:39:27 +0200 Subject: [PATCH 0705/1030] added IHostModule to be able identify module representing a host --- openpype/modules/interfaces.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 334485cab2..424dd158fd 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,4 +1,4 @@ -from abc import abstractmethod +from abc import abstractmethod, abstractproperty from openpype import resources @@ -320,3 +320,13 @@ class ISettingsChangeListener(OpenPypeInterface): self, old_value, new_value, changes, project_name, new_value_metadata ): pass + + +class IHostModule(OpenPypeInterface): + """Module which also contain a host implementation.""" + + @abstractproperty + def host_name(self): + """Name of host which module represents.""" + + pass From c86ab4fecfbb6e502723bb86dbbf0748a8135753 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:08 +0200 Subject: [PATCH 0706/1030] added ability to inmport host modules on load modules --- openpype/modules/base.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1bd343fd07..32fa4d2f31 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -140,7 +140,7 @@ class _LoadCache: def get_default_modules_dir(): """Path to default OpenPype modules.""" - current_dir = os.path.abspath(os.path.dirname(__file__)) + current_dir = os.path.dirname(os.path.abspath(__file__)) output = [] for folder_name in ("default_modules", ): @@ -298,6 +298,8 @@ def _load_modules(): # Add current directory at first place # - has small differences in import logic current_dir = os.path.abspath(os.path.dirname(__file__)) + hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts") + module_dirs.insert(0, hosts_dir) module_dirs.insert(0, current_dir) processed_paths = set() @@ -314,6 +316,7 @@ def _load_modules(): continue is_in_current_dir = dirpath == current_dir + is_in_host_dir = dirpath == hosts_dir for filename in os.listdir(dirpath): # Ignore filenames if filename in IGNORED_FILENAMES: @@ -353,6 +356,24 @@ def _load_modules(): sys.modules[new_import_str] = default_module setattr(openpype_modules, basename, default_module) + elif is_in_host_dir: + import_str = "openpype.hosts.{}".format(basename) + new_import_str = "{}.{}".format(modules_key, basename) + # Until all hosts are converted to be able use them as + # modules is this error check needed + try: + default_module = __import__( + import_str, fromlist=("", ) + ) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) + + except Exception: + log.warning( + "Failed to import host folder {}".format(basename), + exc_info=True + ) + elif os.path.isdir(fullpath): import_module_from_dirpath(dirpath, filename, modules_key) From 5736b9133cd8f2b2a62146cf6c9fb8310a74f4b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:32 +0200 Subject: [PATCH 0707/1030] added helper methods to be able get host module by host name --- openpype/modules/base.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 32fa4d2f31..ef577e5aa2 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -825,6 +825,45 @@ class ModulesManager: output.extend(hook_paths) return output + def get_host_module(self, host_name): + """Find host module by host name. + + Args: + host_name (str): Host name for which is found host module. + + Returns: + OpenPypeModule: Found host module by name. + None: There was not found module inheriting IHostModule which has + host name set to passed 'host_name'. + """ + + from openpype_interfaces import IHostModule + + for module in self.get_enabled_modules(): + if ( + isinstance(module, IHostModule) + and module.host_name == host_name + ): + return module + return None + + def get_host_names(self): + """List of available host names based on host modules. + + Returns: + Iterable[str]: All available host names based on enabled modules + inheriting 'IHostModule'. + """ + + from openpype_interfaces import IHostModule + + host_names = { + module.host_name + for module in self.get_enabled_modules() + if isinstance(module, IHostModule) + } + return host_names + def print_report(self): """Print out report of time spent on modules initialization parts. From a2dadc85bd51c2fc25baf5098ec5fdcf08e00269 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:42:54 +0200 Subject: [PATCH 0708/1030] added 'OpenPypeMaya' module --- openpype/hosts/maya/__init__.py | 7 +++++++ openpype/hosts/maya/module.py | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 openpype/hosts/maya/module.py diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index c1c82c62e5..2178534b89 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,4 +1,5 @@ import os +from .module import OpenPypeMaya def add_implementation_envs(env, _app): @@ -25,3 +26,9 @@ def add_implementation_envs(env, _app): for key, value in defaults.items(): if not env.get(key): env[key] = value + + +__all__ = ( + "OpenPypeMaya", + "add_implementation_envs", +) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py new file mode 100644 index 0000000000..8dfd96d4ab --- /dev/null +++ b/openpype/hosts/maya/module.py @@ -0,0 +1,10 @@ +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + + +class OpenPypeMaya(OpenPypeModule, IHostModule): + name = "openpype_maya" + host_name = "maya" + + def initialize(self, module_settings): + self.enabled = True From 88be0405986196894b16ae5cb98d303d3d0e9598 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:43:50 +0200 Subject: [PATCH 0709/1030] modev 'add_implementation_envs' to maya module and application knows that it should look there --- openpype/hosts/maya/__init__.py | 28 ---------------------------- openpype/hosts/maya/module.py | 27 +++++++++++++++++++++++++++ openpype/lib/applications.py | 6 ++++-- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 2178534b89..72b4d5853c 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,34 +1,6 @@ -import os from .module import OpenPypeMaya -def add_implementation_envs(env, _app): - # Add requirements to PYTHONPATH - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_python_paths = [ - os.path.join(pype_root, "openpype", "hosts", "maya", "startup") - ] - old_python_path = env.get("PYTHONPATH") or "" - for path in old_python_path.split(os.pathsep): - if not path: - continue - - norm_path = os.path.normpath(path) - if norm_path not in new_python_paths: - new_python_paths.append(norm_path) - - env["PYTHONPATH"] = os.pathsep.join(new_python_paths) - - # Set default values if are not already set via settings - defaults = { - "OPENPYPE_LOG_NO_COLORS": "Yes" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - __all__ = ( "OpenPypeMaya", - "add_implementation_envs", ) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 8dfd96d4ab..0af68788bc 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -1,6 +1,9 @@ +import os from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostModule +MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + class OpenPypeMaya(OpenPypeModule, IHostModule): name = "openpype_maya" @@ -8,3 +11,27 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to PYTHONPATH + new_python_paths = [ + os.path.join(MAYA_ROOT_DIR, "startup") + ] + old_python_path = env.get("PYTHONPATH") or "" + for path in old_python_path.split(os.pathsep): + if not path: + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_python_paths: + new_python_paths.append(norm_path) + + env["PYTHONPATH"] = os.pathsep.join(new_python_paths) + + # Set default values if are not already set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "Yes" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index da8623ea13..e47ec8cd11 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1508,8 +1508,10 @@ def prepare_app_environments( final_env = None # Add host specific environments if app.host_name and implementation_envs: - module = __import__("openpype.hosts", fromlist=[app.host_name]) - host_module = getattr(module, app.host_name, None) + host_module = modules_manager.get_host_module(app.host_name) + if not host_module: + module = __import__("openpype.hosts", fromlist=[app.host_name]) + host_module = getattr(module, app.host_name, None) add_implementation_envs = None if host_module: add_implementation_envs = getattr( From 8fe20486a91a8943b847b610d342df163dee3e1b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Aug 2022 12:51:01 +0200 Subject: [PATCH 0710/1030] 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 0711/1030] 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 32176ba234cf1bff28e15c4efce51cc00d641037 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:06 +0200 Subject: [PATCH 0712/1030] modules does not have to inherit from ILaunchHookPaths and application is passed to 'collect_launch_hook_paths --- openpype/lib/applications.py | 4 +++- openpype/modules/base.py | 38 ++++++++++++++++++++++++++++------ openpype/modules/interfaces.py | 33 ++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e47ec8cd11..5443320960 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -996,7 +996,9 @@ class ApplicationLaunchContext: paths.append(path) # Load modules paths - paths.extend(self.modules_manager.collect_launch_hook_paths()) + paths.extend( + self.modules_manager.collect_launch_hook_paths(self.application) + ) return paths diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ef577e5aa2..e26075283d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -789,24 +789,50 @@ class ModulesManager: output.extend(paths) return output - def collect_launch_hook_paths(self): - """Helper to collect hooks from modules inherited ILaunchHookPaths. + def collect_launch_hook_paths(self, app): + """Helper to collect application launch hooks. + + It used to be based on 'ILaunchHookPaths' which is not true anymore. + Module just have to have implemented 'get_launch_hook_paths' method. + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. Returns: list: Paths to launch hook directories. """ - from openpype_interfaces import ILaunchHookPaths str_type = type("") expected_types = (list, tuple, set) output = [] for module in self.get_enabled_modules(): - # Skip module that do not inherit from `ILaunchHookPaths` - if not isinstance(module, ILaunchHookPaths): + # Skip module if does not have implemented 'get_launch_hook_paths' + func = getattr(module, "get_launch_hook_paths", None) + if func is None: + continue + + func = module.get_launch_hook_paths + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 + + # Pass application argument if method expect it. + try: + if expect_args: + hook_paths = func(app) + else: + hook_paths = func() + except Exception: + self.log.warning( + "Failed to call 'get_launch_hook_paths'", + exc_info=True + ) continue - hook_paths = module.get_launch_hook_paths() if not hook_paths: continue diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 424dd158fd..de9ba13800 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -50,12 +50,32 @@ class IPluginPaths(OpenPypeInterface): class ILaunchHookPaths(OpenPypeInterface): """Module has launch hook paths to return. + Modules does not have to inherit from this interface (changed 8.11.2022). + Module just have to have implemented 'get_launch_hook_paths' to be able use + the advantage. + Expected result is list of paths. ["path/to/launch_hooks_dir"] """ @abstractmethod - def get_launch_hook_paths(self): + def get_launch_hook_paths(self, app): + """Paths to directory with application launch hooks. + + Method can be also defined without arguments. + ```python + def get_launch_hook_paths(self): + return [] + ``` + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. + + Returns: + Iterable[str]: Path to directories where launch hooks can be found. + """ + pass @@ -66,6 +86,7 @@ class ITrayModule(OpenPypeInterface): The module still must be usable if is not used in tray even if would do nothing. """ + tray_initialized = False _tray_manager = None @@ -78,16 +99,19 @@ class ITrayModule(OpenPypeInterface): This is where GUIs should be loaded or tray specific parts should be prepared. """ + pass @abstractmethod def tray_menu(self, tray_menu): """Add module's action to tray menu.""" + pass @abstractmethod def tray_start(self): """Start procedure in Pype tray.""" + pass @abstractmethod @@ -96,6 +120,7 @@ class ITrayModule(OpenPypeInterface): This is place where all threads should be shut. """ + pass def execute_in_main_thread(self, callback): @@ -104,6 +129,7 @@ class ITrayModule(OpenPypeInterface): Some callbacks need to be processed on main thread (menu actions must be added on main thread or they won't get triggered etc.) """ + if not self.tray_initialized: # TODO Called without initialized tray, still main thread needed try: @@ -128,6 +154,7 @@ class ITrayModule(OpenPypeInterface): msecs (int): Duration of message visibility in miliseconds. Default is 10000 msecs, may differ by Qt version. """ + if self._tray_manager: self._tray_manager.show_tray_message(title, message, icon, msecs) @@ -280,16 +307,19 @@ class ITrayService(ITrayModule): def set_service_running_icon(self): """Change icon of an QAction to green circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_running()) def set_service_failed_icon(self): """Change icon of an QAction to red circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_failed()) def set_service_idle_icon(self): """Change icon of an QAction to orange circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_idle()) @@ -303,6 +333,7 @@ class ISettingsChangeListener(OpenPypeInterface): "publish": ["path/to/publish_plugins"] } """ + @abstractmethod def on_system_settings_save( self, old_value, new_value, changes, new_value_metadata From 0ae844401cc271ef0edf9b16a5dda4893dd7bcfd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:35 +0200 Subject: [PATCH 0713/1030] maya is registering it's launch hooks --- openpype/hosts/maya/module.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 0af68788bc..e058f1cef5 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -35,3 +35,10 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): for key, value in defaults.items(): if not env.get(key): env[key] = value + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(MAYA_ROOT_DIR, "hooks") + ] From 58af54c4437d0495f2f00c7962455bd8cdbf1a1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:33:35 +0200 Subject: [PATCH 0714/1030] let host module add it's prelaunch hooks and don't guess it --- openpype/lib/applications.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 5443320960..e23cc6215f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -962,32 +962,24 @@ class ApplicationLaunchContext: # TODO load additional studio paths from settings import openpype - pype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) + openpype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) - # --- START: Backwards compatibility --- - hooks_dir = os.path.join(pype_dir, "hooks") + global_hooks_dir = os.path.join(openpype_dir, "hooks") - subfolder_names = ["global"] - if self.host_name: - subfolder_names.append(self.host_name) - for subfolder_name in subfolder_names: - path = os.path.join(hooks_dir, subfolder_name) - if ( - os.path.exists(path) - and os.path.isdir(path) - and path not in paths - ): - paths.append(path) - # --- END: Backwards compatibility --- - - subfolders_list = [ - ["hooks"] + hooks_dirs = [ + global_hooks_dir ] if self.host_name: - subfolders_list.append(["hosts", self.host_name, "hooks"]) + # If host requires launch hooks and is module then launch hooks + # should be collected using 'collect_launch_hook_paths' + # - module have to implement 'get_launch_hook_paths' + host_module = self.modules_manager.get_host_module(self.host_name) + if not host_module: + hooks_dirs.append(os.path.join( + openpype_dir, "hosts", self.host_name, "hooks" + )) - for subfolders in subfolders_list: - path = os.path.join(pype_dir, *subfolders) + for path in hooks_dirs: if ( os.path.exists(path) and os.path.isdir(path) From 7d304d0f8695775e6f3e49d2b0271ac2b8564883 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:39:32 +0200 Subject: [PATCH 0715/1030] host module can define workfile extensions --- openpype/lib/applications.py | 23 ++++++++++++++++------- openpype/modules/interfaces.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e23cc6215f..0f380d0f4b 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1303,6 +1303,7 @@ def get_app_environments_for_context( dict: Environments for passed context and application. """ + from openpype.modules import ModulesManager from openpype.pipeline import AvalonMongoDB, Anatomy # Avalon database connection @@ -1315,8 +1316,6 @@ def get_app_environments_for_context( asset_doc = get_asset_by_name(project_name, asset_name) if modules_manager is None: - from openpype.modules import ModulesManager - modules_manager = ModulesManager() # Prepare app object which can be obtained only from ApplciationManager @@ -1343,7 +1342,7 @@ def get_app_environments_for_context( }) prepare_app_environments(data, env_group, modules_manager) - prepare_context_environments(data, env_group) + prepare_context_environments(data, env_group, modules_manager) # Discard avalon connection dbcon.uninstall() @@ -1564,7 +1563,7 @@ def apply_project_environments_value( return env -def prepare_context_environments(data, env_group=None): +def prepare_context_environments(data, env_group=None, modules_manager=None): """Modify launch environments with context data for launched host. Args: @@ -1652,10 +1651,10 @@ def prepare_context_environments(data, env_group=None): data["env"]["AVALON_APP"] = app.host_name data["env"]["AVALON_WORKDIR"] = workdir - _prepare_last_workfile(data, workdir) + _prepare_last_workfile(data, workdir, modules_manager) -def _prepare_last_workfile(data, workdir): +def _prepare_last_workfile(data, workdir, modules_manager): """last workfile workflow preparation. Function check if should care about last workfile workflow and tries @@ -1670,8 +1669,13 @@ def _prepare_last_workfile(data, workdir): result will be stored. workdir (str): Path to folder where workfiles should be stored. """ + + from openpype.modules import ModulesManager from openpype.pipeline import HOST_WORKFILE_EXTENSIONS + if not modules_manager: + modules_manager = ModulesManager() + log = data["log"] _workdir_data = data.get("workdir_data") @@ -1719,7 +1723,12 @@ def _prepare_last_workfile(data, workdir): # Last workfile path last_workfile_path = data.get("last_workfile_path") or "" if not last_workfile_path: - extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + host_module = modules_manager.get_host_module(app.host_name) + if host_module: + extensions = host_module.get_workfile_extensions() + else: + extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + if extensions: anatomy = data["anatomy"] project_settings = data["project_settings"] diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index de9ba13800..14f49204ee 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -361,3 +361,14 @@ class IHostModule(OpenPypeInterface): """Name of host which module represents.""" pass + + def get_workfile_extensions(self): + """Define workfile extensions for host. + + Not all hosts support workfiles thus this is optional implementation. + + Returns: + List[str]: Extensions used for workfiles with dot. + """ + + return [] From 9b623c1dd3e335aeb48d3428f6a0cba5e5793e51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 14:14:21 +0200 Subject: [PATCH 0716/1030] maya define it's workfile extensions only in module itself --- openpype/hosts/maya/api/workio.py | 4 +--- openpype/hosts/maya/module.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/workio.py b/openpype/hosts/maya/api/workio.py index fd4961c4bf..8c31974c73 100644 --- a/openpype/hosts/maya/api/workio.py +++ b/openpype/hosts/maya/api/workio.py @@ -2,11 +2,9 @@ import os from maya import cmds -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["maya"] + return [".ma", ".mb"] def has_unsaved_changes(): diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index e058f1cef5..5a215be8d2 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -42,3 +42,6 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): return [ os.path.join(MAYA_ROOT_DIR, "hooks") ] + + def get_workfile_extensions(self): + return [".ma", ".mb"] From 25616886bff2b6fda0b4c9646ea9256389ba248f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:08:14 +0200 Subject: [PATCH 0717/1030] raise and error when nothing is selected --- openpype/hosts/maya/api/lib_template_builder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 855c72e361..34a8450a26 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,6 +40,9 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) + if not selection: + raise ValueError("Nothing is selected") + placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) From 683468c5633a42b8c5e80510ab060f981452d02c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:08 +0200 Subject: [PATCH 0718/1030] use 'filter_profiles' function for profiles filtering --- .../workfile/abstract_template_loader.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 725ab1dab3..51d06cdb3f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,7 +7,11 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings -from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.lib import ( + Logger, + filter_profiles, + get_linked_assets, +) from openpype.pipeline import legacy_io, Anatomy from openpype.pipeline.load import ( get_loaders_by_name, @@ -167,22 +171,23 @@ class AbstractTemplateLoader: anatomy = Anatomy(project_name) project_settings = get_project_settings(project_name) - build_info = project_settings[host_name]['templated_workfile_build'] - profiles = build_info['profiles'] + build_info = project_settings[host_name]["templated_workfile_build"] + profile = filter_profiles( + build_info["profiles"], + { + "task_types": task_type, + "tasks": task_name + } + ) - for prf in profiles: - if prf['task_types'] and task_type not in prf['task_types']: - continue - if prf['tasks'] and task_name not in prf['tasks']: - continue - path = prf['path'] - break - else: # IF no template were found (no break happened) + if not profile: raise TemplateProfileNotFound( "No matching profile found for task '{}' of type '{}' " "with host '{}'".format(task_name, task_type, host_name) ) - if path is None: + + path = profile["path"] + if not path: raise TemplateLoadingFailed( "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " From bb9a16100acd9a7d94ec6ff6ea15891916eea580 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:23 +0200 Subject: [PATCH 0719/1030] removed unnecessary finally statement --- openpype/pipeline/workfile/abstract_template_loader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 51d06cdb3f..0ed32033af 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -205,9 +205,8 @@ class AbstractTemplateLoader: raise KeyError( "Could not solve key '{}' in template path '{}'".format( missing_key, path)) - finally: - solved_path = os.path.normpath(solved_path) + solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): raise TemplateNotFound( "Template found in openPype settings for task '{}' with host " From 12a8307a8331334ee9700efba2127211ea332ff0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:40:02 +0200 Subject: [PATCH 0720/1030] simplified path formatting --- .../workfile/abstract_template_loader.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0ed32033af..5afec56d71 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -8,6 +8,7 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import ( + StringTemplate, Logger, filter_profiles, get_linked_assets, @@ -192,19 +193,35 @@ class AbstractTemplateLoader: "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " "Settings\\Profiles".format(host_name.title())) - try: - solved_path = None - while True: + + # Try fill path with environments and anatomy roots + fill_data = { + key: value + for key, value in os.environ.items() + } + fill_data["root"] = anatomy.roots + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + + if path and os.path.exists(path): + self.log.info("Found template at: '{}'".format(path)) + return path + + solved_path = None + while True: + try: solved_path = anatomy.path_remapper(path) - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - except KeyError as missing_key: - raise KeyError( - "Could not solve key '{}' in template path '{}'".format( - missing_key, path)) + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): @@ -213,7 +230,7 @@ class AbstractTemplateLoader: "'{}' does not exists. (Not found : {})".format( task_name, host_name, solved_path)) - self.log.info("Found template at : '{}'".format(solved_path)) + self.log.info("Found template at: '{}'".format(solved_path)) return solved_path From 748dcf1ad207edd3dbf3bc98120d9e46bf9b39e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:51:27 +0200 Subject: [PATCH 0721/1030] fix filter and sort --- .../workfile/abstract_template_loader.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5afec56d71..1c8ede25e6 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -351,11 +351,15 @@ class AbstractTemplateLoader: self.populate_template(ignored_ids=loaded_containers_ids) def get_placeholders(self): - placeholder_class = self.placeholder_class - placeholders = map(placeholder_class, self.get_template_nodes()) - valid_placeholders = filter(placeholder_class.is_valid, placeholders) - sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.get_order) + placeholders = map(self.placeholder_class, self.get_template_nodes()) + valid_placeholders = filter( + lambda i: i.is_valid, + placeholders + ) + sorted_placeholders = list(sorted( + valid_placeholders, + key=lambda i: i.order + )) return sorted_placeholders @abstractmethod @@ -450,21 +454,6 @@ class AbstractPlaceholder: def order(self): return self.data["order"] - def get_order(self): - """Placeholder order. - - Order is used to sort them by priority - Priority is lowset first, highest last - (ex: - 1: First to load - 100: Last to load) - - Returns: - int: Order priority - """ - - return self.order - @property def loader_name(self): """Return placeholder loader type. From 7eaa278c741ceaa30daf056dd17ec9e4b4ceed10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:54:19 +0200 Subject: [PATCH 0722/1030] removed invalid default setting for templates --- openpype/settings/defaults/project_settings/maya.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 9c2c737ece..e9109abd22 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -123,7 +123,6 @@ "defaults": [ "Main" ] - }, "CreateAss": { "enabled": true, @@ -969,13 +968,7 @@ ] }, "templated_workfile_build": { - "profiles": [ - { - "task_types": [], - "tasks": [], - "path": "/path/to/your/template" - } - ] + "profiles": [] }, "filters": { "preset 1": { From cf0ac3f8b56c5d0f63ab6fa00966fcfe6b76ee08 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 11 Aug 2022 17:10:02 +0200 Subject: [PATCH 0723/1030] blender ops refresh manager after process events --- openpype/hosts/blender/api/lib.py | 2 +- openpype/hosts/blender/api/ops.py | 17 ++++++++++++----- .../hosts/blender/blender_addon/startup/init.py | 8 +++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 20098c0fe8..9cd1ace821 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -234,7 +234,7 @@ def lsattrs(attrs: Dict) -> List: def read(node: bpy.types.bpy_struct_meta_idprop): """Return user-defined attributes from `node`""" - data = dict(node.get(pipeline.AVALON_PROPERTY)) + data = dict(node.get(pipeline.AVALON_PROPERTY, {})) # Ignore hidden/internal data data = { diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 4f8410da74..e0e09277df 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -26,7 +26,7 @@ PREVIEW_COLLECTIONS: Dict = dict() # This seems like a good value to keep the Qt app responsive and doesn't slow # down Blender. At least on macOS I the interace of Blender gets very laggy if # you make it smaller. -TIMER_INTERVAL: float = 0.01 +TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1 class BlenderApplication(QtWidgets.QApplication): @@ -164,6 +164,12 @@ def _process_app_events() -> Optional[float]: dialog.setDetailedText(detail) dialog.exec_() + # Refresh Manager + if GlobalClass.app: + manager = GlobalClass.app.get_window("WM_OT_avalon_manager") + if manager: + manager.refresh() + if not GlobalClass.is_windows: if OpenFileCacher.opening_file: return TIMER_INTERVAL @@ -192,10 +198,11 @@ class LaunchQtApp(bpy.types.Operator): self._app = BlenderApplication.get_app() GlobalClass.app = self._app - bpy.app.timers.register( - _process_app_events, - persistent=True - ) + if not bpy.app.timers.is_registered(_process_app_events): + bpy.app.timers.register( + _process_app_events, + persistent=True + ) def execute(self, context): """Execute the operator. diff --git a/openpype/hosts/blender/blender_addon/startup/init.py b/openpype/hosts/blender/blender_addon/startup/init.py index 13a4b8a7a1..8dbff8a91d 100644 --- a/openpype/hosts/blender/blender_addon/startup/init.py +++ b/openpype/hosts/blender/blender_addon/startup/init.py @@ -1,4 +1,10 @@ from openpype.pipeline import install_host from openpype.hosts.blender import api -install_host(api) + +def register(): + install_host(api) + + +def unregister(): + pass From 66ee0beaf6d0e09eae6a8a9887a90651618a73f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 17:43:47 +0200 Subject: [PATCH 0724/1030] fix empty or query --- openpype/client/entities.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index f9d3badb1a..c798c0ad6d 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1130,11 +1130,12 @@ def _get_representations( for item in _regex_filters(flatten_filters): for key, value in item.items(): - if key == "$or": - or_queries.append(value) - else: + if key != "$or": query_filter[key] = value + elif value: + or_queries.append(value) + if len(or_queries) == 1: query_filter["$or"] = or_queries[0] elif or_queries: From cd167a9055723c941e34c15fd3d5cc8edbaf481e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 11 Aug 2022 18:00:34 +0200 Subject: [PATCH 0725/1030] Removed submodule vendor/configs/OpenColorIO-Configs --- .gitmodules | 5 +---- vendor/configs/OpenColorIO-Configs | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/.gitmodules b/.gitmodules index bac3132b77..fe93791c4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,4 @@ [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 + url = https://github.com/EvotecIT/PSWriteColor.git \ No newline at end of file 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 37ed6bc897168e42159fb656f06d413b11c601da Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 11 Aug 2022 18:02:21 +0200 Subject: [PATCH 0726/1030] :recycle: change location of ocio configs --- .../maya/plugins/publish/extract_look.py | 18 +- poetry.lock | 821 +++--------------- pyproject.toml | 4 +- 3 files changed, 131 insertions(+), 712 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 0b26e922d5..b425efba6f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -40,15 +40,15 @@ def get_ocio_config_path(profile_folder): Returns: str: Path to vendorized config file. """ - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "configs", - "OpenColorIO-Configs", - profile_folder, - "config.ocio" - ) - + try: + import OpenColorIOConfigs + return os.path.join( + os.path.dirname(OpenColorIOConfigs.__file__), + profile_folder, + "config.ocio" + ) + except ImportError: + return None def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. diff --git a/poetry.lock b/poetry.lock index 919a352505..df8d8ab14a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -48,7 +48,7 @@ aiohttp = ">=3,<4" [[package]] name = "aiohttp-middlewares" -version = "2.0.0" +version = "2.1.0" description = "Collection of useful middlewares for aiohttp applications." category = "main" optional = false @@ -114,7 +114,7 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.11.5" +version = "2.11.7" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -147,7 +147,7 @@ python-versions = ">=3.5" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -155,17 +155,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "autopep8" @@ -181,11 +181,11 @@ toml = "*" [[package]] name = "babel" -version = "2.9.1" +version = "2.10.3" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pytz = ">=2015.7" @@ -236,7 +236,7 @@ python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -279,7 +279,7 @@ test = ["pytest-runner (>=2.7,<3)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)" [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -306,7 +306,7 @@ python-versions = "*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -320,7 +320,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.2" +version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -408,7 +408,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "dropbox" -version = "11.31.0" +version = "11.33.0" description = "Official Dropbox API Client" category = "main" optional = false @@ -433,7 +433,7 @@ prefixed = ">=0.3.2" [[package]] name = "evdev" -version = "1.5.0" +version = "1.6.0" description = "Bindings to the Linux input handling subsystem" category = "main" optional = false @@ -455,7 +455,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "frozenlist" -version = "1.3.0" +version = "1.3.1" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -490,7 +490,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gazu" -version = "0.8.28" +version = "0.8.30" description = "Gazu is a client for Zou, the API to store the data of your CG production." category = "main" optional = false @@ -530,7 +530,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "google-api-core" -version = "2.8.1" +version = "2.8.2" description = "Google API client core library" category = "main" optional = false @@ -539,13 +539,11 @@ python-versions = ">=3.6" [package.dependencies] google-auth = ">=1.25.0,<3.0dev" googleapis-common-protos = ">=1.56.2,<2.0dev" -protobuf = ">=3.15.0,<4.0.0dev" +protobuf = ">=3.15.0,<5.0.0dev" requests = ">=2.18.0,<3.0.0dev" [package.extras] grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-api-python-client" @@ -565,7 +563,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "2.7.0" +version = "2.10.0" description = "Google Authentication Library" category = "main" optional = false @@ -598,14 +596,14 @@ six = "*" [[package]] name = "googleapis-common-protos" -version = "1.56.2" +version = "1.56.4" description = "Common protobufs used in Google APIs" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -protobuf = ">=3.15.0,<4.0.0dev" +protobuf = ">=3.15.0,<5.0.0dev" [package.extras] grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] @@ -631,7 +629,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" optional = false @@ -639,7 +637,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.11.4" +version = "4.12.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -652,7 +650,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -692,15 +690,15 @@ testing = ["colorama", "docopt", "pytest (>=3.1.0)"] [[package]] name = "jeepney" -version = "0.7.1" +version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +trio = ["async-generator", "trio"] +test = ["async-timeout", "trio", "testpath", "pytest-asyncio (>=0.17)", "pytest-trio", "pytest"] [[package]] name = "jinja2" @@ -799,6 +797,21 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "opencolorio-configs" +version = "1.0.2" +description = "Curated set of OpenColorIO Configs for use in OpenPype" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/pypeclub/OpenColorIO-Configs.git" +reference = "main" +resolved_reference = "07c5e865bf2b115b589dd2876ae632cd410821b5" + [[package]] name = "opentimelineio" version = "0.14.0.dev1" @@ -875,14 +888,14 @@ six = "*" [[package]] name = "pillow" -version = "9.1.1" +version = "9.2.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] @@ -930,11 +943,11 @@ python-versions = "*" [[package]] name = "protobuf" -version = "3.19.4" -description = "Protocol Buffers" +version = "4.21.5" +description = "" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "py" @@ -1354,7 +1367,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rsa" -version = "4.8" +version = "4.9" description = "Pure-Python RSA implementation" category = "main" optional = false @@ -1408,7 +1421,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "slack-sdk" -version = "3.17.0" +version = "3.18.1" description = "The Slack API Platform SDK for Python" category = "main" optional = false @@ -1487,9 +1500,9 @@ docutils = "*" sphinx = "*" [package.extras] +test = ["pytest-cov", "pytest (>=3.0.0)"] +lint = ["pylint", "flake8", "black"] dev = ["pre-commit"] -lint = ["black", "flake8", "pylint"] -test = ["pytest (>=3.0.0)", "pytest-cov"] [[package]] name = "sphinx-rtd-theme" @@ -1638,11 +1651,11 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "uritemplate" @@ -1654,11 +1667,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -1711,11 +1724,11 @@ ujson = ["ujson"] [[package]] name = "yarl" -version = "1.7.2" +version = "1.8.1" description = "Yet another URL library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] idna = ">=2.0" @@ -1724,20 +1737,20 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "bd8e0a03668c380c6e76c8cd6c71020692f4ea9f32de7a4f09433564faa9dad0" +content-hash = "89fb7e8ad310b5048bf78561f1146194c8779e286d839cc000f04e88be87f3f3" [metadata.files] acre = [] @@ -1819,10 +1832,7 @@ aiohttp-json-rpc = [ {file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"}, {file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"}, ] -aiohttp-middlewares = [ - {file = "aiohttp-middlewares-2.0.0.tar.gz", hash = "sha256:e08ba04dc0e8fe379aa5e9444a68485c275677ee1e18c55cbb855de0c3629502"}, - {file = "aiohttp_middlewares-2.0.0-py3-none-any.whl", hash = "sha256:29cf1513176b4013844711975ff520e26a8a5d8f9fefbbddb5e91224a86b043e"}, -] +aiohttp-middlewares = [] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, @@ -1840,10 +1850,7 @@ arrow = [ {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] -astroid = [ - {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, - {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, -] +astroid = [] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, @@ -1852,99 +1859,21 @@ asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] +atomicwrites = [] +attrs = [] autopep8 = [ {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"}, {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"}, ] -babel = [ - {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, - {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, -] -bcrypt = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, -] +babel = [] +bcrypt = [] blessed = [ {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, ] -cachetools = [ - {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, - {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, -] -certifi = [ - {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"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, -] +cachetools = [] +certifi = [] +cffi = [] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -1957,10 +1886,7 @@ clique = [ {file = "clique-1.6.1-py2.py3-none-any.whl", hash = "sha256:8619774fa035661928dd8c93cd805acf2d42533ccea1b536c09815ed426c9858"}, {file = "clique-1.6.1.tar.gz", hash = "sha256:90165c1cf162d4dd1baef83ceaa1afc886b453e379094fa5b60ea470d1733e66"}, ] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] +colorama = [] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, @@ -1969,73 +1895,8 @@ coolname = [ {file = "coolname-1.1.0-py2.py3-none-any.whl", hash = "sha256:e6a83a0ac88640f4f3d2070438dbe112fe80cfebc119c93bd402976ec84c0978"}, {file = "coolname-1.1.0.tar.gz", hash = "sha256:410fe6ea9999bf96f2856ef0c726d5f38782bbefb7bb1aca0e91e0dc98ed09e3"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] -cryptography = [ - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, - {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, - {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, - {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, - {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, - {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, - {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, - {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, - {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, - {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, -] +coverage = [] +cryptography = [] cx-freeze = [ {file = "cx_Freeze-6.9-cp310-cp310-win32.whl", hash = "sha256:776d4fb68a4831691acbd3c374362b9b48ce2e568514a73c3d4cb14d5dcf1470"}, {file = "cx_Freeze-6.9-cp310-cp310-win_amd64.whl", hash = "sha256:243f36d35a034a409cd6247d8cb5d1fbfd7374e3e668e813d0811f64d6bd5ed3"}, @@ -2064,14 +1925,8 @@ cx-logging = [ {file = "cx_Logging-3.0-cp39-cp39-win_amd64.whl", hash = "sha256:302e9c4f65a936c288a4fa59a90e7e142d9ef994aa29676731acafdcccdbb3f5"}, {file = "cx_Logging-3.0.tar.gz", hash = "sha256:ba8a7465facf7b98d8f494030fb481a2e8aeee29dc191e10383bb54ed42bdb34"}, ] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] -dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, -] +deprecated = [] +dill = [] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, @@ -2080,90 +1935,22 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -dropbox = [ - {file = "dropbox-11.31.0-py2-none-any.whl", hash = "sha256:393a99dfe30d42fd73c265b9b7d24bb21c9a961739cd097c3541e709eb2a209c"}, - {file = "dropbox-11.31.0-py3-none-any.whl", hash = "sha256:5f924102fd6464def81573320c6aa4ea9cd3368e1b1c13d838403dd4c9ffc919"}, - {file = "dropbox-11.31.0.tar.gz", hash = "sha256:f483d65b702775b9abf7b9328f702c68c6397fc01770477c6ddbfb1d858a5bcf"}, -] +dropbox = [] enlighten = [ {file = "enlighten-1.10.2-py2.py3-none-any.whl", hash = "sha256:b237fe562b320bf9f1d4bb76d0c98e0daf914372a76ab87c35cd02f57aa9d8c1"}, {file = "enlighten-1.10.2.tar.gz", hash = "sha256:7a5b83cd0f4d095e59d80c648ebb5f7ffca0cd8bcf7ae6639828ee1ad000632a"}, ] -evdev = [ - {file = "evdev-1.5.0.tar.gz", hash = "sha256:5b33b174f7c84576e7dd6071e438bf5ad227da95efd4356a39fe4c8355412fe6"}, -] +evdev = [] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] -frozenlist = [ - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, - {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, - {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, - {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, - {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, - {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, - {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, -] +frozenlist = [] ftrack-python-api = [] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] -gazu = [ - {file = "gazu-0.8.28-py2.py3-none-any.whl", hash = "sha256:ec4f7c2688a2b37ee8a77737e4e30565ad362428c3ade9046136a998c043e51c"}, -] +gazu = [] gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, @@ -2172,42 +1959,24 @@ gitpython = [ {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, ] -google-api-core = [ - {file = "google-api-core-2.8.1.tar.gz", hash = "sha256:958024c6aa3460b08f35741231076a4dd9a4c819a6a39d44da9627febe8b28f0"}, - {file = "google_api_core-2.8.1-py3-none-any.whl", hash = "sha256:ce1daa49644b50398093d2a9ad886501aa845e2602af70c3001b9f402a9d7359"}, -] +google-api-core = [] google-api-python-client = [ {file = "google-api-python-client-1.12.11.tar.gz", hash = "sha256:1b4bd42a46321e13c0542a9e4d96fa05d73626f07b39f83a73a947d70ca706a9"}, {file = "google_api_python_client-1.12.11-py2.py3-none-any.whl", hash = "sha256:7e0a1a265c8d3088ee1987778c72683fcb376e32bada8d7767162bd9c503fd9b"}, ] -google-auth = [ - {file = "google-auth-2.7.0.tar.gz", hash = "sha256:8a954960f852d5f19e6af14dd8e75c20159609e85d8db37e4013cc8c3824a7e1"}, - {file = "google_auth-2.7.0-py2.py3-none-any.whl", hash = "sha256:df549a1433108801b11bdcc0e312eaf0d5f0500db42f0523e4d65c78722e8475"}, -] +google-auth = [] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] -googleapis-common-protos = [ - {file = "googleapis-common-protos-1.56.2.tar.gz", hash = "sha256:b09b56f5463070c2153753ef123f07d2e49235e89148e9b2459ec8ed2f68d7d3"}, - {file = "googleapis_common_protos-1.56.2-py2.py3-none-any.whl", hash = "sha256:023eaea9d8c1cceccd9587c6af6c20f33eeeb05d4148670f2b0322dc1511700c"}, -] +googleapis-common-protos = [] httplib2 = [ {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, ] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, - {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, -] +idna = [] +imagesize = [] +importlib-metadata = [] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -2220,18 +1989,12 @@ jedi = [ {file = "jedi-0.13.3-py2.py3-none-any.whl", hash = "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"}, {file = "jedi-0.13.3.tar.gz", hash = "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b"}, ] -jeepney = [ - {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, - {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, -] +jeepney = [] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] -jinxed = [ - {file = "jinxed-1.2.0-py2.py3-none-any.whl", hash = "sha256:cfc2b2e4e3b4326954d546ba6d6b9a7a796ddcb0aef8d03161d005177eb0d48b"}, - {file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"}, -] +jinxed = [] jsonschema = [ {file = "jsonschema-2.6.0-py2.py3-none-any.whl", hash = "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08"}, {file = "jsonschema-2.6.0.tar.gz", hash = "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"}, @@ -2283,28 +2046,12 @@ log4mongo = [ {file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2313,27 +2060,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2343,12 +2077,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2418,15 +2146,13 @@ multidict = [ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] +opencolorio-configs = [] opentimelineio = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] -paramiko = [ - {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, - {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, -] +paramiko = [] parso = [ {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, @@ -2435,50 +2161,8 @@ pathlib2 = [ {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, {file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"}, ] -pillow = [ - {file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"}, - {file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"}, - {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"}, - {file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"}, - {file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"}, - {file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"}, - {file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"}, - {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"}, - {file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"}, - {file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"}, - {file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"}, - {file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"}, - {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"}, - {file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"}, - {file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"}, - {file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"}, - {file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"}, - {file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"}, - {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"}, - {file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"}, - {file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"}, - {file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"}, - {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"}, - {file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"}, - {file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] +pillow = [] +platformdirs = [] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, @@ -2491,34 +2175,7 @@ prefixed = [ {file = "prefixed-0.3.2-py2.py3-none-any.whl", hash = "sha256:5e107306462d63f2f03c529dbf11b0026fdfec621a9a008ca639d71de22995c3"}, {file = "prefixed-0.3.2.tar.gz", hash = "sha256:ca48277ba5fa8346dd4b760847da930c7b84416387c39e93affef086add2c029"}, ] -protobuf = [ - {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, - {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, - {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, - {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, - {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, - {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, - {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, - {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, - {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, - {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, - {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, - {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, - {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, - {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, - {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, - {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, - {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, - {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, -] +protobuf = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -2577,14 +2234,8 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pylint = [ - {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, - {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, -] +pygments = [] +pylint = [] pymongo = [ {file = "pymongo-3.12.3-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f"}, {file = "pymongo-3.12.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6"}, @@ -2711,42 +2362,10 @@ pynput = [ {file = "pynput-1.7.6-py3.9.egg", hash = "sha256:264429fbe676e98e9050ad26a7017453bdd08768adb25cafb918347cf9f1eb4a"}, {file = "pynput-1.7.6.tar.gz", hash = "sha256:3a5726546da54116b687785d38b1db56997ce1d28e53e8d22fc656d8b92e533c"}, ] -pyobjc-core = [ - {file = "pyobjc-core-8.5.tar.gz", hash = "sha256:704c275439856c0d1287469f0d589a7d808d48b754a93d9ce5415d4eaf06d576"}, - {file = "pyobjc_core-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0c234143b48334443f5adcf26e668945a6d47bc1fa6223e80918c6c735a029d9"}, - {file = "pyobjc_core-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1486ee533f0d76f666804ce89723ada4db56bfde55e56151ba512d3f849857f8"}, - {file = "pyobjc_core-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:412de06dfa728301c04b3e46fd7453320a8ae8b862e85236e547cd797a73b490"}, - {file = "pyobjc_core-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b3e09cccb1be574a82cc9f929ae27fc4283eccc75496cb5d51534caa6bb83a3"}, - {file = "pyobjc_core-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:eeafe21f879666ab7f57efcc6b007c9f5f8733d367b7e380c925203ed83f000d"}, - {file = "pyobjc_core-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0071686976d7ea8c14690950e504a13cb22b4ebb2bc7b5ec47c1c1c0f6eff41"}, -] -pyobjc-framework-applicationservices = [ - {file = "pyobjc-framework-ApplicationServices-8.5.tar.gz", hash = "sha256:fa3015ef8e3add90af3447d7fdcc7f8dd083cc2a1d58f99a569480a2df10d2b1"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:436b16ebe448a829a8312e10208eec81a2adcae1fff674dbcc3262e1bd76e0ca"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:408958d14aa7fcf46f2163754c211078bc63be1368934d86188202914dce077d"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1d6cd4ce192859a22e208da4d7177a1c3ceb1ef2f64c339fd881102b1210cadd"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0251d092adb1d2d116fd9f147ceef0e53b158a46c21245131c40b9d7b786d0db"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:9742e69fe6d4545d0e02b0ad0a7a2432bc9944569ee07d6e90ffa5ef614df9f7"}, - {file = "pyobjc_framework_ApplicationServices-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16f5677c14ea903c6aaca1dd121521825c39e816cae696d6ae32c0b287252ab2"}, -] -pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-8.5.tar.gz", hash = "sha256:569bd3a020f64b536fb2d1c085b37553e50558c9f907e08b73ffc16ae68e1861"}, - {file = "pyobjc_framework_Cocoa-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7a7c160416696bf6035dfcdf0e603aaa52858d6afcddfcc5ab41733619ac2529"}, - {file = "pyobjc_framework_Cocoa-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6ceba444282030be8596b812260e8d28b671254a51052ad778d32da6e17db847"}, - {file = "pyobjc_framework_Cocoa-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f46b2b161b8dd40c7b9e00bc69636c3e6480b2704a69aee22ee0154befbe163a"}, - {file = "pyobjc_framework_Cocoa-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b31d425aee8698cbf62b187338f5ca59427fa4dca2153a73866f7cb410713119"}, - {file = "pyobjc_framework_Cocoa-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:898359ac1f76eedec8aa156847682378a8950824421c40edb89391286e607dc4"}, - {file = "pyobjc_framework_Cocoa-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:baa2947f76b119a3360973d74d57d6dada87ac527bab9a88f31596af392f123c"}, -] -pyobjc-framework-quartz = [ - {file = "pyobjc-framework-Quartz-8.5.tar.gz", hash = "sha256:d2bc5467a792ddc04814f12a1e9c2fcaf699a1c3ad3d4264cfdce6b9c7b10624"}, - {file = "pyobjc_framework_Quartz-8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e9f0fb663f7872c9de94169031ac42b91ad01bd4cad49a9f1a0164be8f028426"}, - {file = "pyobjc_framework_Quartz-8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:567eec91287cfe9a1b6433717192c585935de8f3daa28d82ce72fdd6c7ac00f6"}, - {file = "pyobjc_framework_Quartz-8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f910ab41a712ffc7a8c3e3716a2d6f39ea4419004b26a2fd2d2f740ff5c262c"}, - {file = "pyobjc_framework_Quartz-8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29d07066781628278bf0e5278abcfc96ef6724c66c5629a0b4c214d319a82e55"}, - {file = "pyobjc_framework_Quartz-8.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:72abcde1a3d72be11f2c881c9b9872044c8f2de86d2047b67fe771713638b107"}, - {file = "pyobjc_framework_Quartz-8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8809b9a2df2f461697bdb45b6d1b5a4f881f88f09450e3990858e64e3e26c530"}, -] +pyobjc-core = [] +pyobjc-framework-applicationservices = [] +pyobjc-framework-cocoa = [] +pyobjc-framework-quartz = [] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -2770,14 +2389,8 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -python-engineio = [ - {file = "python-engineio-3.14.2.tar.gz", hash = "sha256:eab4553f2804c1ce97054c8b22cf0d5a9ab23128075248b97e1a5b2f29553085"}, - {file = "python_engineio-3.14.2-py2.py3-none-any.whl", hash = "sha256:5a9e6086d192463b04a1428ff1f85b6ba631bbb19d453b144ffc04f530542b84"}, -] -python-socketio = [ - {file = "python-socketio-4.6.1.tar.gz", hash = "sha256:cd1f5aa492c1eb2be77838e837a495f117e17f686029ebc03d62c09e33f4fa10"}, - {file = "python_socketio-4.6.1-py2.py3-none-any.whl", hash = "sha256:5a21da53fdbdc6bb6c8071f40e13d100e0b279ad997681c2492478e06f370523"}, -] +python-engineio = [] +python-socketio = [] python-xlib = [ {file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"}, {file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"}, @@ -2805,10 +2418,7 @@ pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] -"qt.py" = [ - {file = "Qt.py-1.3.7-py2.py3-none-any.whl", hash = "sha256:150099d1c6f64c9621a2c9d79d45102ec781c30ee30ee69fc082c6e9be7324fe"}, - {file = "Qt.py-1.3.7.tar.gz", hash = "sha256:803c7bdf4d6230f9a466be19d55934a173eabb61406d21cb91e80c2a3f773b1f"}, -] +"qt.py" = [] qtawesome = [ {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, @@ -2821,18 +2431,9 @@ recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -rsa = [ - {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, - {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, -] -secretstorage = [ - {file = "SecretStorage-3.3.2-py3-none-any.whl", hash = "sha256:755dc845b6ad76dcbcbc07ea3da75ae54bb1ea529eb72d15f83d26499a5df319"}, - {file = "SecretStorage-3.3.2.tar.gz", hash = "sha256:0a8eb9645b320881c222e827c26f4cfcf55363e8b374a021981ef886657a912f"}, -] +requests = [] +rsa = [] +secretstorage = [] semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, @@ -2842,10 +2443,7 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -slack-sdk = [ - {file = "slack_sdk-3.17.0-py2.py3-none-any.whl", hash = "sha256:0816efc43d1d2db8286e8dbcbb2e86fd0f71c206c01c521c2cb054ecb40f9ced"}, - {file = "slack_sdk-3.17.0.tar.gz", hash = "sha256:860cd0e50c454b955f14321c8c5486a47cc1e0e84116acdb009107f836752feb"}, -] +slack-sdk = [] smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, @@ -2854,18 +2452,9 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -speedcopy = [ - {file = "speedcopy-2.1.4-py3-none-any.whl", hash = "sha256:e09eb1de67ae0e0b51d5b99a28882009d565a37a3cb3c6bae121e3a5d3cccb17"}, - {file = "speedcopy-2.1.4.tar.gz", hash = "sha256:eff007a97e49ec1934df4fa8074f4bd1cf4a3b14c5499d914988785cff0c199a"}, -] -sphinx = [ - {file = "Sphinx-5.0.1-py3-none-any.whl", hash = "sha256:36aa2a3c2f6d5230be94585bc5d74badd5f9ed8f3388b8eedc1726fe45b1ad30"}, - {file = "Sphinx-5.0.1.tar.gz", hash = "sha256:f4da1187785a5bc7312cc271b0e867a93946c319d106363e102936a3d9857306"}, -] -sphinx-qt-documentation = [ - {file = "sphinx_qt_documentation-0.4-py3-none-any.whl", hash = "sha256:fa131093f75cd1bd48699cd132e18e4d46ba9eaadc070e6026867cea75ecdb7b"}, - {file = "sphinx_qt_documentation-0.4.tar.gz", hash = "sha256:f43ba17baa93e353fb94045027fb67f9d935ed158ce8662de93f08b88eec6774"}, -] +speedcopy = [] +sphinx = [] +sphinx-qt-documentation = [] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, @@ -2914,44 +2503,13 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, -] +typed-ast = [] +typing-extensions = [] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, ] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] +urllib3 = [] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, @@ -2960,151 +2518,10 @@ websocket-client = [ {file = "websocket-client-0.59.0.tar.gz", hash = "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c"}, {file = "websocket_client-0.59.0-py2.py3-none-any.whl", hash = "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32"}, ] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] +wrapt = [] wsrpc-aiohttp = [ {file = "wsrpc-aiohttp-3.2.0.tar.gz", hash = "sha256:f467abc51bcdc760fc5aeb7041abdeef46eeca3928dc43dd6e7fa7a533563818"}, {file = "wsrpc_aiohttp-3.2.0-py3-none-any.whl", hash = "sha256:fa9b0bf5cb056898cb5c9f64cbc5eacb8a5dd18ab1b7f0cd4a2208b4a7fde282"}, ] -yarl = [ - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, - {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, - {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, - {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, - {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, - {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, - {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, - {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, - {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, - {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, - {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, - {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, - {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, - {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, -] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +yarl = [] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 994c83d369..1d757deaa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" +OpenColorIO-Configs = { git = "https://github.com/pypeclub/OpenColorIO-Configs.git", branch = "main" } [tool.poetry.dev-dependencies] @@ -80,13 +81,14 @@ cx_freeze = "~6.9" GitPython = "^3.1.17" jedi = "^0.13" Jinja2 = "^2.11" +markupsafe = "2.0.1" pycodestyle = "^2.5.0" pydocstyle = "^3.0.0" pylint = "^2.4.4" pytest = "^6.1" pytest-cov = "*" pytest-print = "*" -Sphinx = "*" +Sphinx = "5.0.1" sphinx-rtd-theme = "*" sphinxcontrib-websupport = "*" sphinx-qt-documentation = "*" From 7f48af4bdcf524ae91447038b658a60aa256f80e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 00:11:52 +0800 Subject: [PATCH 0727/1030] 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 0728/1030] 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 8ba5d0079952731a2e7a98490701750bffa28a9e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 18:59:43 +0200 Subject: [PATCH 0729/1030] move env setup function used in prelaunch hook from api higher --- openpype/hosts/resolve/utils.py | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 openpype/hosts/resolve/utils.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py new file mode 100644 index 0000000000..382a7cf344 --- /dev/null +++ b/openpype/hosts/resolve/utils.py @@ -0,0 +1,54 @@ +import os +import shutil +from openpype.lib import Logger + +RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def setup(env): + log = Logger.get_logger("ResolveSetup") + scripts = {} + us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + us_paths = [os.path.join( + RESOLVE_ROOT_DIR, + "utility_scripts" + )] + + # collect script dirs + if us_env: + log.info(f"Utility Scripts Env: `{us_env}`") + us_paths = us_env.split( + os.pathsep) + us_paths + + # collect scripts from dirs + for path in us_paths: + scripts.update({path: os.listdir(path)}) + + log.info(f"Utility Scripts Dir: `{us_paths}`") + log.info(f"Utility Scripts: `{scripts}`") + + # make sure no script file is in folder + for s in os.listdir(us_dir): + path = os.path.join(us_dir, s) + log.info(f"Removing `{path}`...") + if os.path.isdir(path): + shutil.rmtree(path, onerror=None) + else: + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for d, sl in scripts.items(): + # directory and scripts list + for s in sl: + # script in script list + src = os.path.join(d, s) + dst = os.path.join(us_dir, s) + log.info(f"Copying `{src}` to `{dst}`...") + if os.path.isdir(src): + shutil.copytree( + src, dst, symlinks=False, + ignore=None, ignore_dangling_symlinks=False + ) + else: + shutil.copy2(src, dst) From 07d89fc23b593890d10f7094af2be04399f8dedd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:01:24 +0200 Subject: [PATCH 0730/1030] fixed imports to use in-DCC imports from resolve.api --- openpype/hosts/resolve/__init__.py | 129 ----------------- openpype/hosts/resolve/api/__init__.py | 134 +++++++++++++++++- openpype/hosts/resolve/api/action.py | 2 +- openpype/hosts/resolve/api/lib.py | 6 +- openpype/hosts/resolve/api/menu.py | 4 +- openpype/hosts/resolve/api/pipeline.py | 13 +- openpype/hosts/resolve/api/preload_console.py | 4 +- openpype/hosts/resolve/api/utils.py | 96 ++----------- openpype/hosts/resolve/api/workio.py | 19 +-- .../hosts/resolve/hooks/pre_resolve_setup.py | 20 +-- .../plugins/create/create_shot_clip.py | 15 +- .../hosts/resolve/plugins/load/load_clip.py | 21 +-- .../plugins/publish/extract_workfile.py | 4 +- .../plugins/publish/precollect_instances.py | 26 ++-- .../OpenPype_sync_util_scripts.py | 5 +- .../utility_scripts/__OpenPype__Menu__.py | 6 +- .../utility_scripts/tests/test_otio_as_edl.py | 4 +- .../testing_create_timeline_item_from_path.py | 15 +- .../tests/testing_load_media_pool_item.py | 10 +- 19 files changed, 233 insertions(+), 300 deletions(-) diff --git a/openpype/hosts/resolve/__init__.py b/openpype/hosts/resolve/__init__.py index 3e49ce3b9b..e69de29bb2 100644 --- a/openpype/hosts/resolve/__init__.py +++ b/openpype/hosts/resolve/__init__.py @@ -1,129 +0,0 @@ -from .api.utils import ( - setup, - get_resolve_module -) - -from .api.pipeline import ( - install, - uninstall, - ls, - containerise, - update_container, - publish, - launch_workfiles_app, - maintained_selection, - remove_instance, - list_instances -) - -from .api.lib import ( - maintain_current_timeline, - publish_clip_color, - get_project_manager, - get_current_project, - get_current_timeline, - create_bin, - get_media_pool_item, - create_media_pool_item, - create_timeline_item, - get_timeline_item, - get_video_track_names, - get_current_timeline_items, - get_pype_timeline_item_by_name, - get_timeline_item_pype_tag, - set_timeline_item_pype_tag, - imprint, - set_publish_attribute, - get_publish_attribute, - create_compound_clip, - swap_clips, - get_pype_clip_metadata, - set_project_manager_to_folder_name, - get_otio_clip_instance_data, - get_reformated_path -) - -from .api.menu import launch_pype_menu - -from .api.plugin import ( - ClipLoader, - TimelineItemLoader, - Creator, - PublishClip -) - -from .api.workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root -) - -from .api.testing_utils import TestGUI - - -__all__ = [ - # pipeline - "install", - "uninstall", - "ls", - "containerise", - "update_container", - "reload_pipeline", - "publish", - "launch_workfiles_app", - "maintained_selection", - "remove_instance", - "list_instances", - - # utils - "setup", - "get_resolve_module", - - # lib - "maintain_current_timeline", - "publish_clip_color", - "get_project_manager", - "get_current_project", - "get_current_timeline", - "create_bin", - "get_media_pool_item", - "create_media_pool_item", - "create_timeline_item", - "get_timeline_item", - "get_video_track_names", - "get_current_timeline_items", - "get_pype_timeline_item_by_name", - "get_timeline_item_pype_tag", - "set_timeline_item_pype_tag", - "imprint", - "set_publish_attribute", - "get_publish_attribute", - "create_compound_clip", - "swap_clips", - "get_pype_clip_metadata", - "set_project_manager_to_folder_name", - "get_otio_clip_instance_data", - "get_reformated_path", - - # menu - "launch_pype_menu", - - # plugin - "ClipLoader", - "TimelineItemLoader", - "Creator", - "PublishClip", - - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - - "TestGUI" -] diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 48bd938e57..cf1edb4c35 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -1,11 +1,137 @@ """ resolve api """ -import os bmdvr = None bmdvf = None -API_DIR = os.path.dirname(os.path.abspath(__file__)) -HOST_DIR = os.path.dirname(API_DIR) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +from .utils import ( + get_resolve_module +) + +from .pipeline import ( + install, + uninstall, + ls, + containerise, + update_container, + publish, + launch_workfiles_app, + maintained_selection, + remove_instance, + list_instances +) + +from .lib import ( + maintain_current_timeline, + publish_clip_color, + get_project_manager, + get_current_project, + get_current_timeline, + create_bin, + get_media_pool_item, + create_media_pool_item, + create_timeline_item, + get_timeline_item, + get_video_track_names, + get_current_timeline_items, + get_pype_timeline_item_by_name, + get_timeline_item_pype_tag, + set_timeline_item_pype_tag, + imprint, + set_publish_attribute, + get_publish_attribute, + create_compound_clip, + swap_clips, + get_pype_clip_metadata, + set_project_manager_to_folder_name, + get_otio_clip_instance_data, + get_reformated_path +) + +from .menu import launch_pype_menu + +from .plugin import ( + ClipLoader, + TimelineItemLoader, + Creator, + PublishClip +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +from .testing_utils import TestGUI + + +__all__ = [ + "bmdvr", + "bmdvf", + + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "update_container", + "reload_pipeline", + "publish", + "launch_workfiles_app", + "maintained_selection", + "remove_instance", + "list_instances", + + # utils + "get_resolve_module", + + # lib + "maintain_current_timeline", + "publish_clip_color", + "get_project_manager", + "get_current_project", + "get_current_timeline", + "create_bin", + "get_media_pool_item", + "create_media_pool_item", + "create_timeline_item", + "get_timeline_item", + "get_video_track_names", + "get_current_timeline_items", + "get_pype_timeline_item_by_name", + "get_timeline_item_pype_tag", + "set_timeline_item_pype_tag", + "imprint", + "set_publish_attribute", + "get_publish_attribute", + "create_compound_clip", + "swap_clips", + "get_pype_clip_metadata", + "set_project_manager_to_folder_name", + "get_otio_clip_instance_data", + "get_reformated_path", + + # menu + "launch_pype_menu", + + # plugin + "ClipLoader", + "TimelineItemLoader", + "Creator", + "PublishClip", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", + + "TestGUI" +] diff --git a/openpype/hosts/resolve/api/action.py b/openpype/hosts/resolve/api/action.py index f8f338a850..d55a24a39a 100644 --- a/openpype/hosts/resolve/api/action.py +++ b/openpype/hosts/resolve/api/action.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import pyblish.api -from ...action import get_errored_instances_from_context +from openpype.action import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 93ccdaf812..f41eb36caf 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -4,13 +4,13 @@ import re import os import contextlib from opentimelineio import opentime + +from openpype.lib import Logger from openpype.pipeline.editorial import is_overlapping_otio_ranges from ..otio import davinci_export as otio_export -from openpype.api import Logger - -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index 9e0dd12376..2c7678ee5b 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -3,13 +3,13 @@ import sys from Qt import QtWidgets, QtCore +from openpype.tools.utils import host_tools + from .pipeline import ( publish, launch_workfiles_app ) -from openpype.tools.utils import host_tools - def load_stylesheet(): path = os.path.join(os.path.dirname(__file__), "menu_style.qss") diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 4a7d1c5bea..1c8d9dc01c 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -7,7 +7,7 @@ from collections import OrderedDict from pyblish import api as pyblish -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( schema, register_loader_plugin_path, @@ -16,11 +16,15 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) -from . import lib -from . import PLUGINS_DIR from openpype.tools.utils import host_tools -log = Logger().get_logger(__name__) +from . import lib +from .utils import get_resolve_module + +log = Logger.get_logger(__name__) + +HOST_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") @@ -39,7 +43,6 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ - from .. import get_resolve_module log.info("openpype.hosts.resolve installed") diff --git a/openpype/hosts/resolve/api/preload_console.py b/openpype/hosts/resolve/api/preload_console.py index 1e3a56b4dd..a822ea2460 100644 --- a/openpype/hosts/resolve/api/preload_console.py +++ b/openpype/hosts/resolve/api/preload_console.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import time from openpype.hosts.resolve.utils import get_resolve_module -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) wait_delay = 2.5 wait = 0.00 diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py index 9b3762f328..871b3af38d 100644 --- a/openpype/hosts/resolve/api/utils.py +++ b/openpype/hosts/resolve/api/utils.py @@ -4,21 +4,21 @@ Resolve's tools for setting environment """ -import sys import os -import shutil -from . import HOST_DIR -from openpype.api import Logger -log = Logger().get_logger(__name__) +import sys + +from openpype.lib import Logger + +log = Logger.get_logger(__name__) def get_resolve_module(): - from openpype.hosts import resolve + from openpype.hosts.resolve import api # dont run if already loaded - if resolve.api.bmdvr: + if api.bmdvr: log.info(("resolve module is assigned to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) - return resolve.api.bmdvr + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) + return api.bmdvr try: """ The PYTHONPATH needs to be set correctly for this import @@ -71,79 +71,9 @@ def get_resolve_module(): # assign global var and return bmdvr = bmd.scriptapp("Resolve") bmdvf = bmd.scriptapp("Fusion") - resolve.api.bmdvr = bmdvr - resolve.api.bmdvf = bmdvf + api.bmdvr = bmdvr + api.bmdvf = bmdvf log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvf`: {resolve.api.bmdvf}")) - - -def _sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Resolve/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = os.environ - - # initiate inputs - scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") - us_paths = [os.path.join( - HOST_DIR, - "utility_scripts" - )] - - # collect script dirs - if us_env: - log.info(f"Utility Scripts Env: `{us_env}`") - us_paths = us_env.split( - os.pathsep) + us_paths - - # collect scripts from dirs - for path in us_paths: - scripts.update({path: os.listdir(path)}) - - log.info(f"Utility Scripts Dir: `{us_paths}`") - log.info(f"Utility Scripts: `{scripts}`") - - # make sure no script file is in folder - if next((s for s in os.listdir(us_dir)), None): - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) - log.info(f"Removing `{path}`...") - if os.path.isdir(path): - shutil.rmtree(path, onerror=None) - else: - os.remove(path) - - # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.join(d, s) - dst = os.path.join(us_dir, s) - log.info(f"Copying `{src}` to `{dst}`...") - if os.path.isdir(src): - shutil.copytree( - src, dst, symlinks=False, - ignore=None, ignore_dangling_symlinks=False - ) - else: - shutil.copy2(src, dst) - - -def setup(env=None): - """ Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch() - """ - if not env: - env = os.environ - - # synchronize resolve utility scripts - _sync_utility_scripts(env) - - log.info("Resolve OpenPype wrapper has been installed") + f"`pype.hosts.resolve.api.bmdvf`: {api.bmdvf}")) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index f175769387..5a742ecf7e 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -2,14 +2,14 @@ import os from openpype.api import Logger -from .. import ( +from .lib import ( get_project_manager, get_current_project, set_project_manager_to_folder_name ) -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) exported_projet_ext = ".drp" @@ -60,7 +60,7 @@ def open_file(filepath): # load project from input path project = pm.LoadProject(fname) log.info(f"Project {project.GetName()} opened...") - return True + except AttributeError: log.warning((f"Project with name `{fname}` does not exist! It will " f"be imported from {filepath} and then loaded...")) @@ -69,9 +69,8 @@ def open_file(filepath): project = pm.LoadProject(fname) log.info(f"Project imported/loaded {project.GetName()}...") return True - else: - return False - + return False + return True def current_file(): pm = get_project_manager() @@ -80,13 +79,9 @@ def current_file(): name = project.GetName() fname = name + exported_projet_ext current_file = os.path.join(current_dir, fname) - normalised = os.path.normpath(current_file) - - # Unsaved current file - if normalised == "": + if not current_file: return None - - return normalised + return os.path.normpath(current_file) def work_root(session): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 978e3760fd..1d977e2d8e 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,7 +1,7 @@ import os -import importlib + from openpype.lib import PreLaunchHook -from openpype.hosts.resolve.api import utils +from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): @@ -43,18 +43,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env.get("PRE_PYTHON_SCRIPT", "")) self.launch_context.env["PRE_PYTHON_SCRIPT"] = pre_py_sc self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") - try: - __import__("openpype.hosts.resolve") - __import__("pyblish") - except ImportError: - self.log.warning( - "pyblish: Could not load Resolve integration.", - exc_info=True - ) - - else: - # Resolve Setup integration - importlib.reload(utils) - self.log.debug(f"-- utils.__file__: `{utils.__file__}`") - utils.setup(self.launch_context.env) + # Resolve Setup integration + setup(self.launch_context.env) diff --git a/openpype/hosts/resolve/plugins/create/create_shot_clip.py b/openpype/hosts/resolve/plugins/create/create_shot_clip.py index dbf10c5163..4b14f2493f 100644 --- a/openpype/hosts/resolve/plugins/create/create_shot_clip.py +++ b/openpype/hosts/resolve/plugins/create/create_shot_clip.py @@ -1,9 +1,12 @@ # from pprint import pformat -from openpype.hosts import resolve -from openpype.hosts.resolve.api import lib +from openpype.hosts.resolve.api import plugin, lib +from openpype.hosts.resolve.api.lib import ( + get_video_track_names, + create_bin, +) -class CreateShotClip(resolve.Creator): +class CreateShotClip(plugin.Creator): """Publishable clip""" label = "Create Publishable Clip" @@ -11,7 +14,7 @@ class CreateShotClip(resolve.Creator): icon = "film" defaults = ["Main"] - gui_tracks = resolve.get_video_track_names() + gui_tracks = get_video_track_names() gui_name = "OpenPype publish attributes creator" gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { @@ -250,7 +253,7 @@ class CreateShotClip(resolve.Creator): sq_markers = self.timeline.GetMarkers() # create media bin for compound clips (trackItems) - mp_folder = resolve.create_bin(self.timeline.GetName()) + mp_folder = create_bin(self.timeline.GetName()) kwargs = { "ui_inputs": widget.result, @@ -264,6 +267,6 @@ class CreateShotClip(resolve.Creator): self.rename_index = i self.log.info(track_item_data) # convert track item to timeline media pool item - track_item = resolve.PublishClip( + track_item = plugin.PublishClip( self, track_item_data, **kwargs).convert() track_item.SetClipColor(lib.publish_clip_color) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 190a5a7206..a0c78c182f 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,21 +1,22 @@ from copy import deepcopy -from importlib import reload from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) -from openpype.hosts import resolve +# from openpype.hosts import resolve from openpype.pipeline import ( get_representation_path, legacy_io, ) from openpype.hosts.resolve.api import lib, plugin -reload(plugin) -reload(lib) +from openpype.hosts.resolve.api.pipeline import ( + containerise, + update_container, +) -class LoadClip(resolve.TimelineItemLoader): +class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip Place clip to timeline on its asset origin timings collected @@ -46,7 +47,7 @@ class LoadClip(resolve.TimelineItemLoader): }) # load clip to timeline and get main variables - timeline_item = resolve.ClipLoader( + timeline_item = plugin.ClipLoader( self, context, **options).load() namespace = namespace or timeline_item.GetName() version = context['version'] @@ -80,7 +81,7 @@ class LoadClip(resolve.TimelineItemLoader): self.log.info("Loader done: `{}`".format(name)) - return resolve.containerise( + return containerise( timeline_item, name, namespace, context, self.__class__.__name__, @@ -98,7 +99,7 @@ class LoadClip(resolve.TimelineItemLoader): context.update({"representation": representation}) name = container['name'] namespace = container['namespace'] - timeline_item_data = resolve.get_pype_timeline_item_by_name(namespace) + timeline_item_data = lib.get_pype_timeline_item_by_name(namespace) timeline_item = timeline_item_data["clip"]["item"] project_name = legacy_io.active_project() version = get_version_by_id(project_name, representation["parent"]) @@ -109,7 +110,7 @@ class LoadClip(resolve.TimelineItemLoader): self.fname = get_representation_path(representation) context["version"] = {"data": version_data} - loader = resolve.ClipLoader(self, context) + loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob @@ -136,7 +137,7 @@ class LoadClip(resolve.TimelineItemLoader): # update color of clip regarding the version order self.set_item_color(timeline_item, version) - return resolve.update_container(timeline_item, data_imprint) + return update_container(timeline_item, data_imprint) @classmethod def set_item_color(cls, timeline_item, version): diff --git a/openpype/hosts/resolve/plugins/publish/extract_workfile.py b/openpype/hosts/resolve/plugins/publish/extract_workfile.py index e3d60465a2..ea8f19cd8c 100644 --- a/openpype/hosts/resolve/plugins/publish/extract_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/extract_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api import openpype.api -from openpype.hosts import resolve +from openpype.hosts.resolve.api.lib import get_project_manager class ExtractWorkfile(openpype.api.Extractor): @@ -29,7 +29,7 @@ class ExtractWorkfile(openpype.api.Extractor): os.path.join(staging_dir, drp_file_name)) # write out the drp workfile - resolve.get_project_manager().ExportProject( + get_project_manager().ExportProject( project.GetName(), drp_file_path) # create drp workfile representation diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index ee51998c0d..8ec169ad65 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -1,9 +1,15 @@ -import pyblish -from openpype.hosts import resolve - -# # developer reload modules from pprint import pformat +import pyblish + +from openpype.hosts.resolve.api.lib import ( + get_current_timeline_items, + get_timeline_item_pype_tag, + publish_clip_color, + get_publish_attribute, + get_otio_clip_instance_data, +) + class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" @@ -14,8 +20,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): otio_timeline = context.data["otioTimeline"] - selected_timeline_items = resolve.get_current_timeline_items( - filter=True, selecting_color=resolve.publish_clip_color) + selected_timeline_items = get_current_timeline_items( + filter=True, selecting_color=publish_clip_color) self.log.info( "Processing enabled track items: {}".format( @@ -27,7 +33,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): timeline_item = timeline_item_data["clip"]["item"] # get pype tag data - tag_data = resolve.get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_pype_tag(timeline_item) self.log.debug(f"__ tag_data: {pformat(tag_data)}") if not tag_data: @@ -67,7 +73,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": timeline_item, "families": families, - "publish": resolve.get_publish_attribute(timeline_item), + "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, "handleEnd": handle_end, @@ -75,7 +81,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) # otio clip data - otio_data = resolve.get_otio_clip_instance_data( + otio_data = get_otio_clip_instance_data( otio_timeline, timeline_item_data) or {} data.update(otio_data) @@ -134,7 +140,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "family": family, "families": [], - "publish": resolve.get_publish_attribute(timeline_item) + "publish": get_publish_attribute(timeline_item) }) context.create_instance(**data) diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py index 3a16b9c966..8f3917bece 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py @@ -6,10 +6,11 @@ from openpype.pipeline import install_host def main(env): - import openpype.hosts.resolve as bmdvr + from openpype.hosts.resolve.utils import setup + import openpype.hosts.resolve.api as bmdvr # Registers openpype's Global pyblish plugins install_host(bmdvr) - bmdvr.setup(env) + setup(env) if __name__ == "__main__": diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py index 89ade9238b..1087a7b7a0 100644 --- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py +++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py @@ -2,13 +2,13 @@ import os import sys from openpype.pipeline import install_host -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def main(env): - import openpype.hosts.resolve as bmdvr + import openpype.hosts.resolve.api as bmdvr # activate resolve from openpype install_host(bmdvr) diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py index 8433bd9172..92f2e43a72 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py @@ -6,8 +6,8 @@ import opentimelineio as otio from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.testing_utils import TestGUI from openpype.hosts.resolve.otio import davinci_export as otio_export diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py index 477955d527..91a361ec08 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py @@ -2,11 +2,16 @@ import os import sys -from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr import clique +from openpype.pipeline import install_host +from openpype.hosts.resolve.api.testing_utils import TestGUI +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) + class ThisTestGUI(TestGUI): extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] @@ -55,10 +60,10 @@ class ThisTestGUI(TestGUI): # skip if unwanted extension if ext not in self.extensions: return - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py index 872d620162..2e83188bde 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py @@ -1,13 +1,17 @@ #! python3 from openpype.pipeline import install_host -import openpype.hosts.resolve as bmdvr +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) def file_processing(fpath): - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) From a777238d83282e24c6238b92143b3a424ccde40d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:11:49 +0200 Subject: [PATCH 0731/1030] fix handling of host name in error message --- openpype/host/host.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 48907e7ec7..9cdbb819e1 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -19,8 +19,15 @@ class MissingMethodsError(ValueError): joined_missing = ", ".join( ['"{}"'.format(item) for item in missing_methods] ) + if isinstance(host, HostBase): + host_name = host.name + else: + try: + host_name = host.__file__.replace("\\", "/").split("/")[-3] + except Exception: + host_name = str(host) message = ( - "Host \"{}\" miss methods {}".format(host.name, joined_missing) + "Host \"{}\" miss methods {}".format(host_name, joined_missing) ) super(MissingMethodsError, self).__init__(message) From d9e3815878b3868b18478b9dea2328d140bf2d92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Aug 2022 12:23:06 +0200 Subject: [PATCH 0732/1030] 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 0733/1030] 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 0734/1030] 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 312b6d3243ce66bc2e2749c964fd1b178369f9ec Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 13:00:41 +0200 Subject: [PATCH 0735/1030] :bug: fix finding of last version --- igniter/bootstrap_repos.py | 73 ++++++++++++++++++-------------------- start.py | 3 ++ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 750b2f1bf7..73ef8283a7 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -514,6 +514,9 @@ class OpenPypeVersion(semver.VersionInfo): ValueError: if invalid path is specified. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version _openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): return _openpype_versions @@ -540,8 +543,7 @@ class OpenPypeVersion(semver.VersionInfo): )[0]: continue - if compatible_with and not detected_version.is_compatible( - compatible_with): + if not detected_version.is_compatible(compatible_with): continue detected_version.path = item @@ -610,6 +612,8 @@ class OpenPypeVersion(semver.VersionInfo): remote = True installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version local_versions = [] remote_versions = [] if local: @@ -630,8 +634,7 @@ class OpenPypeVersion(semver.VersionInfo): all_versions.sort() latest_version: OpenPypeVersion latest_version = all_versions[-1] - if compatible_with and not latest_version.is_compatible( - compatible_with): + if not latest_version.is_compatible(compatible_with): return None return latest_version @@ -1153,10 +1156,12 @@ class BootstrapRepos: versions compatible with specified one. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if isinstance(version, str): version = OpenPypeVersion(version=version) - installed_version = OpenPypeVersion.get_installed_version() if installed_version == version: return installed_version @@ -1250,51 +1255,41 @@ class BootstrapRepos: ok install it as normal version. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" " not implemented yet.")) - version_dir = "" - if compatible_with: - version_dir = f"{compatible_with.major}.{compatible_with.minor}" + 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) + dirs_to_search = [self.data_dir, self.data_dir / version_dir] if openpype_path: - dirs_to_search = [openpype_path] - - if compatible_with: - dirs_to_search.append(openpype_path / version_dir) - else: + dirs_to_search = [openpype_path, openpype_path / version_dir] + elif os.getenv("OPENPYPE_PATH") \ + and Path(os.getenv("OPENPYPE_PATH")).exists(): # 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"))] + dirs_to_search = [Path(os.getenv("OPENPYPE_PATH")), + Path(os.getenv("OPENPYPE_PATH")) / version_dir] + else: + try: + registry_dir = Path( + str(self.registry.get_item("openPypePath"))) + if registry_dir.exists(): + dirs_to_search = [ + registry_dir, registry_dir / version_dir + ] - 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(): - 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 + except ValueError: + # nothing found in registry, we'll use data dir + pass openpype_versions = [] for dir_to_search in dirs_to_search: @@ -1685,6 +1680,9 @@ class BootstrapRepos: ValueError: if invalid path is specified. """ + installed_version = OpenPypeVersion.get_installed_version() + if not compatible_with: + compatible_with = installed_version if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") @@ -1711,8 +1709,7 @@ class BootstrapRepos: ): continue - if compatible_with and \ - not detected_version.is_compatible(compatible_with): + if not detected_version.is_compatible(compatible_with): continue detected_version.path = item diff --git a/start.py b/start.py index 5cdffafb6e..c7bced20bd 100644 --- a/start.py +++ b/start.py @@ -629,6 +629,9 @@ def _determine_mongodb() -> str: def _initialize_environment(openpype_version: OpenPypeVersion) -> None: version_path = openpype_version.path + if not version_path: + _print(f"!!! Version {openpype_version} doesn't have path set.") + raise ValueError("No path set in specified OpenPype version.") os.environ["OPENPYPE_VERSION"] = str(openpype_version) # set OPENPYPE_REPOS_ROOT to point to currently used OpenPype version. os.environ["OPENPYPE_REPOS_ROOT"] = os.path.normpath( From 7526d4cfa5252b646469c79db782b1b4a04373ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Aug 2022 13:34:37 +0200 Subject: [PATCH 0736/1030] 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 0737/1030] 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 089cd3f9fa3587178c9fe73371b4470588b8467b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:00 +0200 Subject: [PATCH 0738/1030] added missing docstring for 'context_filters' argument --- openpype/client/entities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index c798c0ad6d..67ddb09ddb 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1220,6 +1220,8 @@ def get_archived_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. + context_filters (Dict[str, List[str, re.Pattern]]): Filter by + representation context fields. names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. fields (Iterable[str]): Fields that should be returned. All fields are From aefb992ce55145f94790bbaa5cdbf17136684e1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:35 +0200 Subject: [PATCH 0739/1030] removed unused 'is_context' property --- .../workfile/abstract_template_loader.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 1c8ede25e6..e2f9fdba0f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -465,23 +465,6 @@ class AbstractPlaceholder: return self.data["loader"] - @property - def is_context(self): - """Check if is placeholder context type. - - context_asset: For loading current asset - linked_asset: For loading linked assets - - Question: - There seems to be more build options and this property is not used, - should be removed? - - Returns: - bool: true if placeholder is a context placeholder - """ - - return self.builder_type == "context_asset" - @property def is_valid(self): """Test validity of placeholder. From 32c2440e4a6d5d5ec3d54c2d1a44ffc5f0f81ae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:55 +0200 Subject: [PATCH 0740/1030] fix docstring header --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e2f9fdba0f..05a98a1ddc 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,7 +456,7 @@ class AbstractPlaceholder: @property def loader_name(self): - """Return placeholder loader type. + """Return placeholder loader name. Returns: str: Loader name that will be used to load placeholder From dc73bbdb13044d077b5576cd33ebc7b51597a70c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 20:49:34 +0800 Subject: [PATCH 0741/1030] 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 0742/1030] 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 0743/1030] 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 0c72b8e278d3e0ac2af5b69ff09b115908a4b632 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 15:31:56 +0200 Subject: [PATCH 0744/1030] :recycle: refactor compatibility check --- igniter/bootstrap_repos.py | 143 +++++++++++++++++++------------------ igniter/install_thread.py | 19 ++++- openpype/version.py | 2 +- start.py | 24 +++---- 4 files changed, 104 insertions(+), 84 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 73ef8283a7..3a2dbe81c4 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -411,16 +411,7 @@ class OpenPypeVersion(semver.VersionInfo): # 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, 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 - ) - + versions = OpenPypeVersion.get_versions_from_directory(dir_to_search) filtered_versions = [] for version in versions: @@ -498,14 +489,11 @@ class OpenPypeVersion(semver.VersionInfo): @staticmethod def get_versions_from_directory( - openpype_dir: Path, - compatible_with: OpenPypeVersion = None) -> List: + openpype_dir: Path) -> 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 @@ -515,17 +503,27 @@ class OpenPypeVersion(semver.VersionInfo): """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version - _openpype_versions = [] + openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): - return _openpype_versions + return openpype_versions # iterate over directory in first level and find all that might # contain OpenPype. for item in openpype_dir.iterdir(): - - # if file, strip extension, in case of dir not. + # if the item is directory with major.minor version, dive deeper + try: + ver_dir = item.name.split(".")[ + 0] == installed_version.major and \ + item.name.split(".")[ + 1] == installed_version.minor # noqa: E051 + if item.is_dir() and ver_dir: + _versions = OpenPypeVersion.get_versions_from_directory( + item) + if _versions: + openpype_versions.append(_versions) + except IndexError: + pass + # if file exists, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -543,13 +541,10 @@ class OpenPypeVersion(semver.VersionInfo): )[0]: continue - if not detected_version.is_compatible(compatible_with): - continue - detected_version.path = item - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) - return sorted(_openpype_versions) + return sorted(openpype_versions) @staticmethod def get_installed_version_str() -> str: @@ -577,15 +572,14 @@ class OpenPypeVersion(semver.VersionInfo): def get_latest_version( staging: bool = False, local: bool = None, - remote: bool = None, - compatible_with: OpenPypeVersion = None + remote: bool = None ) -> Union[OpenPypeVersion, None]: - """Get latest available version. + """Get the latest available version. The version does not contain information about path and source. - This is utility version to get latest version from all found. Build - version is not listed if staging is enabled. + This is utility version to get the latest version from all found. + Build version is not listed if staging is enabled. Arguments 'local' and 'remote' define if local and remote repository versions are used. All versions are used if both are not set (or set @@ -597,8 +591,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. + + Returns: + Latest OpenPypeVersion or None """ if local is None and remote is None: @@ -612,8 +607,6 @@ class OpenPypeVersion(semver.VersionInfo): remote = True installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version local_versions = [] remote_versions = [] if local: @@ -633,10 +626,7 @@ class OpenPypeVersion(semver.VersionInfo): all_versions.sort() latest_version: OpenPypeVersion - latest_version = all_versions[-1] - if not latest_version.is_compatible(compatible_with): - return None - return latest_version + return all_versions[-1] @classmethod def get_expected_studio_version(cls, staging=False, global_settings=None): @@ -1191,13 +1181,27 @@ class BootstrapRepos: @staticmethod def find_latest_openpype_version( - staging, compatible_with: OpenPypeVersion = None): + staging: bool, + compatible_with: OpenPypeVersion = None + ) -> Union[OpenPypeVersion, None]: + """Find the latest available OpenPype version in all location. + + Args: + staging (bool): True to look for staging versions. + compatible_with (OpenPypeVersion, optional): If set, it will + try to find the latest version compatible with the + one specified. + + Returns: + Latest OpenPype version on None if nothing was found. + + """ installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions( - staging=staging, compatible_with=compatible_with + staging=staging ) remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, compatible_with=compatible_with + staging=staging ) all_versions = local_versions + remote_versions if not staging: @@ -1206,6 +1210,12 @@ class BootstrapRepos: if not all_versions: return None + if compatible_with: + all_versions = [ + version for version in all_versions + if version.is_compatible(installed_version) + ] + all_versions.sort() latest_version = all_versions[-1] if latest_version == installed_version: @@ -1222,8 +1232,7 @@ class BootstrapRepos: self, openpype_path: Union[Path, str] = None, staging: bool = False, - include_zips: bool = False, - compatible_with: OpenPypeVersion = None + include_zips: bool = False ) -> Union[List[OpenPypeVersion], None]: """Get ordered dict of detected OpenPype version. @@ -1256,36 +1265,29 @@ class BootstrapRepos: """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" " not implemented yet.")) - 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, self.data_dir / version_dir] + dirs_to_search = [self.data_dir] if openpype_path: - dirs_to_search = [openpype_path, openpype_path / version_dir] + dirs_to_search = [openpype_path] elif os.getenv("OPENPYPE_PATH") \ and Path(os.getenv("OPENPYPE_PATH")).exists(): # first try OPENPYPE_PATH and if that is not available, # try registry. - dirs_to_search = [Path(os.getenv("OPENPYPE_PATH")), - Path(os.getenv("OPENPYPE_PATH")) / version_dir] + dirs_to_search = [Path(os.getenv("OPENPYPE_PATH"))] else: try: registry_dir = Path( str(self.registry.get_item("openPypePath"))) if registry_dir.exists(): - dirs_to_search = [ - registry_dir, registry_dir / version_dir - ] + dirs_to_search = [registry_dir] except ValueError: # nothing found in registry, we'll use data dir @@ -1295,7 +1297,7 @@ class BootstrapRepos: for dir_to_search in dirs_to_search: try: openpype_versions += self.get_openpype_versions( - dir_to_search, staging, compatible_with=compatible_with) + dir_to_search, staging) except ValueError: # location is invalid, skip it pass @@ -1663,15 +1665,12 @@ class BootstrapRepos: def get_openpype_versions( self, openpype_dir: Path, - staging: bool = False, - compatible_with: OpenPypeVersion = None) -> list: + staging: bool = False) -> 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 @@ -1681,17 +1680,24 @@ class BootstrapRepos: """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") - _openpype_versions = [] + openpype_versions = [] # iterate over directory in first level and find all that might # contain OpenPype. for item in openpype_dir.iterdir(): - - # if file, strip extension, in case of dir not. + # if the item is directory with major.minor version, dive deeper + try: + ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E051 + if item.is_dir() and ver_dir: + _versions = self.get_openpype_versions( + item, staging=staging) + if _versions: + openpype_versions.append(_versions) + except IndexError: + pass + # if it is file, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -1709,17 +1715,14 @@ class BootstrapRepos: ): continue - if not detected_version.is_compatible(compatible_with): - continue - detected_version.path = item if staging and detected_version.is_staging(): - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) if not staging and not detected_version.is_staging(): - _openpype_versions.append(detected_version) + openpype_versions.append(detected_version) - return sorted(_openpype_versions) + return sorted(openpype_versions) class OpenPypeVersionExists(Exception): diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 8e31f8cb8f..0cccf664e7 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -62,7 +62,7 @@ class InstallThread(QThread): progress_callback=self.set_progress, message=self.message) local_version = OpenPypeVersion.get_installed_version_str() - # if user did entered nothing, we install OpenPype from local version. + # if user did enter nothing, we install OpenPype from local version. # zip content of `repos`, copy it to user data dir and append # version to it. if not self._path: @@ -93,6 +93,23 @@ class InstallThread(QThread): detected = bs.find_openpype(include_zips=True) if detected: + if not OpenPypeVersion.get_installed_version().is_compatible( + detected[-1]): + self.message.emit(( + f"Latest detected version {detected[-1]} " + "is not compatible with the currently running " + f"{local_version}" + ), True) + self.message.emit(( + "Filtering detected versions to compatible ones..." + ), False) + + detected = [ + version for version in detected + if version.is_compatible( + OpenPypeVersion.get_installed_version()) + ] + if OpenPypeVersion( version=local_version, path=Path()) < detected[-1]: self.message.emit(( diff --git a/openpype/version.py b/openpype/version.py index c41e69d00d..d85f9f60ed 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring Pype version.""" +"""Package declaring OpenPype version.""" __version__ = "3.13.1-nightly.1" diff --git a/start.py b/start.py index c7bced20bd..52e98bb6e1 100644 --- a/start.py +++ b/start.py @@ -699,8 +699,7 @@ def _find_frozen_openpype(use_version: str = None, # Version says to use latest version _print(">>> Finding latest version defined by use version") openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version - ) + use_staging) else: _print(f">>> Finding specified version \"{use_version}\"") openpype_version = bootstrap.find_openpype_version( @@ -712,18 +711,11 @@ def _find_frozen_openpype(use_version: str = None, 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(f">>> Finding studio version \"{studio_version}\"") openpype_version = bootstrap.find_openpype_version( - studio_version, use_staging, compatible_with=installed_version - ) + studio_version, use_staging) if openpype_version is None: raise OpenPypeVersionNotFound(( "Requested OpenPype version " @@ -737,8 +729,8 @@ def _find_frozen_openpype(use_version: str = None, ">>> Finding latest version compatible " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version - ) + use_staging, compatible_with=installed_version) + if openpype_version is None: if use_staging: reason = "Didn't find any staging versions." @@ -756,6 +748,14 @@ def _find_frozen_openpype(use_version: str = None, _initialize_environment(openpype_version) return version_path + if not installed_version.is_compatible(openpype_version): + raise OpenPypeVersionIncompatible( + ( + f"Latest version found {openpype_version} is not " + f"compatible with currently running {installed_version}" + ) + ) + # test if latest detected is installed (in user data dir) is_inside = False try: From ffea3e85fee6058fd3fc38982d228c51f463645c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:34:30 +0800 Subject: [PATCH 0745/1030] 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 0746/1030] 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 0747/1030] 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 c1d3d704106638e1d28ef338a958496790578c40 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 15:40:17 +0200 Subject: [PATCH 0748/1030] :rotating_light: fix hound :dog: --- igniter/bootstrap_repos.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 3a2dbe81c4..6a04198fc9 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1264,7 +1264,6 @@ class BootstrapRepos: ok install it as normal version. """ - installed_version = OpenPypeVersion.get_installed_version() if openpype_path and not isinstance(openpype_path, Path): raise NotImplementedError( ("Finding OpenPype in non-filesystem locations is" @@ -1689,7 +1688,7 @@ class BootstrapRepos: for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper try: - ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E051 + ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E501 if item.is_dir() and ver_dir: _versions = self.get_openpype_versions( item, staging=staging) From 85575e3a99f5618304fc41f5e73a117fe66abc0b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Aug 2022 21:40:40 +0800 Subject: [PATCH 0749/1030] 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 0750/1030] 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 0751/1030] 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 7176723aa5f8710ca422d9fd40577a6b85bc7b81 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 17:26:46 +0200 Subject: [PATCH 0752/1030] :bug: fix arguments and recursive folders --- igniter/bootstrap_repos.py | 44 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 6a04198fc9..01d7c4bb7e 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -425,7 +425,7 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_remote_versions( cls, production: bool = None, - staging: bool = None, compatible_with: OpenPypeVersion = None + staging: bool = None ) -> List: """Get all versions available in OpenPype Path. @@ -470,13 +470,7 @@ class OpenPypeVersion(semver.VersionInfo): if not dir_to_search: return [] - # 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) + versions = cls.get_versions_from_directory(dir_to_search) filtered_versions = [] for version in versions: @@ -511,18 +505,13 @@ class OpenPypeVersion(semver.VersionInfo): # contain OpenPype. for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper - try: - ver_dir = item.name.split(".")[ - 0] == installed_version.major and \ - item.name.split(".")[ - 1] == installed_version.minor # noqa: E051 - if item.is_dir() and ver_dir: - _versions = OpenPypeVersion.get_versions_from_directory( - item) - if _versions: - openpype_versions.append(_versions) - except IndexError: - pass + + if item.is_dir() and re.match(r"^\d+\.\d+$", item.name): + _versions = OpenPypeVersion.get_versions_from_directory( + item) + if _versions: + openpype_versions += _versions + # if file exists, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) @@ -1687,15 +1676,12 @@ class BootstrapRepos: # contain OpenPype. for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper - try: - ver_dir = item.name.split(".")[0] == installed_version.major and item.name.split(".")[1] == installed_version.minor # noqa: E501 - if item.is_dir() and ver_dir: - _versions = self.get_openpype_versions( - item, staging=staging) - if _versions: - openpype_versions.append(_versions) - except IndexError: - pass + if item.is_dir() and re.match(r"^\d+\.\d+$", item.name): + _versions = self.get_openpype_versions( + item, staging=staging) + if _versions: + openpype_versions += _versions + # if it is file, strip extension, in case of dir don't. name = item.name if item.is_dir() else item.stem result = OpenPypeVersion.version_in_str(name) From 7676827644c385991e35687b303032dd491e9361 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 17:27:47 +0200 Subject: [PATCH 0753/1030] initial commit of settings dev --- website/docs/dev_settings.md | 890 +++++++++++++++++++++++++++++++++++ website/sidebars.js | 1 + 2 files changed, 891 insertions(+) create mode 100644 website/docs/dev_settings.md diff --git a/website/docs/dev_settings.md b/website/docs/dev_settings.md new file mode 100644 index 0000000000..483bd18535 --- /dev/null +++ b/website/docs/dev_settings.md @@ -0,0 +1,890 @@ +--- +id: dev_settings +title: Settings +sidebar_label: Settings +--- + +Settings gives ability to change how OpenPype behaves in certain situations. Settings are split into 3 categories **system settings**, **project anatomy** and **project settings**. Project anatomy and project settings are in grouped into single category but there is a technical difference (explained later). Only difference in system and project settings is that system settings can't be technically handled on a project level or their values must be available no matter in which project are values received. Settings have headless entities or settings UI. + +There is one more category **local settings** but they don't have ability to be changed or defined easily. Local settings can change how settings work per machine, can affect both system and project settings but they're hardcoded for predefined values at this moment. + +## Settings schemas +System and project settings are defined by settings schemas. Schema define structure of output value, what value types output will contain, how settings are stored and how it's UI input will look. + +## Settings values +Output of settings is a json serializable value. There are 3 possible types of value **default values**, **studio overrides** and **project overrides**. Default values must be always available for all settings schemas, their values are stored to code. Default values is what everyone who just installed OpenPype will use as default values. It is good practice to set example values but they should be relevant. + +Setting overrides is what makes settings powerful tool. Overrides contain only a part of settings with additional metadata which describe which parts of settings values that should be replaced from overrides values. Using overrides gives ability to save only specific values and use default values for rest. It is super useful in project settings which have up to 2 levels of overrides. In project settings are used **default values** as base on which are applied **studio overrides** and then **project overrides**. In practice it is possible to save only studio overrides which affect all projects. Changes in studio overrides are then propagated to all projects without project overrides. But values can be locked on project level so studio overrides are not used. + +## Settings storage +As was mentined default values are stored into repository files. Overrides are stored to Mongo database. The value in mongo contain only overrides with metadata so their content on it's own is useless and must be used with combination of default values. System settings and project settings are stored into special collection. Single document represents one set of overrides with OpenPype version for which is stored. Settings are versioned and are loaded in specific order - current OpenPype version overrides or first lower available. If there are any overrides with same or lower version then first higher version is used. If there are any overrides then no overrides are applied. + +Project anatomy is stored into project document thus is not versioned and it's values are always overriden. Any changes in anatomy schema may have drastic effect on production and OpenPype updates. + +## Settings schema items +As was mentioned schema items define output type of values, how they are stored and how they look in UI. +- schemas are (by default) defined by a json files +- OpenPype core system settings schemas are stored in `~/openpype/settings/entities/schemas/system_schema/` and project settings in `~/openpype/settings/entities/schemas/projects_schema/` + - both contain `schema_main.json` which are entry points +- OpenPype modules/addons can define their settings schemas using `BaseModuleSettingsDef` in that case some functionality may be slightly modified +- single schema item is represented by dictionary (object) in json which has `"type"` key. + - **type** is only common key which is required for all schema items +- each item may have "input modifiers" (other keys in dictionary) and they may be required or optional based on the type +- there are special keys across all items + - `"is_file"` - this key is used when defaults values are stored which define that this key is a filename where it's values are stored + - key is validated must be once in hierarchy else it won't be possible to store default values + - make sense to fill it only if it's value if `true` + - `"is_group"` - define that all values under a key in settings hierarchy will be overridden if any value is modified + - this key is not allowed for all inputs as they may not have technical ability to handle it + - key is validated can be only once in hierarchy and is automatically filled on last possible item if is not defined in schemas + - make sense to fill it only if it's value if `true` +- all entities can have set `"tooltip"` key with description which will be shown in UI on hover + +### Inner schema +Settings schemas are big json files which would became unmanageable if would be in single file. To be able to split them into multiple files to help organize them special types `schema` and `template` were added. Both types are relating to a different file by filename. If json file contains dictionary it is considered as `schema` if contains list it is considered as `template`. + +#### schema +Schema item is replaced by content of entered schema name. It is recommended that schema file is used only once in settings hierarchy. Templates are meant for reusing. +- schema must have `"name"` key which is name of schema that should be used + +```javascript +{ + "type": "schema", + "name": "my_schema_name" +} +``` + +#### template +Templates are almost the same as schema items but can contain one or more items which can be formatted with additional data or some keys can be skipped if needed. Templates are meant for reusing the same schemas with ability to modify content. + +- legacy name is `schema_template` (still usable) +- template must have `"name"` key which is name of template file that should be used +- to fill formatting keys use `"template_data"` +- all items in template, except `__default_values__`, will replace `template` item in original schema +- template may contain other templates + +```javascript +// Example template json file content +[ + { + // Define default values for formatting values + // - gives ability to set the value but have default value + "__default_values__": { + "multipath_executables": true + } + }, { + "type": "raw-json", + "label": "{host_label} Environments", + "key": "{host_name}_environments" + }, { + "type": "path", + "key": "{host_name}_executables", + "label": "{host_label} - Full paths to executables", + "multiplatform": "{multipath_executables}", + "multipath": true + } +] +``` +```javascript +// Example usage of the template in schema +{ + "type": "dict", + "key": "template_examples", + "label": "Schema template examples", + "children": [ + { + "type": "template", + "name": "example_template", + "template_data": [ + { + "host_label": "Maya 2019", + "host_name": "maya_2019", + "multipath_executables": false + }, + { + "host_label": "Maya 2020", + "host_name": "maya_2020" + }, + { + "host_label": "Maya 2021", + "host_name": "maya_2021" + } + ] + } + ] +} +``` +```javascript +// The same schema defined without templates +{ + "type": "dict", + "key": "template_examples", + "label": "Schema template examples", + "children": [ + { + "type": "raw-json", + "label": "Maya 2019 Environments", + "key": "maya_2019_environments" + }, { + "type": "path", + "key": "maya_2019_executables", + "label": "Maya 2019 - Full paths to executables", + "multiplatform": false, + "multipath": true + }, { + "type": "raw-json", + "label": "Maya 2020 Environments", + "key": "maya_2020_environments" + }, { + "type": "path", + "key": "maya_2020_executables", + "label": "Maya 2020 - Full paths to executables", + "multiplatform": true, + "multipath": true + }, { + "type": "raw-json", + "label": "Maya 2021 Environments", + "key": "maya_2021_environments" + }, { + "type": "path", + "key": "maya_2021_executables", + "label": "Maya 2021 - Full paths to executables", + "multiplatform": true, + "multipath": true + } + ] +} +``` + +Template data can be used only to fill templates in values but not in keys. It is also possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above). +```javascript +{ + ... + // Allowed + "key": "{to_fill}" + ... + // Not allowed + "{to_fill}": "value" + ... +} +``` + +Because formatting value can be only string it is possible to use formatting values which are replaced with different type. +```javascript +// Template data +{ + "template_data": { + "executable_multiplatform": { + "type": "schema", + "name": "my_multiplatform_schema" + } + } +} +// Template content +{ + ... + // Allowed - value is replaced with dictionary + "multiplatform": "{executable_multiplatform}" + ... + // Not allowed - there is no way how it could be replaced + "multiplatform": "{executable_multiplatform}_enhanced_string" + ... +} +``` + +#### dynamic_schema +Dynamic schema item marks a place in settings schema where schemas defined by `BaseModuleSettingsDef` can be placed. +- example: +``` +{ + "type": "dynamic_schema", + "name": "project_settings/global" +} +``` +- `BaseModuleSettingsDef` with implemented `get_settings_schemas` can return a dictionary where key define a dynamic schema name and value schemas that will be put there +- dynamic schemas work almost the same way as templates + - one item can be replaced by multiple items (or by 0 items) +- goal is to dynamically load settings of OpenPype modules without having their schemas or default values in core repository + - values of these schemas are saved using the `BaseModuleSettingsDef` methods +- we recommend to use `JsonFilesSettingsDef` which has full implementation of storing default values to json files + - requires only to implement method `get_settings_root_path` which should return path to root directory where settings schema can be found and default values will be saved + +### Basic Dictionary inputs +These inputs wraps another inputs into {key: value} relation + +#### dict +- this is dictionary type wrapping more inputs with keys defined in schema +- may be used as dynamic children (e.g. in `list` or `dict-modifiable`) + - in that case the only key modifier is `children` which is list of it's keys + - USAGE: e.g. List of dictionaries where each dictionary have same structure. +- if is not used as dynamic children then must have defined `"key"` under which are it's values stored +- may be with or without `"label"` (only for GUI) + - `"label"` must be set to be able mark item as group with `"is_group"` key set to True +- item with label can visually wrap it's children + - this option is enabled by default to turn off set `"use_label_wrap"` to `False` + - label wrap is by default collapsible + - that can be set with key `"collapsible"` to `True`/`False` + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add lighter background with `"highlight_content"` (Default: `False`) + - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color + - output is dictionary `{the "key": children values}` +``` +# Example +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "collapsible": true, + "highlight_content": true, + "is_group": true, + "is_file": true, + "children": [ + ...ITEMS... + ] +} + +# Without label +{ + "type": "dict", + "key": "global", + "children": [ + ...ITEMS... + ] +} + +# When used as widget +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} +``` + +#### dict-roots +- entity can be used only in Project settings +- keys of dictionary are based on current project roots +- they are not updated "live" it is required to save root changes and then + modify values on this entity + # TODO do live updates +``` +{ + "type": "dict-roots", + "key": "roots", + "label": "Roots", + "object_type": { + "type": "path", + "multiplatform": true, + "multipath": false + } +} +``` + +#### dict-conditional +- is similar to `dict` but has always available one enum entity + - the enum entity has single selection and it's value define other children entities +- each value of enumerator have defined children that will be used + - there is no way how to have shared entities across multiple enum items +- value from enumerator is also stored next to other values + - to define the key under which will be enum value stored use `enum_key` + - `enum_key` must match key regex and any enum item can't have children with same key + - `enum_label` is label of the entity for UI purposes +- enum items are define with `enum_children` + - it's a list where each item represents single item for the enum + - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` + - enum items can define `label` for UI purposes + - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) +- to set default value for `enum_key` set it with `enum_default` +- entity must have defined `"label"` if is not used as widget +- is set as group if any parent is not group (can't have children as group) +- may be with or without `"label"` (only for GUI) + - `"label"` must be set to be able mark item as group with `"is_group"` key set to True +- item with label can visually wrap it's children + - this option is enabled by default to turn off set `"use_label_wrap"` to `False` + - label wrap is by default collapsible + - that can be set with key `"collapsible"` to `True`/`False` + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add lighter background with `"highlight_content"` (Default: `False`) + - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color +- for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`) + - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`) +- output is dictionary `{the "key": children values}` +- using this type as template item for list type can be used to create infinite hierarchies + +``` +# Example +{ + "type": "dict-conditional", + "key": "my_key", + "label": "My Key", + "enum_key": "type", + "enum_label": "label", + "enum_children": [ + # Each item must be a dictionary with 'key' + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": "text" + } + ] + }, + { + # Separator does not have children as "separator" value is enough + "key": "separator", + "label": "Separator" + } + ] +} +``` + +How output of the schema could look like on save: +``` +{ + "type": "separator" +} + +{ + "type": "action", + "key": "action_1", + "label": "Action 1", + "command": "run command -arg" +} + +{ + "type": "menu", + "children": [ + "child_1", + "child_2" + ] +} +``` + +### Inputs for setting any kind of value (`Pure` inputs) +- all inputs must have defined `"key"` if are not used as dynamic item + - they can also have defined `"label"` + +#### boolean +- simple checkbox, nothing more to set +``` +{ + "type": "boolean", + "key": "my_boolean_key", + "label": "Do you want to use Pype?" +} +``` + +#### number +- number input, can be used for both integer and float + - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) + - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) + - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) +- for UI it is possible to show slider to enable this option set `show_slider` to `true` +``` +{ + "type": "number", + "key": "fps", + "label": "Frame rate (FPS)" + "decimal": 2, + "minimum": 1, + "maximum": 300000 +} +``` + +``` +{ + "type": "number", + "key": "ratio", + "label": "Ratio" + "decimal": 3, + "minimum": 0, + "maximum": 1, + "show_slider": true +} +``` + +#### text +- simple text input + - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) + +``` +{ + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" +} +``` + +#### path-input +- Do not use this input in schema please (use `path` instead) +- this input is implemented to add additional features to text input +- this is meant to be used in proxy input `path` + +#### raw-json +- a little bit enhanced text input for raw json +- can store dictionary (`{}`) or list (`[]`) but not both + - by default stores dictionary to change it to list set `is_list` to `True` +- has validations of json format +- output can be stored as string + - this is to allow any keys in dictionary + - set key `store_as_string` to `true` + - code using that setting must expected that value is string and use json module to convert it to python types + +``` +{ + "type": "raw-json", + "key": "profiles", + "label": "Extract Review profiles", + "is_list": true +} +``` + +#### enum +- enumeration of values that are predefined in schema +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- values are defined under value of key `"enum_items"` as list + - each item in list is simple dictionary where value is label and key is value which will be stored + - should be possible to enter single dictionary if order of items doesn't matter +- it is possible to set default selected value/s with `default` attribute + - it is recommended to use this option only in single selection mode + - at the end this option is used only when defying default settings value or in dynamic items + +``` +{ + "key": "tags", + "label": "Tags", + "type": "enum", + "multiselection": true, + "enum_items": [ + {"burnin": "Add burnins"}, + {"ftrackreview": "Add to Ftrack"}, + {"delete": "Delete output"}, + {"slate-frame": "Add slate frame"}, + {"no-handles": "Skip handle frames"} + ] +} +``` + +#### anatomy-templates-enum +- enumeration of all available anatomy template keys +- have only single selection mode +- it is possible to define default value `default` + - `"work"` is used if default value is not specified +- enum values are not updated on the fly it is required to save templates and + reset settings to recache values +``` +{ + "key": "host", + "label": "Host name", + "type": "anatomy-templates-enum", + "default": "publish" +} +``` + +#### hosts-enum +- enumeration of available hosts +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) +- it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) +- to filter host names it is required to define `"hosts_filter"` which is list of host names that will be available + - do not pass empty string if `use_empty_value` is enabled + - ignoring host names would be more dangerous in some cases +``` +{ + "key": "host", + "label": "Host name", + "type": "hosts-enum", + "multiselection": false, + "use_empty_value": true, + "custom_labels": { + "": "N/A", + "nuke": "Nuke" + }, + "hosts_filter": [ + "nuke" + ] +} +``` + +#### apps-enum +- enumeration of available application and their variants from system settings + - applications without host name are excluded +- can be used only in project settings +- has only `multiselection` +- used only in project anatomy +``` +{ + "type": "apps-enum", + "key": "applications", + "label": "Applications" +} +``` + +#### tools-enum +- enumeration of available tools and their variants from system settings +- can be used only in project settings +- has only `multiselection` +- used only in project anatomy +``` +{ + "type": "tools-enum", + "key": "tools_env", + "label": "Tools" +} +``` + +#### task-types-enum +- enumeration of task types from current project +- enum values are not updated on the fly and modifications of task types on project require save and reset to be propagated to this enum +- has set `multiselection` to `True` but can be changed to `False` in schema + +#### deadline_url-enum +- deadline module specific enumerator using deadline system settings to fill it's values +- TODO: move this type to deadline module + +### Inputs for setting value using Pure inputs +- these inputs also have required `"key"` +- attribute `"label"` is required in few conditions + - when item is marked `as_group` or when `use_label_wrap` +- they use Pure inputs "as widgets" + +#### list +- output is list +- items can be added and removed +- items in list must be the same type +- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True` + - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does) +- type of items is defined with key `"object_type"` +- there are 2 possible ways how to set the type: + 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) + 2.) item type name as string without modifiers (e.g. `text`) + 3.) enhancement of 1.) there is also support of `template` type but be carefull about endless loop of templates + - goal of using `template` is to easily change same item definitions in multiple lists + +1.) with item modifiers +``` +{ + "type": "list", + "key": "exclude_ports", + "label": "Exclude ports", + "object_type": { + "type": "number", # number item type + "minimum": 1, # minimum modifier + "maximum": 65535 # maximum modifier + } +} +``` + +2.) without modifiers +``` +{ + "type": "list", + "key": "exclude_ports", + "label": "Exclude ports", + "object_type": "text" +} +``` + +3.) with template definition +``` +# Schema of list item where template is used +{ + "type": "list", + "key": "menu_items", + "label": "Menu Items", + "object_type": { + "type": "template", + "name": "template_object_example" + } +} + +# WARNING: +# In this example the template use itself inside which will work in `list` +# but may cause an issue in other entity types (e.g. `dict`). + +'template_object_example.json' : +[ + { + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible": true, + "key": "menu_items", + "label": "Menu items", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + } + ] + }, { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": { + "type": "template", + "name": "template_object_example" + } + } + ] + } + ] + } +] +``` + +#### dict-modifiable +- one of dictionary inputs, this is only used as value input +- items in this input can be removed and added same way as in `list` input +- value items in dictionary must be the same type +- type of items is defined with key `"object_type"` +- required keys may be defined under `"required_keys"` + - required keys must be defined as a list (e.g. `["key_1"]`) and are moved to the top + - these keys can't be removed or edited (it is possible to edit label if item is collapsible) +- there are 2 possible ways how to set the type: + 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) + 2.) item type name as string without modifiers (e.g. `text`) +- this input can be collapsible + - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + +1.) with item modifiers +``` +{ + "type": "dict-modifiable", + "object_type": { + "type": "number", + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +2.) without modifiers +``` +{ + "type": "dict-modifiable", + "object_type": "text", + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +#### path +- input for paths, use `path-input` internally +- has 2 input modifiers `"multiplatform"` and `"multipath"` + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multipath"` - it is possible to enter multiple paths + - if both are enabled result is dictionary with lists + +``` +{ + "type": "path", + "key": "ffmpeg_path", + "label": "FFmpeg path", + "multiplatform": true, + "multipath": true +} +``` + +#### list-strict +- input for strict number of items in list +- each child item can be different type with different possible modifiers +- it is possible to display them in horizontal or vertical layout + - key `"horizontal"` as `True`/`False` (Default: `True`) +- each child may have defined `"label"` which is shown next to input + - label does not reflect modifications or overrides (TODO) +- children item are defined under key `"object_types"` which is list of dictionaries + - key `"children"` is not used because is used for hierarchy validations in schema +- USAGE: For colors, transformations, etc. Custom number and different modifiers + give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. + +``` +{ + "type": "list-strict", + "key": "color", + "label": "Color", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] +} +``` + +#### color +- preimplemented entity to store and load color values +- entity store and expect list of 4 integers in range 0-255 + - integers represents rgba [Red, Green, Blue, Alpha] + +``` +{ + "type": "color", + "key": "bg_color", + "label": "Background Color" +} +``` + +### Noninteractive items +Items used only for UI purposes. + +#### label +- add label with note or explanations +- it is possible to use html tags inside the label +- set `work_wrap` to `true`/`false` if you want to enable word wrapping in UI (default: `false`) + +``` +{ + "type": "label", + "label": "RED LABEL: Normal label" +} +``` + +#### separator +- legacy name is `splitter` (still usable) +- visual separator of items (more divider than separator) + +``` +{ + "type": "separator" +} +``` + +### Anatomy +Anatomy represents data stored on project document. + +#### anatomy +- entity works similarly to `dict` +- anatomy has always all keys overridden with overrides + - overrides are not applied as all anatomy data must be available from project document + - all children must be groups + +### Proxy wrappers +- should wraps multiple inputs only visually +- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled +- can't be used as widget (first item in e.g. `list`, `dict-modifiable`, etc.) + +#### form +- wraps inputs into form look layout +- should be used only for Pure inputs + +``` +{ + "type": "dict-form", + "children": [ + { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + ... + } + ] +} +``` + + +#### collapsible-wrap +- wraps inputs into collapsible widget + - looks like `dict` but does not hold `"key"` +- should be used only for Pure inputs + +``` +{ + "type": "collapsible-wrap", + "label": "Collapsible example" + "children": [ + { + "type": "text", + "key": "_example_input_collapsible", + "label": "Example input in collapsible wrapper" + }, { + ... + } + ] +} diff --git a/website/sidebars.js b/website/sidebars.js index 9d60a5811c..b7b44bbada 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -152,6 +152,7 @@ module.exports = { "dev_build", "dev_testing", "dev_contribute", + "dev_settings", { type: "category", label: "Hosts integrations", From 60ea9728f63afa2c0ec2c32bd619fec8e64993ec Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 17:43:02 +0200 Subject: [PATCH 0754/1030] :rotating_light: fix hound :dog: --- igniter/bootstrap_repos.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 01d7c4bb7e..3dab67ebf1 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -496,7 +496,6 @@ class OpenPypeVersion(semver.VersionInfo): ValueError: if invalid path is specified. """ - installed_version = OpenPypeVersion.get_installed_version() openpype_versions = [] if not openpype_dir.exists() and not openpype_dir.is_dir(): return openpype_versions @@ -1667,7 +1666,6 @@ class BootstrapRepos: ValueError: if invalid path is specified. """ - installed_version = OpenPypeVersion.get_installed_version() if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError(f"specified directory {openpype_dir} is invalid") From c3b69e86d4a64d63865dbdb55a06b3a1f7a67c38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 17:52:33 +0200 Subject: [PATCH 0755/1030] small tweaks --- website/docs/dev_settings.md | 64 ++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/website/docs/dev_settings.md b/website/docs/dev_settings.md index 483bd18535..cb16ae76ca 100644 --- a/website/docs/dev_settings.md +++ b/website/docs/dev_settings.md @@ -214,7 +214,7 @@ These inputs wraps another inputs into {key: value} relation #### dict - this is dictionary type wrapping more inputs with keys defined in schema -- may be used as dynamic children (e.g. in `list` or `dict-modifiable`) +- may be used as dynamic children (e.g. in [list](#list) or [dict-modifiable](#dict-modifiable)) - in that case the only key modifier is `children` which is list of it's keys - USAGE: e.g. List of dictionaries where each dictionary have same structure. - if is not used as dynamic children then must have defined `"key"` under which are it's values stored @@ -600,7 +600,7 @@ How output of the schema could look like on save: - type of items is defined with key `"object_type"` - there are 2 possible ways how to set the type: 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) - 2.) item type name as string without modifiers (e.g. `text`) + 2.) item type name as string without modifiers (e.g. [text](#text)) 3.) enhancement of 1.) there is also support of `template` type but be carefull about endless loop of templates - goal of using `template` is to easily change same item definitions in multiple lists @@ -690,18 +690,31 @@ How output of the schema could look like on save: - one of dictionary inputs, this is only used as value input - items in this input can be removed and added same way as in `list` input - value items in dictionary must be the same type -- type of items is defined with key `"object_type"` - required keys may be defined under `"required_keys"` - required keys must be defined as a list (e.g. `["key_1"]`) and are moved to the top - these keys can't be removed or edited (it is possible to edit label if item is collapsible) -- there are 2 possible ways how to set the type: - 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) - 2.) item type name as string without modifiers (e.g. `text`) +- type of items is defined with key `"object_type"` + - there are 2 possible ways how to set the object type (Examples below): + 1. just a type name as string without modifiers (e.g. `"text"`) + 2. full types with modifiers as dictionary(`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` - this input can be collapsible + - `"use_label_wrap"` must be set to `True` (Default behavior) - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) -1.) with item modifiers +1. **Object type** without modifiers +``` +{ + "type": "dict-modifiable", + "object_type": "text", + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +2. **Object type** with item modifiers ``` { "type": "dict-modifiable", @@ -717,22 +730,10 @@ How output of the schema could look like on save: } ``` -2.) without modifiers -``` -{ - "type": "dict-modifiable", - "object_type": "text", - "is_group": true, - "key": "templates_mapping", - "label": "Muster - Templates mapping", - "is_file": true -} -``` - #### path - input for paths, use `path-input` internally - has 2 input modifiers `"multiplatform"` and `"multipath"` - - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs (result is dictionary) - `"multipath"` - it is possible to enter multiple paths - if both are enabled result is dictionary with lists @@ -797,6 +798,8 @@ How output of the schema could look like on save: - preimplemented entity to store and load color values - entity store and expect list of 4 integers in range 0-255 - integers represents rgba [Red, Green, Blue, Alpha] +- has modifier `"use_alpha"` which can be `True`/`False` + - alpha is always `255` if set to `True` and alpha slider is not visible in UI ``` { @@ -806,6 +809,13 @@ How output of the schema could look like on save: } ``` +### Anatomy +Anatomy represents data stored on project document. Item cares about **Project Anatomy**. + +#### anatomy +- entity is just enhanced [dict](#dict) item +- anatomy has always all keys overridden with overrides + ### Noninteractive items Items used only for UI purposes. @@ -831,15 +841,6 @@ Items used only for UI purposes. } ``` -### Anatomy -Anatomy represents data stored on project document. - -#### anatomy -- entity works similarly to `dict` -- anatomy has always all keys overridden with overrides - - overrides are not applied as all anatomy data must be available from project document - - all children must be groups - ### Proxy wrappers - should wraps multiple inputs only visually - these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled @@ -888,3 +889,8 @@ Anatomy represents data stored on project document. } ] } +``` + + +## How to add new settings +Always start with modifying or adding new schema and don't worry about values. When you think schema is ready to use launch OpenPype settings in development mode using `poetry run python ./start.py settings --dev` or prepared script in `~/openpype/tools/run_settings(.sh|.ps1)`. Settings opened in development mode have checkbox `Modify defaults` available in bottom left corner. When checked default values are modified and saved on `Save`. This is recommended approach how default settings should be created instead of direct modification of files. From 8b94d746e5595caa42bafae6184663683fd8e4f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:00:26 +0200 Subject: [PATCH 0756/1030] show outdated build dialog when expected version can't be used with current build --- openpype/tools/tray/pype_tray.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 4e5db06a92..2f3e1bcab3 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -10,19 +10,19 @@ from Qt import QtCore, QtGui, QtWidgets import openpype.version from openpype.api import ( - Logger, resources, get_system_settings ) -from openpype.lib import ( - get_openpype_execute_args, +from openpype.lib import get_openpype_execute_args, Logger +from openpype.lib.openpype_version import ( op_version_control_available, + get_expected_version, + get_installed_version, is_current_version_studio_latest, is_current_version_higher_than_expected, is_running_from_build, is_running_staging, - get_expected_version, - get_openpype_version + get_openpype_version, ) from openpype.modules import TrayModulesManager from openpype import style @@ -329,6 +329,21 @@ class TrayManager: self._version_dialog.close() return + installed_version = get_installed_version() + expected_version = get_expected_version() + + # Request new build if is needed + if not expected_version.is_compatible(installed_version): + if ( + self._version_dialog is not None + and self._version_dialog.isVisible() + ): + self._version_dialog.close() + + dialog = BuildVersionDialog() + dialog.exec_() + return + if self._version_dialog is None: self._version_dialog = VersionUpdateDialog() self._version_dialog.restart_requested.connect( @@ -338,7 +353,6 @@ class TrayManager: self._outdated_version_ignored ) - expected_version = get_expected_version() current_version = get_openpype_version() current_is_higher = is_current_version_higher_than_expected() From ad64c3a66e10c2c34ecd4fe3549f636ce5777959 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:02:08 +0200 Subject: [PATCH 0757/1030] added backwards compatibility for 'is_compatible' method --- openpype/tools/tray/pype_tray.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 2f3e1bcab3..85bc00ead6 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -333,7 +333,11 @@ class TrayManager: expected_version = get_expected_version() # Request new build if is needed - if not expected_version.is_compatible(installed_version): + if ( + # Backwards compatibility + not hasattr(expected_version, "is_compatible") + or not expected_version.is_compatible(installed_version) + ): if ( self._version_dialog is not None and self._version_dialog.isVisible() From 1469c471d15c5556e4f0b2f5d06f1e07dcc74724 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:23:12 +0200 Subject: [PATCH 0758/1030] added javascript notation --- website/docs/dev_settings.md | 74 ++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/website/docs/dev_settings.md b/website/docs/dev_settings.md index cb16ae76ca..e5917c7549 100644 --- a/website/docs/dev_settings.md +++ b/website/docs/dev_settings.md @@ -195,7 +195,7 @@ Because formatting value can be only string it is possible to use formatting val #### dynamic_schema Dynamic schema item marks a place in settings schema where schemas defined by `BaseModuleSettingsDef` can be placed. - example: -``` +```javascript { "type": "dynamic_schema", "name": "project_settings/global" @@ -228,8 +228,8 @@ These inputs wraps another inputs into {key: value} relation - it is possible to add lighter background with `"highlight_content"` (Default: `False`) - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color - output is dictionary `{the "key": children values}` -``` -# Example +```javascript +// Example { "key": "applications", "type": "dict", @@ -243,7 +243,7 @@ These inputs wraps another inputs into {key: value} relation ] } -# Without label +// Without label { "type": "dict", "key": "global", @@ -252,7 +252,7 @@ These inputs wraps another inputs into {key: value} relation ] } -# When used as widget +// When used as widget { "type": "list", "key": "profiles", @@ -283,7 +283,7 @@ These inputs wraps another inputs into {key: value} relation - they are not updated "live" it is required to save root changes and then modify values on this entity # TODO do live updates -``` +```javascript { "type": "dict-roots", "key": "roots", @@ -327,8 +327,8 @@ These inputs wraps another inputs into {key: value} relation - output is dictionary `{the "key": children values}` - using this type as template item for list type can be used to create infinite hierarchies -``` -# Example +```javascript +// Example { "type": "dict-conditional", "key": "my_key", @@ -336,7 +336,7 @@ These inputs wraps another inputs into {key: value} relation "enum_key": "type", "enum_label": "label", "enum_children": [ - # Each item must be a dictionary with 'key' + // Each item must be a dictionary with 'key' { "key": "action", "label": "Action", @@ -371,7 +371,7 @@ These inputs wraps another inputs into {key: value} relation ] }, { - # Separator does not have children as "separator" value is enough + // Separator does not have children as "separator" value is enough "key": "separator", "label": "Separator" } @@ -380,7 +380,7 @@ These inputs wraps another inputs into {key: value} relation ``` How output of the schema could look like on save: -``` +```javascript { "type": "separator" } @@ -407,7 +407,7 @@ How output of the schema could look like on save: #### boolean - simple checkbox, nothing more to set -``` +```javascript { "type": "boolean", "key": "my_boolean_key", @@ -422,7 +422,7 @@ How output of the schema could look like on save: - key `"maxium"` as maximum allowed number to enter (Default: `99999`) - key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) - for UI it is possible to show slider to enable this option set `show_slider` to `true` -``` +```javascript { "type": "number", "key": "fps", @@ -433,7 +433,7 @@ How output of the schema could look like on save: } ``` -``` +```javascript { "type": "number", "key": "ratio", @@ -450,7 +450,7 @@ How output of the schema could look like on save: - key `"multiline"` allows to enter multiple lines of text (Default: `False`) - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) -``` +```javascript { "type": "text", "key": "deadline_pool", @@ -473,7 +473,7 @@ How output of the schema could look like on save: - set key `store_as_string` to `true` - code using that setting must expected that value is string and use json module to convert it to python types -``` +```javascript { "type": "raw-json", "key": "profiles", @@ -492,7 +492,7 @@ How output of the schema could look like on save: - it is recommended to use this option only in single selection mode - at the end this option is used only when defying default settings value or in dynamic items -``` +```javascript { "key": "tags", "label": "Tags", @@ -515,7 +515,7 @@ How output of the schema could look like on save: - `"work"` is used if default value is not specified - enum values are not updated on the fly it is required to save templates and reset settings to recache values -``` +```javascript { "key": "host", "label": "Host name", @@ -532,7 +532,7 @@ How output of the schema could look like on save: - to filter host names it is required to define `"hosts_filter"` which is list of host names that will be available - do not pass empty string if `use_empty_value` is enabled - ignoring host names would be more dangerous in some cases -``` +```javascript { "key": "host", "label": "Host name", @@ -555,7 +555,7 @@ How output of the schema could look like on save: - can be used only in project settings - has only `multiselection` - used only in project anatomy -``` +```javascript { "type": "apps-enum", "key": "applications", @@ -568,7 +568,7 @@ How output of the schema could look like on save: - can be used only in project settings - has only `multiselection` - used only in project anatomy -``` +```javascript { "type": "tools-enum", "key": "tools_env", @@ -605,7 +605,7 @@ How output of the schema could look like on save: - goal of using `template` is to easily change same item definitions in multiple lists 1.) with item modifiers -``` +```javascript { "type": "list", "key": "exclude_ports", @@ -619,7 +619,7 @@ How output of the schema could look like on save: ``` 2.) without modifiers -``` +```javascript { "type": "list", "key": "exclude_ports", @@ -629,8 +629,8 @@ How output of the schema could look like on save: ``` 3.) with template definition -``` -# Schema of list item where template is used +```javascript +// Schema of list item where template is used { "type": "list", "key": "menu_items", @@ -641,9 +641,9 @@ How output of the schema could look like on save: } } -# WARNING: -# In this example the template use itself inside which will work in `list` -# but may cause an issue in other entity types (e.g. `dict`). +// WARNING: +// In this example the template use itself inside which will work in `list` +// but may cause an issue in other entity types (e.g. `dict`). 'template_object_example.json' : [ @@ -703,7 +703,7 @@ How output of the schema could look like on save: - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) 1. **Object type** without modifiers -``` +```javascript { "type": "dict-modifiable", "object_type": "text", @@ -715,7 +715,7 @@ How output of the schema could look like on save: ``` 2. **Object type** with item modifiers -``` +```javascript { "type": "dict-modifiable", "object_type": { @@ -737,7 +737,7 @@ How output of the schema could look like on save: - `"multipath"` - it is possible to enter multiple paths - if both are enabled result is dictionary with lists -``` +```javascript { "type": "path", "key": "ffmpeg_path", @@ -759,7 +759,7 @@ How output of the schema could look like on save: - USAGE: For colors, transformations, etc. Custom number and different modifiers give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. -``` +```javascript { "type": "list-strict", "key": "color", @@ -801,7 +801,7 @@ How output of the schema could look like on save: - has modifier `"use_alpha"` which can be `True`/`False` - alpha is always `255` if set to `True` and alpha slider is not visible in UI -``` +```javascript { "type": "color", "key": "bg_color", @@ -824,7 +824,7 @@ Items used only for UI purposes. - it is possible to use html tags inside the label - set `work_wrap` to `true`/`false` if you want to enable word wrapping in UI (default: `false`) -``` +```javascript { "type": "label", "label": "RED LABEL: Normal label" @@ -835,7 +835,7 @@ Items used only for UI purposes. - legacy name is `splitter` (still usable) - visual separator of items (more divider than separator) -``` +```javascript { "type": "separator" } @@ -850,7 +850,7 @@ Items used only for UI purposes. - wraps inputs into form look layout - should be used only for Pure inputs -``` +```javascript { "type": "dict-form", "children": [ @@ -875,7 +875,7 @@ Items used only for UI purposes. - looks like `dict` but does not hold `"key"` - should be used only for Pure inputs -``` +```javascript { "type": "collapsible-wrap", "label": "Collapsible example" From cbff5972a2b1c6aa7ebd9b19d519f122c955fe61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 18:23:24 +0200 Subject: [PATCH 0759/1030] added screenshot of settings UI in dev mode --- website/docs/assets/settings_dev.png | Bin 0 -> 15237 bytes website/docs/dev_settings.md | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 website/docs/assets/settings_dev.png diff --git a/website/docs/assets/settings_dev.png b/website/docs/assets/settings_dev.png new file mode 100644 index 0000000000000000000000000000000000000000..4d0359461e570163925bda6b3a2f5dd1c87c3e7b GIT binary patch literal 15237 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 a Date: Fri, 12 Aug 2022 18:41:55 +0200 Subject: [PATCH 0760/1030] :recycle: distribute ocio as zips --- .../maya/plugins/publish/extract_look.py | 6 ++++- poetry.lock | 25 +++---------------- pyproject.toml | 6 +++-- tools/fetch_thirdparty_libs.py | 23 +++++++++++++---- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b425efba6f..b416669b87 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -43,7 +43,11 @@ def get_ocio_config_path(profile_folder): try: import OpenColorIOConfigs return os.path.join( - os.path.dirname(OpenColorIOConfigs.__file__), + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin", + "ocioconfig" + "OpenColorIOConfigs", profile_folder, "config.ocio" ) diff --git a/poetry.lock b/poetry.lock index df8d8ab14a..21b6bda880 100644 --- a/poetry.lock +++ b/poetry.lock @@ -797,21 +797,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "opencolorio-configs" -version = "1.0.2" -description = "Curated set of OpenColorIO Configs for use in OpenPype" -category = "main" -optional = false -python-versions = "*" -develop = false - -[package.source] -type = "git" -url = "https://github.com/pypeclub/OpenColorIO-Configs.git" -reference = "main" -resolved_reference = "07c5e865bf2b115b589dd2876ae632cd410821b5" - [[package]] name = "opentimelineio" version = "0.14.0.dev1" @@ -1284,7 +1269,7 @@ python-versions = "*" [[package]] name = "pytz" -version = "2022.1" +version = "2022.2" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -1750,7 +1735,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "89fb7e8ad310b5048bf78561f1146194c8779e286d839cc000f04e88be87f3f3" +content-hash = "de7422afb6aed02f75e1696afdda9ad6c7bf32da76b5022ee3e8f71a1ac4bae2" [metadata.files] acre = [] @@ -2146,7 +2131,6 @@ multidict = [ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] -opencolorio-configs = [] opentimelineio = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -2398,10 +2382,7 @@ python-xlib = [ python3-xlib = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, ] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] +pytz = [] pywin32 = [ {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, diff --git a/pyproject.toml b/pyproject.toml index 1d757deaa0..b7b3fb967f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,8 +70,6 @@ requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" aiohttp-middlewares = "^2.0.0" -OpenColorIO-Configs = { git = "https://github.com/pypeclub/OpenColorIO-Configs.git", branch = "main" } - [tool.poetry.dev-dependencies] flake8 = "^3.7" @@ -144,6 +142,10 @@ hash = "3894dec7e4e521463891a869586850e8605f5fd604858b674c87323bf33e273d" url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz" hash = "sha256:..." +[openpype.thirdparty.ocioconfig] +url = "https://distribute.openpype.io/thirdparty/OpenColorIO-Configs-1.0.2.zip" +hash = "4ac17c1f7de83465e6f51dd352d7117e07e765b66d00443257916c828e35b6ce" + [tool.pyright] include = [ "igniter", diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index b616beab27..421cc32dbd 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -109,13 +109,20 @@ except AttributeError: for k, v in thirdparty.items(): _print(f"processing {k}") - destination_path = openpype_root / "vendor" / "bin" / k / platform_name - url = v.get(platform_name).get("url") + destination_path = openpype_root / "vendor" / "bin" / k + if not v.get(platform_name): _print(("missing definition for current " - f"platform [ {platform_name} ]"), 1) - sys.exit(1) + f"platform [ {platform_name} ]"), 2) + _print("trying to get universal url for all platforms") + url = v.get("url") + if not url: + _print("cannot get url", 1) + sys.exit(1) + else: + url = v.get(platform_name).get("url") + destination_path = destination_path / platform_name parsed_url = urlparse(url) @@ -147,7 +154,13 @@ for k, v in thirdparty.items(): # get file with checksum _print("Calculating sha256 ...", 2) calc_checksum = sha256_sum(temp_file) - if v.get(platform_name).get("hash") != calc_checksum: + + if v.get(platform_name): + item_hash = v.get(platform_name).get("hash") + else: + item_hash = v.get("hash") + + if item_hash != calc_checksum: _print("Downloaded files checksum invalid.") sys.exit(1) From 5a2e8f6d8f814bd0d7f6707580edd08e0098fec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 12 Aug 2022 18:44:16 +0200 Subject: [PATCH 0761/1030] :bug: remove import --- .../maya/plugins/publish/extract_look.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b416669b87..cece8ee22b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -40,19 +40,17 @@ def get_ocio_config_path(profile_folder): Returns: str: Path to vendorized config file. """ - try: - import OpenColorIOConfigs - return os.path.join( - os.environ["OPENPYPE_ROOT"], - "vendor", - "bin", - "ocioconfig" - "OpenColorIOConfigs", - profile_folder, - "config.ocio" - ) - except ImportError: - return None + + return os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin", + "ocioconfig" + "OpenColorIOConfigs", + profile_folder, + "config.ocio" + ) + def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. From 1ad9728962b92e55fa4d16601a7a48add381a456 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:32:13 +0200 Subject: [PATCH 0762/1030] :recycle: remove forgotten args, fix typos --- igniter/bootstrap_repos.py | 35 +++++++++++++++-------------------- start.py | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 3dab67ebf1..56ec2749ca 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -381,7 +381,7 @@ class OpenPypeVersion(semver.VersionInfo): @classmethod def get_local_versions( cls, production: bool = None, - staging: bool = None, compatible_with: OpenPypeVersion = None + staging: bool = None ) -> List: """Get all versions available on this machine. @@ -391,8 +391,10 @@ 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. + + Returns: + list: of compatible versions available on the machine. + """ # Return all local versions if arguments are set to None if production is None and staging is None: @@ -435,8 +437,7 @@ 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: @@ -745,9 +746,9 @@ class BootstrapRepos: self, repo_dir: Path = None) -> Union[OpenPypeVersion, None]: """Copy zip created from OpenPype repositories to user data dir. - This detect OpenPype version either in local "live" OpenPype + This detects OpenPype version either in local "live" OpenPype repository or in user provided path. Then it will zip it in temporary - directory and finally it will move it to destination which is user + directory, and finally it will move it to destination which is user data directory. Existing files will be replaced. Args: @@ -758,7 +759,7 @@ class BootstrapRepos: """ # if repo dir is not set, we detect local "live" OpenPype repository - # version and use it as a source. Otherwise repo_dir is user + # version and use it as a source. Otherwise, repo_dir is user # entered location. if repo_dir: version = self.get_version(repo_dir) @@ -1122,21 +1123,19 @@ class BootstrapRepos: @staticmethod def find_openpype_version( version: Union[str, OpenPypeVersion], - staging: bool, - compatible_with: OpenPypeVersion = None + staging: bool ) -> 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. + + Returns: + requested OpenPypeVersion. """ installed_version = OpenPypeVersion.get_installed_version() - if not compatible_with: - compatible_with = installed_version if isinstance(version, str): version = OpenPypeVersion(version=version) @@ -1144,8 +1143,7 @@ class BootstrapRepos: return installed_version local_versions = OpenPypeVersion.get_local_versions( - staging=staging, production=not staging, - compatible_with=compatible_with + staging=staging, production=not staging ) zip_version = None for local_version in local_versions: @@ -1159,8 +1157,7 @@ class BootstrapRepos: return zip_version remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, production=not staging, - compatible_with=compatible_with + staging=staging, production=not staging ) for remote_version in remote_versions: if remote_version == version: @@ -1237,8 +1234,6 @@ 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. diff --git a/start.py b/start.py index 52e98bb6e1..bfbcc77bc9 100644 --- a/start.py +++ b/start.py @@ -689,7 +689,7 @@ def _find_frozen_openpype(use_version: str = None, # Collect OpenPype versions installed_version = OpenPypeVersion.get_installed_version() # Expected version that should be used by studio settings - # - this option is used only if version is not explictly set and if + # - this option is used only if version is not explicitly set and if # studio has set explicit version in settings studio_version = OpenPypeVersion.get_expected_studio_version(use_staging) From b61e47a15d4ea7f843aa5a17963f8f4d0d73c77f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:45:26 +0200 Subject: [PATCH 0763/1030] :recycle: don't look for compatible version automatically --- igniter/bootstrap_repos.py | 12 +----------- start.py | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 56ec2749ca..dfcca2cf33 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1166,16 +1166,12 @@ class BootstrapRepos: @staticmethod def find_latest_openpype_version( - staging: bool, - compatible_with: OpenPypeVersion = None + staging: bool ) -> Union[OpenPypeVersion, None]: """Find the latest available OpenPype version in all location. Args: staging (bool): True to look for staging versions. - compatible_with (OpenPypeVersion, optional): If set, it will - try to find the latest version compatible with the - one specified. Returns: Latest OpenPype version on None if nothing was found. @@ -1195,12 +1191,6 @@ class BootstrapRepos: if not all_versions: return None - if compatible_with: - all_versions = [ - version for version in all_versions - if version.is_compatible(installed_version) - ] - all_versions.sort() latest_version = all_versions[-1] if latest_version == installed_version: diff --git a/start.py b/start.py index bfbcc77bc9..9837252a1f 100644 --- a/start.py +++ b/start.py @@ -729,7 +729,7 @@ def _find_frozen_openpype(use_version: str = None, ">>> Finding latest version compatible " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( - use_staging, compatible_with=installed_version) + use_staging) if openpype_version is None: if use_staging: From aa0fe93a504a3a513239c541e698a99600de9736 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Aug 2022 19:50:07 +0200 Subject: [PATCH 0764/1030] :bug: fix version list --- start.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 9837252a1f..084eb7451a 100644 --- a/start.py +++ b/start.py @@ -726,7 +726,7 @@ def _find_frozen_openpype(use_version: str = None, else: # Default behavior to use latest version _print(( - ">>> Finding latest version compatible " + ">>> Finding latest version " f"with [ {installed_version} ]")) openpype_version = bootstrap.find_latest_openpype_version( use_staging) @@ -947,7 +947,12 @@ def _boot_print_versions(use_staging, local_version, openpype_root): openpype_versions = bootstrap.find_openpype( include_zips=True, staging=use_staging, - compatible_with=compatible_with) + ) + openpype_versions = [ + version for version in openpype_versions + if version.is_compatible( + OpenPypeVersion.get_installed_version()) + ] list_versions(openpype_versions, local_version) From fd56f09c8423ea6438d6606c69dfa6c45ba9e8eb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 13 Aug 2022 03:48:01 +0000 Subject: [PATCH 0765/1030] [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 ae491af33b234f6ef7130c7f52f8a9e67cd032a4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:58:36 +0300 Subject: [PATCH 0766/1030] Adjust schema to include all lights flag. --- openpype/settings/defaults/project_settings/maya.json | 5 +++-- .../projects_schema/schemas/schema_maya_render_settings.json | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ac0f161cf2..c95d47d576 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -33,7 +33,8 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "", + "default_render_image_folder": "renders", + "enable_all_lights": false, "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { @@ -976,4 +977,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 af197604f8..6ee02ca78f 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 @@ -14,6 +14,11 @@ "key": "default_render_image_folder", "label": "Default render image folder" }, + { + "type": "boolean", + "key": "enable_all_lights", + "label": "Include all lights in Render Setup Layers by default" + }, { "key": "aov_separator", "label": "AOV Separator character", From bbe7bc2fdb533375d9acc48a8c6b2f5c1538ecc1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:20 +0300 Subject: [PATCH 0767/1030] Include `RenderSetupIncludeLights` flag in plugin info, grab value from render instance. --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index f253ceb21a..7966861358 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -62,6 +62,7 @@ payload_skeleton_template = { "RenderLayer": None, # Render only this layer "Renderer": None, "ProjectPath": None, # Resolve relative references + "RenderSetupIncludeLights": None, # Include all lights flag. }, "AuxFiles": [] # Mandatory for Deadline, may be empty } @@ -413,8 +414,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", "") @@ -505,6 +505,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.payload_skeleton["JobInfo"]["Comment"] = comment self.payload_skeleton["PluginInfo"]["RenderLayer"] = renderlayer + self.payload_skeleton["PluginInfo"]["RenderSetupIncludeLights"] = instance.data.get("renderSetupIncludeLights") # noqa # Adding file dependencies. dependencies = instance.context.data["fileDependencies"] dependencies.append(filepath) From d7aba60460ce19af1c9a4c2bb629c967f8d06750 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:36 +0300 Subject: [PATCH 0768/1030] Validate lights flag --- .../hosts/maya/plugins/publish/validate_rendersettings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 1dab3274a0..93ef7d7af7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -242,6 +242,14 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): instance.context.data["project_settings"]["maya"]["publish"]["ValidateRenderSettings"].get( # noqa: E501 "{}_render_attributes".format(renderer)) or [] ) + settings_lights_flag = instance.context.data["project_settings"].get( + "maya", {}).get( + "RenderSettings", {}).get( + "enable_all_lights", {}) + + instance_lights_flag = instance.data.get("renderSetupIncludeLights") + if settings_lights_flag != instance_lights_flag: + cls.log.warning('Instance flag for "Render Setup Include Lights" is set to {0} and Settings flag is set to {1}'.format(instance_lights_flag, settings_lights_flag)) # noqa # go through definitions and test if such node.attribute exists. # if so, compare its value from the one required. From 5322527226344498aec2b830847b42a60e91eca8 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 01:59:56 +0300 Subject: [PATCH 0769/1030] add flag attribute to render creator --- openpype/hosts/maya/plugins/create/create_render.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fbe670b1ea..2f09aaee87 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -71,7 +71,7 @@ class CreateRender(plugin.Creator): label = "Render" family = "rendering" icon = "eye" - + enable_all_lights = True _token = None _user = None _password = None @@ -220,6 +220,12 @@ class CreateRender(plugin.Creator): self.data["tilesY"] = 2 self.data["convertToScanline"] = False self.data["useReferencedAovs"] = False + self.data["renderSetupIncludeLights"] = ( + self._project_settings.get( + "maya", {}).get( + "RenderSettings", {}).get( + "enable_all_lights", {}) + ) # Disable for now as this feature is not working yet # self.data["assScene"] = False From 5146c5a7e7f83032ba5d512a6861fb8f9b1b47f1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:00:15 +0300 Subject: [PATCH 0770/1030] Add flag to collector, fix settings path bug --- openpype/hosts/maya/api/lib_rendersettings.py | 3 +- .../maya/plugins/publish/collect_render.py | 8 +++-- .../publish/validate_render_image_rule.py | 31 +++++++++++-------- 3 files changed, 24 insertions(+), 18 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/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e132cffe53..7035da2ec7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -202,8 +202,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. @@ -318,7 +317,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "useReferencedAovs": render_instance.data.get( "useReferencedAovs") or render_instance.data.get( "vrayUseReferencedAovs") or False, - "aovSeparator": layer_render_products.layer_data.aov_separator # noqa: E501 + "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 + "renderSetupIncludeLights": render_instance.data.get( + "renderSetupIncludeLights" + ) } # Collect Deadline url if Deadline module is enabled 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..353d0ad63a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,6 +1,6 @@ import maya.mel as mel -import pymel.core as pm +from maya import cmds import pyblish.api import openpype.api @@ -11,8 +11,10 @@ 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. """ order = openpype.api.ValidateContentsOrder @@ -22,25 +24,28 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): actions = [openpype.api.RepairAction] def process(self, instance): + required_images_rule = self.get_default_render_image_folder(instance) + current_images_rule = cmds.workspace(fileRuleEntry="images") - default_render_file = self.get_default_render_image_folder(instance) - - 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 ) ) @classmethod def repair(cls, instance): - default = cls.get_default_render_image_folder(instance) - pm.workspace.fileRules["images"] = default - pm.system.Workspace.save() + 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): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('create') \ - .get('CreateRender') \ + .get('RenderSettings') \ .get('default_render_image_folder') From 8fcf5ffa28ae615219d2d3a31b0419bec3a746b6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:19:02 +0300 Subject: [PATCH 0771/1030] Revert "Add flag to collector, fix settings path bug" This reverts part of commit 5146c5a7e7f83032ba5d512a6861fb8f9b1b47f1. --- .../publish/validate_render_image_rule.py | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 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 353d0ad63a..642ca9e25d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,6 +1,6 @@ import maya.mel as mel +import pymel.core as pm -from maya import cmds import pyblish.api import openpype.api @@ -11,10 +11,8 @@ def get_file_rule(rule): class ValidateRenderImageRule(pyblish.api.InstancePlugin): - """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. + """Validates "images" file rule is set to "renders/" + """ order = openpype.api.ValidateContentsOrder @@ -24,28 +22,25 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): actions = [openpype.api.RepairAction] def process(self, instance): - required_images_rule = self.get_default_render_image_folder(instance) - current_images_rule = cmds.workspace(fileRuleEntry="images") - assert current_images_rule == required_images_rule, ( - "Invalid workspace `images` file rule value: '{}'. " - "Must be set to: '{}'".format( - current_images_rule, required_images_rule + default_render_file = self.get_default_render_image_folder(instance) + + assert get_file_rule("images") == default_render_file, ( + "Workspace's `images` file rule must be set to: {}".format( + default_render_file ) ) @classmethod def repair(cls, instance): - 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) + default = cls.get_default_render_image_folder(instance) + pm.workspace.fileRules["images"] = default + pm.system.Workspace.save() @staticmethod def get_default_render_image_folder(instance): return instance.context.data.get('project_settings')\ .get('maya') \ - .get('RenderSettings') \ + .get('create') \ + .get('CreateRender') \ .get('default_render_image_folder') From d2f9c100c35edbe9cafd59db6e732c9ba058e309 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 02:19:42 +0300 Subject: [PATCH 0772/1030] Fix correct path bug --- .../hosts/maya/plugins/publish/validate_render_image_rule.py | 3 +-- 1 file changed, 1 insertion(+), 2 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 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 dd2becdb7964bf43d71368c89f7a9fdae48ce4a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 Aug 2022 11:55:16 +0200 Subject: [PATCH 0773/1030] nuke: collect workfile adding KnownPublishErrorl when untitled --- openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 7349a8f424..822f405a6f 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -8,6 +8,7 @@ from openpype.hosts.nuke.api.lib import ( add_publish_knob, get_avalon_knob_data ) +from openpype.pipeline import KnownPublishError class CollectWorkfile(pyblish.api.ContextPlugin): @@ -22,6 +23,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): current_file = os.path.normpath(nuke.root().name()) + if current_file.lower() == "root": + raise KnownPublishError( + "Workfile is not correct file name. \n" + "Use workfile tool to manage the name correctly." + ) + knob_data = get_avalon_knob_data(root) add_publish_knob(root) From 4bd375409e11822978d352cae20fd1081615aa55 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 Aug 2022 11:55:37 +0200 Subject: [PATCH 0774/1030] nuke: fixing validate rendered frames --- 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 f8e128cd26..237ff423e5 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -127,7 +127,7 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): f_start_h += 1 if ( - collected_frames_len >= frame_length + collected_frames_len != frame_length and coll_start <= f_start_h and coll_end >= f_end_h ): From e6584a9b940782bb6927e807b6a19412a1fd2fe4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 12:08:20 +0200 Subject: [PATCH 0775/1030] 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 0776/1030] 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 0777/1030] 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 553fcdff538178019d76d73ccb0b83119a816ef4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 13:22:25 +0200 Subject: [PATCH 0778/1030] added python 2 compatible attrs to vendor --- .../vendor/python/python_2/attr/__init__.py | 80 + .../vendor/python/python_2/attr/__init__.pyi | 484 +++ openpype/vendor/python/python_2/attr/_cmp.py | 154 + openpype/vendor/python/python_2/attr/_cmp.pyi | 13 + .../vendor/python/python_2/attr/_compat.py | 261 ++ .../vendor/python/python_2/attr/_config.py | 33 + .../vendor/python/python_2/attr/_funcs.py | 422 +++ openpype/vendor/python/python_2/attr/_make.py | 3173 +++++++++++++++++ .../vendor/python/python_2/attr/_next_gen.py | 216 ++ .../python/python_2/attr/_version_info.py | 87 + .../python/python_2/attr/_version_info.pyi | 9 + .../vendor/python/python_2/attr/converters.py | 155 + .../python/python_2/attr/converters.pyi | 13 + .../vendor/python/python_2/attr/exceptions.py | 94 + .../python/python_2/attr/exceptions.pyi | 17 + .../vendor/python/python_2/attr/filters.py | 54 + .../vendor/python/python_2/attr/filters.pyi | 6 + openpype/vendor/python/python_2/attr/py.typed | 0 .../vendor/python/python_2/attr/setters.py | 79 + .../vendor/python/python_2/attr/setters.pyi | 19 + .../vendor/python/python_2/attr/validators.py | 561 +++ .../python/python_2/attr/validators.pyi | 78 + .../vendor/python/python_2/attrs/__init__.py | 70 + .../vendor/python/python_2/attrs/__init__.pyi | 63 + .../python/python_2/attrs/converters.py | 3 + .../python/python_2/attrs/exceptions.py | 3 + .../vendor/python/python_2/attrs/filters.py | 3 + .../vendor/python/python_2/attrs/py.typed | 0 .../vendor/python/python_2/attrs/setters.py | 3 + .../python/python_2/attrs/validators.py | 3 + 30 files changed, 6156 insertions(+) create mode 100644 openpype/vendor/python/python_2/attr/__init__.py create mode 100644 openpype/vendor/python/python_2/attr/__init__.pyi create mode 100644 openpype/vendor/python/python_2/attr/_cmp.py create mode 100644 openpype/vendor/python/python_2/attr/_cmp.pyi create mode 100644 openpype/vendor/python/python_2/attr/_compat.py create mode 100644 openpype/vendor/python/python_2/attr/_config.py create mode 100644 openpype/vendor/python/python_2/attr/_funcs.py create mode 100644 openpype/vendor/python/python_2/attr/_make.py create mode 100644 openpype/vendor/python/python_2/attr/_next_gen.py create mode 100644 openpype/vendor/python/python_2/attr/_version_info.py create mode 100644 openpype/vendor/python/python_2/attr/_version_info.pyi create mode 100644 openpype/vendor/python/python_2/attr/converters.py create mode 100644 openpype/vendor/python/python_2/attr/converters.pyi create mode 100644 openpype/vendor/python/python_2/attr/exceptions.py create mode 100644 openpype/vendor/python/python_2/attr/exceptions.pyi create mode 100644 openpype/vendor/python/python_2/attr/filters.py create mode 100644 openpype/vendor/python/python_2/attr/filters.pyi create mode 100644 openpype/vendor/python/python_2/attr/py.typed create mode 100644 openpype/vendor/python/python_2/attr/setters.py create mode 100644 openpype/vendor/python/python_2/attr/setters.pyi create mode 100644 openpype/vendor/python/python_2/attr/validators.py create mode 100644 openpype/vendor/python/python_2/attr/validators.pyi create mode 100644 openpype/vendor/python/python_2/attrs/__init__.py create mode 100644 openpype/vendor/python/python_2/attrs/__init__.pyi create mode 100644 openpype/vendor/python/python_2/attrs/converters.py create mode 100644 openpype/vendor/python/python_2/attrs/exceptions.py create mode 100644 openpype/vendor/python/python_2/attrs/filters.py create mode 100644 openpype/vendor/python/python_2/attrs/py.typed create mode 100644 openpype/vendor/python/python_2/attrs/setters.py create mode 100644 openpype/vendor/python/python_2/attrs/validators.py diff --git a/openpype/vendor/python/python_2/attr/__init__.py b/openpype/vendor/python/python_2/attr/__init__.py new file mode 100644 index 0000000000..f95c96dd57 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/__init__.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import sys + +from functools import partial + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Factory, + attrib, + attrs, + fields, + fields_dict, + make_class, + validate, +) +from ._version_info import VersionInfo + + +__version__ = "21.4.0" +__version_info__ = VersionInfo._from_version_string(__version__) + +__title__ = "attrs" +__description__ = "Classes Without Boilerplate" +__url__ = "https://www.attrs.org/" +__uri__ = __url__ +__doc__ = __description__ + " <" + __uri__ + ">" + +__author__ = "Hynek Schlawack" +__email__ = "hs@ox.cx" + +__license__ = "MIT" +__copyright__ = "Copyright (c) 2015 Hynek Schlawack" + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +__all__ = [ + "Attribute", + "Factory", + "NOTHING", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "evolve", + "exceptions", + "fields", + "fields_dict", + "filters", + "get_run_validators", + "has", + "ib", + "make_class", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + +if sys.version_info[:2] >= (3, 6): + from ._next_gen import define, field, frozen, mutable # noqa: F401 + + __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/openpype/vendor/python/python_2/attr/__init__.pyi b/openpype/vendor/python/python_2/attr/__init__.pyi new file mode 100644 index 0000000000..c0a2126503 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/__init__.pyi @@ -0,0 +1,484 @@ +import sys + +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._version_info import VersionInfo + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = Union[bool, Callable[[Any], Any]] +_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ConverterType = Callable[[Any], Any] +_FilterType = Callable[[Attribute[_T], _T], bool] +_ReprType = Callable[[Any], str] +_ReprArgType = Union[bool, _ReprType] +_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrArgType = Union[ + _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType +] +_FieldTransformer = Callable[ + [type, List[Attribute[Any]]], List[Attribute[Any]] +] +_CompareWithType = Callable[[Any, Any], bool] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] + +# _make -- + +NOTHING: object + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. +if sys.version_info >= (3, 8): + from typing import Literal + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], + ) -> _T: ... + @overload + def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], + ) -> _T: ... + +else: + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Union[Callable[[Any], _T], Callable[[], _T]], + takes_self: bool = ..., + ) -> _T: ... + +# Static type inference support via __dataclass_transform__ implemented as per: +# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md +# This annotation must be applied to all overloads of "define" and "attrs" +# +# NOTE: This is a typing construct and does not exist at runtime. Extensions +# wrapping attrs decorators should declare a separate __dataclass_transform__ +# signature in the extension module using the specification linked above to +# provide pyright support. +def __dataclass_transform__( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), +) -> Callable[[_T], _T]: ... + +class Attribute(Generic[_T]): + name: str + default: Optional[_T] + validator: Optional[_ValidatorType[_T]] + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: Optional[bool] + init: bool + converter: Optional[_ConverterType] + metadata: Dict[Any, Any] + type: Optional[Type[_T]] + kw_only: bool + on_setattr: _OnSetAttrType + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: object = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +mutable = define +frozen = define # they differ only in their defaults + +# TODO: add support for returning NamedTuple from the mypy plugin +class _Fields(Tuple[Attribute[Any], ...]): + def __getattr__(self, name: str) -> Attribute[Any]: ... + +def fields(cls: type) -> _Fields: ... +def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... +def validate(inst: Any) -> None: ... +def resolve_types( + cls: _C, + globalns: Optional[Dict[str, Any]] = ..., + localns: Optional[Dict[str, Any]] = ..., + attribs: Optional[List[Attribute[Any]]] = ..., +) -> _C: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + collect_by_mro: bool = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: Optional[bool] = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... +def has(cls: type) -> bool: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/openpype/vendor/python/python_2/attr/_cmp.py b/openpype/vendor/python/python_2/attr/_cmp.py new file mode 100644 index 0000000000..6cffa4dbab --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_cmp.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import functools + +from ._compat import new_class +from ._make import _make_ne + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and + ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if + at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + :param Optional[callable] eq: `callable` used to evaluate equality + of two objects. + :param Optional[callable] lt: `callable` used to evaluate whether + one object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether + one object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether + one object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether + one object is greater than or equal to another object. + + :param bool require_same_type: When `True`, equality and ordering methods + will return `NotImplemented` if objects are not of the same type. + + :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = _make_ne() + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + raise ValueError( + "eq must be define is order to complete ordering from " + "lt, le, gt, ge." + ) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = "__%s__" % (name,) + method.__doc__ = "Return a %s b. Computed by attrs." % ( + _operation_names[name], + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + for func in self._requirements: + if not func(self, other): + return False + return True + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/openpype/vendor/python/python_2/attr/_cmp.pyi b/openpype/vendor/python/python_2/attr/_cmp.pyi new file mode 100644 index 0000000000..e71aaff7a1 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Type + +from . import _CompareWithType + +def cmp_using( + eq: Optional[_CompareWithType], + lt: Optional[_CompareWithType], + le: Optional[_CompareWithType], + gt: Optional[_CompareWithType], + ge: Optional[_CompareWithType], + require_same_type: bool, + class_name: str, +) -> Type: ... diff --git a/openpype/vendor/python/python_2/attr/_compat.py b/openpype/vendor/python/python_2/attr/_compat.py new file mode 100644 index 0000000000..dc0cb02b64 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_compat.py @@ -0,0 +1,261 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import platform +import sys +import threading +import types +import warnings + + +PY2 = sys.version_info[0] == 2 +PYPY = platform.python_implementation() == "PyPy" +PY36 = sys.version_info[:2] >= (3, 6) +HAS_F_STRINGS = PY36 +PY310 = sys.version_info[:2] >= (3, 10) + + +if PYPY or PY36: + ordered_dict = dict +else: + from collections import OrderedDict + + ordered_dict = OrderedDict + + +if PY2: + from collections import Mapping, Sequence + + from UserDict import IterableUserDict + + # We 'bundle' isclass instead of using inspect as importing inspect is + # fairly expensive (order of 10-15 ms for a modern machine in 2016) + def isclass(klass): + return isinstance(klass, (type, types.ClassType)) + + def new_class(name, bases, kwds, exec_body): + """ + A minimal stub of types.new_class that we need for make_class. + """ + ns = {} + exec_body(ns) + + return type(name, bases, ns) + + # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. + TYPE = "type" + + def iteritems(d): + return d.iteritems() + + # Python 2 is bereft of a read-only dict proxy, so we make one! + class ReadOnlyDict(IterableUserDict): + """ + Best-effort read-only dict wrapper. + """ + + def __setitem__(self, key, val): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item assignment" + ) + + def update(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'update'" + ) + + def __delitem__(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item deletion" + ) + + def clear(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'clear'" + ) + + def pop(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'pop'" + ) + + def popitem(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'popitem'" + ) + + def setdefault(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'setdefault'" + ) + + def __repr__(self): + # Override to be identical to the Python 3 version. + return "mappingproxy(" + repr(self.data) + ")" + + def metadata_proxy(d): + res = ReadOnlyDict() + res.data.update(d) # We blocked update, so we have to do it like this. + return res + + def just_warn(*args, **kw): # pragma: no cover + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + +else: # Python 3 and later. + from collections.abc import Mapping, Sequence # noqa + + def just_warn(*args, **kw): + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + warnings.warn( + "Running interpreter doesn't sufficiently support code object " + "introspection. Some features like bare super() or accessing " + "__class__ will not work with slotted classes.", + RuntimeWarning, + stacklevel=2, + ) + + def isclass(klass): + return isinstance(klass, type) + + TYPE = "class" + + def iteritems(d): + return d.items() + + new_class = types.new_class + + def metadata_proxy(d): + return types.MappingProxyType(dict(d)) + + +def make_set_closure_cell(): + """Return a function of two arguments (cell, value) which sets + the value stored in the closure cell `cell` to `value`. + """ + # pypy makes this easy. (It also supports the logic below, but + # why not do the easy/fast thing?) + if PYPY: + + def set_closure_cell(cell, value): + cell.__setstate__((value,)) + + return set_closure_cell + + # Otherwise gotta do it the hard way. + + # Create a function that will set its first cellvar to `value`. + def set_first_cellvar_to(value): + x = value + return + + # This function will be eliminated as dead code, but + # not before its reference to `x` forces `x` to be + # represented as a closure cell rather than a local. + def force_x_to_be_a_cell(): # pragma: no cover + return x + + try: + # Extract the code object and make sure our assumptions about + # the closure behavior are correct. + if PY2: + co = set_first_cellvar_to.func_code + else: + co = set_first_cellvar_to.__code__ + if co.co_cellvars != ("x",) or co.co_freevars != (): + raise AssertionError # pragma: no cover + + # Convert this code object to a code object that sets the + # function's first _freevar_ (not cellvar) to the argument. + if sys.version_info >= (3, 8): + # CPython 3.8+ has an incompatible CodeType signature + # (added a posonlyargcount argument) but also added + # CodeType.replace() to do this without counting parameters. + set_first_freevar_code = co.replace( + co_cellvars=co.co_freevars, co_freevars=co.co_cellvars + ) + else: + args = [co.co_argcount] + if not PY2: + args.append(co.co_kwonlyargcount) + args.extend( + [ + co.co_nlocals, + co.co_stacksize, + co.co_flags, + co.co_code, + co.co_consts, + co.co_names, + co.co_varnames, + co.co_filename, + co.co_name, + co.co_firstlineno, + co.co_lnotab, + # These two arguments are reversed: + co.co_cellvars, + co.co_freevars, + ] + ) + set_first_freevar_code = types.CodeType(*args) + + def set_closure_cell(cell, value): + # Create a function using the set_first_freevar_code, + # whose first closure cell is `cell`. Calling it will + # change the value of that cell. + setter = types.FunctionType( + set_first_freevar_code, {}, "setter", (), (cell,) + ) + # And call it to set the cell. + setter(value) + + # Make sure it works on this interpreter: + def make_func_with_cell(): + x = None + + def func(): + return x # pragma: no cover + + return func + + if PY2: + cell = make_func_with_cell().func_closure[0] + else: + cell = make_func_with_cell().__closure__[0] + set_closure_cell(cell, 100) + if cell.cell_contents != 100: + raise AssertionError # pragma: no cover + + except Exception: + return just_warn + else: + return set_closure_cell + + +set_closure_cell = make_set_closure_cell() + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() diff --git a/openpype/vendor/python/python_2/attr/_config.py b/openpype/vendor/python/python_2/attr/_config.py new file mode 100644 index 0000000000..fc9be29d00 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_config.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + + +__all__ = ["set_run_validators", "get_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + raise TypeError("'run' must be bool.") + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/openpype/vendor/python/python_2/attr/_funcs.py b/openpype/vendor/python/python_2/attr/_funcs.py new file mode 100644 index 0000000000..4c90085a40 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_funcs.py @@ -0,0 +1,422 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import copy + +from ._compat import iteritems +from ._make import NOTHING, _obj_setattr, fields +from .exceptions import AttrsAttributeNotFoundError + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the ``attrs`` attribute values of *inst* as a dict. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attrs.Attribute` as the first argument and the + value as the second argument. + :param callable dict_factory: A callable to produce dictionaries from. For + example, to produce ordered dictionaries instead of normal Python + dictionaries, pass in ``collections.OrderedDict``. + :param bool retain_collection_types: Do not convert to ``list`` when + encountering an attribute whose type is ``tuple`` or ``set``. Only + meaningful if ``recurse`` is ``True``. + :param Optional[callable] value_serializer: A hook that is called for every + attribute or dict key/value. It receives the current instance, field + and value and must return the (updated) value. The hook is run *after* + the optional *filter* has been applied. + + :rtype: return type of *dict_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 If a dict has a collection for a key, it is + serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + if has(v.__class__): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain_collection_types is True else list + rv[a.name] = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + ) + elif isinstance(v, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in iteritems(v) + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + if getattr(val.__class__, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(val, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif isinstance(val, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in iteritems(val) + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the ``attrs`` attribute values of *inst* as a tuple. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attrs.Attribute` as the first argument and the + value as the second argument. + :param callable tuple_factory: A callable to produce tuples from. For + example, to produce lists instead of tuples. + :param bool retain_collection_types: Do not convert to ``list`` + or ``dict`` when encountering an attribute which type is + ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is + ``True``. + + :rtype: return type of *tuple_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + rv.append( + cf( + [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + for j in v + ] + ) + ) + elif isinstance(v, dict): + df = v.__class__ if retain is True else dict + rv.append( + df( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk, + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv, + ) + for kk, vv in iteritems(v) + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with ``attrs`` attributes. + + :param type cls: Class to introspect. + :raise TypeError: If *cls* is not a class. + + :rtype: bool + """ + return getattr(cls, "__attrs_attrs__", None) is not None + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't + be found on *cls*. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. + This function will not be removed du to the slightly different approach + compared to `attrs.evolve`. + """ + import warnings + + warnings.warn( + "assoc is deprecated and will be removed after 2018/01.", + DeprecationWarning, + stacklevel=2, + ) + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in iteritems(changes): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + raise AttrsAttributeNotFoundError( + "{k} is not an attrs attribute on {cl}.".format( + k=k, cl=new.__class__ + ) + ) + _obj_setattr(new, k, v) + return new + + +def evolve(inst, **changes): + """ + Create a new instance, based on *inst* with *changes* applied. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise TypeError: If *attr_name* couldn't be found in the class + ``__init__``. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 17.1.0 + """ + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +def resolve_types(cls, globalns=None, localns=None, attribs=None): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in `Attribute`'s *type* + field. In other words, you don't need to resolve your types if you only + use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, e.g. if the name only exists + inside a method, you may pass *globalns* or *localns* to specify other + dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + :param type cls: Class to resolve. + :param Optional[dict] globalns: Dictionary containing global variables. + :param Optional[dict] localns: Dictionary containing local variables. + :param Optional[list] attribs: List of attribs for the given class. + This is necessary when calling from inside a ``field_transformer`` + since *cls* is not an ``attrs`` class yet. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class and you didn't pass any attribs. + :raise NameError: If types cannot be resolved because of missing variables. + + :returns: *cls* so you can use this function also as a class decorator. + Please note that you have to apply it **after** `attrs.define`. That + means the decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _obj_setattr(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/openpype/vendor/python/python_2/attr/_make.py b/openpype/vendor/python/python_2/attr/_make.py new file mode 100644 index 0000000000..d46f8a3e7a --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_make.py @@ -0,0 +1,3173 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +import copy +import inspect +import linecache +import sys +import warnings + +from operator import itemgetter + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + HAS_F_STRINGS, + PY2, + PY310, + PYPY, + isclass, + iteritems, + metadata_proxy, + new_class, + ordered_dict, + set_closure_cell, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + PythonTooOldError, + UnannotatedAttributeError, +) + + +if not PY2: + import typing + + +# This is used at least twice, so cache it here. +_obj_setattr = object.__setattr__ +_init_converter_pat = "__attr_converter_%s" +_init_factory_pat = "__attr_factory_{}" +_tuple_property_pat = ( + " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" +) +_classvar_prefixes = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_hash_cache_field = "_attrs_cached_hash" + +_empty_metadata_singleton = metadata_proxy({}) + +# Unique object for unequivocal getattr() defaults. +_sentinel = object() + +_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(object): + """ + Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + + ``_Nothing`` is a singleton. There is only ever one of it. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + """ + + _singleton = None + + def __new__(cls): + if _Nothing._singleton is None: + _Nothing._singleton = super(_Nothing, cls).__new__(cls) + return _Nothing._singleton + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + def __len__(self): + return 0 # __bool__ for Python 2 + + +NOTHING = _Nothing() +""" +Sentinel to indicate the lack of a value when ``None`` is ambiguous. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since ``None`` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + if PY2: + # For some reason `type(None)` isn't callable in Python 2, but we don't + # actually need a constructor for None objects, we just need any + # available function that returns None. + def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): + return _none_constructor, _args + + else: + + def __reduce__(self, _none_constructor=type(None), _args=()): + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Create a new attribute on a class. + + .. warning:: + + Does *not* do anything unless the class is also decorated with + `attr.s`! + + :param default: A value that is used if an ``attrs``-generated ``__init__`` + is used and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will be + used to construct a new value (useful for mutable data types like lists + or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a value + *must* be supplied when instantiating; otherwise a `TypeError` + will be raised. + + The default can also be set using decorator notation as shown below. + + :type default: Any value + + :param callable factory: Syntactic sugar for + ``default=attr.Factory(factory)``. + + :param validator: `callable` that is called by ``attrs``-generated + ``__init__`` methods after the instance has been initialized. They + receive the initialized instance, the :func:`~attrs.Attribute`, and the + passed value. + + The return value is *not* inspected so the validator has to throw an + exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `get_run_validators`. + + The validator can also be set using decorator notation as shown below. + + :type validator: `callable` or a `list` of `callable`\\ s. + + :param repr: Include this attribute in the generated ``__repr__`` + method. If ``True``, include the attribute; if ``False``, omit it. By + default, the built-in ``repr()`` function is used. To override how the + attribute value is formatted, pass a ``callable`` that takes a single + value and returns a string. Note that the resulting string is used + as-is, i.e. it will be used directly *instead* of calling ``repr()`` + (the default). + :type repr: a `bool` or a `callable` to use a custom function. + + :param eq: If ``True`` (default), include this attribute in the + generated ``__eq__`` and ``__ne__`` methods that check two instances + for equality. To override how the attribute value is compared, + pass a ``callable`` that takes a single value and returns the value + to be compared. + :type eq: a `bool` or a `callable`. + + :param order: If ``True`` (default), include this attributes in the + generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. + To override how the attribute value is ordered, + pass a ``callable`` that takes a single value and returns the value + to be ordered. + :type order: a `bool` or a `callable`. + + :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the + same value. Must not be mixed with *eq* or *order*. + :type cmp: a `bool` or a `callable`. + + :param Optional[bool] hash: Include this attribute in the generated + ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This + is the correct behavior according the Python spec. Setting this value + to anything else than ``None`` is *discouraged*. + :param bool init: Include this attribute in the generated ``__init__`` + method. It is possible to set this to ``False`` and set a default + value. In that case this attributed is unconditionally initialized + with the specified default value or factory. + :param callable converter: `callable` that is called by + ``attrs``-generated ``__init__`` methods to convert attribute's value + to the desired format. It is given the passed-in value, and the + returned value will be used as the new value of the attribute. The + value is converted before being passed to the validator, if any. + :param metadata: An arbitrary mapping, to be used by third-party + components. See `extending_metadata`. + :param type: The type of the attribute. In Python 3.6 or greater, the + preferred method to specify the type is using a variable annotation + (see `PEP 526 `_). + This argument is provided for backward compatibility. + Regardless of the approach used, the type will be stored on + ``Attribute.type``. + + Please note that ``attrs`` doesn't do anything with this metadata by + itself. You can use it as part of your own code or for + `static type checking `. + :param kw_only: Make this attribute keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param on_setattr: Allows to overwrite the *on_setattr* setting from + `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. + Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `attr.s`. + :type on_setattr: `callable`, or a list of callables, or `None`, or + `attrs.setters.NO_OP` + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is ``None`` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated + *convert* to achieve consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + + if factory is not None: + if default is not NOTHING: + raise ValueError( + "The `default` and `factory` arguments are mutually " + "exclusive." + ) + if not callable(factory): + raise ValueError("The `factory` argument must be a callable.") + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + ) + + +def _compile_and_eval(script, globs, locs=None, filename=""): + """ + "Exec" the script with the given global (globs) and local (locs) variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _make_method(name, script, filename, globs=None): + """ + Create the method with the script given and return the method object. + """ + locs = {} + if globs is None: + globs = {} + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + else: + filename = "{}-{}>".format(base_filename[:-1], count) + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs[name] + + +def _make_attr_tuple_class(cls_name, attr_names): + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = "{}Attributes".format(cls_name) + attr_class_template = [ + "class {}(tuple):".format(attr_class_name), + " __slots__ = ()", + ] + if attr_names: + for i, attr_name in enumerate(attr_names): + attr_class_template.append( + _tuple_property_pat.format(index=i, attr_name=attr_name) + ) + else: + attr_class_template.append(" pass") + globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property} + _compile_and_eval("\n".join(attr_class_template), globs) + return globs[attr_class_name] + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +_Attributes = _make_attr_tuple_class( + "_Attributes", + [ + # all attributes to build dunder methods for + "attrs", + # attributes that have been inherited + "base_attrs", + # map inherited attributes to their originating classes + "base_attrs_map", + ], +) + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_classvar_prefixes) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + + Requires Python 3. + """ + attr = getattr(cls, attrib_name, _sentinel) + if attr is _sentinel: + return False + + for base_cls in cls.__mro__[1:]: + a = getattr(base_cls, attrib_name, None) + if attr is a: + return False + + return True + + +def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + if _has_own_attribute(cls, "__annotations__"): + return cls.__annotations__ + + return {} + + +def _counter_getter(e): + """ + Key function for sorting to avoid re-creating a lambda for every class. + """ + return e[1].counter + + +def _collect_base_attrs(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer +): + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + *collect_by_mro* is True, collect them in the correct MRO order, otherwise + use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = [(name, ca) for name, ca in iteritems(these)] + + if not isinstance(these, ordered_dict): + ca_list.sort(key=_counter_getter) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if not isinstance(a, _CountingAttr): + if a is NOTHING: + a = attrib() + else: + a = attrib(default=a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if len(unannotated) > 0: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + ), + key=lambda e: e[1].counter, + ) + + own_attrs = [ + Attribute.from_counting_attr( + name=attr_name, ca=ca, type=anns.get(attr_name) + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + raise ValueError( + "No mandatory attributes allowed after an attribute with a " + "default value or factory. Attribute in question: %r" % (a,) + ) + + if had_default is False and a.default is not NOTHING: + had_default = True + + if field_transformer is not None: + attrs = field_transformer(cls, attrs) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) + + +if PYPY: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError() + +else: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + raise FrozenInstanceError() + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + raise FrozenInstanceError() + + +class _ClassBuilder(object): + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_pre_init", + "_has_post_init", + "_is_exc", + "_on_setattr", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + "_has_custom_setattr", + ) + + def __init__( + self, + cls, + these, + slots, + frozen, + weakref_slot, + getstate_setstate, + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_custom_setattr, + field_transformer, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if slots else {} + self._attrs = attrs + self._base_names = set(a.name for a in base_attrs) + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = slots + self._frozen = frozen + self._weakref_slot = weakref_slot + self._cache_hash = cache_hash + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = is_exc + self._on_setattr = on_setattr + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + + if frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif on_setattr in ( + _ng_default_on_setattr, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + on_setattr == _ng_default_on_setattr + and not (has_validator or has_converter) + ) + or (on_setattr == setters.validate and not has_validator) + or (on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if getstate_setstate: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + def __repr__(self): + return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + else: + return self._patch_original_class() + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _sentinel) is not _sentinel + ): + try: + delattr(cls, name) + except AttributeError: + # This can happen if a base class defines a class + # variable and we want to set an attribute with the + # same name by using only a type annotation. + pass + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = object.__setattr__ + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in iteritems(self._cls_dict) + if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") + } + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = object.__setattr__ + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = dict() + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overriden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in iteritems(existing_slots) + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_hash_cache_field) + cd["__slots__"] = tuple(slot_names) + + qualname = getattr(self._cls, "__qualname__", None) + if qualname is not None: + cd["__qualname__"] = qualname + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . On Python 3, + # if a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in cls.__dict__.values(): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # ValueError: Cell is empty + pass + else: + if match: + set_closure_cell(cell, cls) + + return cls + + def add_repr(self, ns): + self._cls_dict["__repr__"] = self._add_method_dunders( + _make_repr(self._attrs, ns, self._cls) + ) + return self + + def add_str(self): + repr = self._cls_dict.get("__repr__") + if repr is None: + raise ValueError( + "__str__ can only be generated if a __repr__ exists." + ) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return tuple(getattr(self, name) for name in state_attr_names) + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_hash_cache_field, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + self._cls_dict["__hash__"] = self._add_method_dunders( + _make_hash( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + ) + + return self + + def add_init(self): + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + ) + + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + self._cls_dict["__attrs_init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + ) + + return self + + def add_eq(self): + cd = self._cls_dict + + cd["__eq__"] = self._add_method_dunders( + _make_eq(self._cls, self._attrs) + ) + cd["__ne__"] = self._add_method_dunders(_make_ne()) + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + if self._frozen: + return self + + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + raise ValueError( + "Can't combine custom __setattr__ with on_setattr hooks." + ) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _obj_setattr(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders(self, method): + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__) + ) + except AttributeError: + pass + + try: + method.__doc__ = "Method generated by attrs for class %s." % ( + self._cls.__qualname__, + ) + except AttributeError: + pass + + return method + + +_CMP_DEPRECATION = ( + "The usage of `cmp` is deprecated and will be removed on or after " + "2021-06-01. Please use `eq` and `order` instead." +) + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + + auto_detect must be False on Python 2. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, +): + r""" + A class decorator that adds `dunder + `_\ -methods according to the + specified attributes using `attr.ib` or the *these* argument. + + :param these: A dictionary of name to `attr.ib` mappings. This is + useful to avoid the definition of your attributes within the class body + because you can't (e.g. if you want to add ``__repr__`` methods to + Django models) or don't want to. + + If *these* is not ``None``, ``attrs`` will *not* search the class body + for attributes and will *not* remove any attributes from it. + + If *these* is an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the attributes inside *these*. Otherwise the order + of the definition of the attributes is used. + + :type these: `dict` of `str` to `attr.ib` + + :param str repr_ns: When using nested classes, there's no way in Python 2 + to automatically detect that. Therefore it's possible to set the + namespace explicitly for a more meaningful ``repr`` output. + :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, + *order*, and *hash* arguments explicitly, assume they are set to + ``True`` **unless any** of the involved methods for one of the + arguments is implemented in the *current* class (i.e. it is *not* + inherited from some base class). + + So for example by implementing ``__eq__`` on a class yourself, + ``attrs`` will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible + ``__ne__`` by default, so it *should* be enough to only implement + ``__eq__`` in most cases). + + .. warning:: + + If you prevent ``attrs`` from creating the ordering methods for you + (``order=False``, e.g. by implementing ``__le__``), it becomes + *your* responsibility to make sure its ordering is sound. The best + way is to use the `functools.total_ordering` decorator. + + + Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, + *cmp*, or *hash* overrides whatever *auto_detect* would determine. + + *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises + an `attrs.exceptions.PythonTooOldError`. + + :param bool repr: Create a ``__repr__`` method with a human readable + representation of ``attrs`` attributes.. + :param bool str: Create a ``__str__`` method that is identical to + ``__repr__``. This is usually not necessary except for + `Exception`\ s. + :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + and ``__ne__`` methods that check two instances for equality. + + They compare the instances as if they were tuples of their ``attrs`` + attributes if and only if the types of both classes are *identical*! + :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + ``__gt__``, and ``__ge__`` methods that behave like *eq* above and + allow instances to be ordered. If ``None`` (default) mirror value of + *eq*. + :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* + and *order* to the same value. Must not be mixed with *eq* or *order*. + :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method + is generated according how *eq* and *frozen* are set. + + 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to + None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning the + ``__hash__`` method of the base class will be used (if base class is + ``object``, this means it will fall back to id-based hashing.). + + Although not recommended, you can decide for yourself and force + ``attrs`` to create one (e.g. if the class is immutable even though you + didn't freeze it programmatically) by passing ``True`` or not. Both of + these cases are rather special and should be used carefully. + + See our documentation on `hashing`, Python's documentation on + `object.__hash__`, and the `GitHub issue that led to the default \ + behavior `_ for more + details. + :param bool init: Create a ``__init__`` method that initializes the + ``attrs`` attributes. Leading underscores are stripped for the argument + name. If a ``__attrs_pre_init__`` method exists on the class, it will + be called before the class is initialized. If a ``__attrs_post_init__`` + method exists on the class, it will be called after the class is fully + initialized. + + If ``init`` is ``False``, an ``__attrs_init__`` method will be + injected instead. This allows you to define a custom ``__init__`` + method that can do pre-init work such as ``super().__init__()``, + and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. + :param bool slots: Create a `slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the default + dict classes, but have some gotchas you should know about, so we + encourage you to read the `glossary entry `. + :param bool frozen: Make instances immutable after initialization. If + someone attempts to modify a frozen instance, + `attr.exceptions.FrozenInstanceError` is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` method + on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other words: + ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You can + circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + :param bool weakref_slot: Make instances weak-referenceable. This has no + effect unless ``slots`` is also enabled. + :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated + attributes (Python 3.6 and later only) from the class body. + + In this case, you **must** annotate every field. If ``attrs`` + encounters a field that is set to an `attr.ib` but lacks a type + annotation, an `attr.exceptions.UnannotatedAttributeError` is + raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't + want to set a type. + + If you assign a value to those attributes (e.g. ``x: int = 42``), that + value becomes the default value like if it were passed using + ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also + works as expected in most cases (see warning below). + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `attr.ib` are **ignored**. + + .. warning:: + For features that use the attribute name to create decorators (e.g. + `validators `), you still *must* assign `attr.ib` to + them. Otherwise Python will either not find the name or try to use + the default value to call e.g. ``validator`` on it. + + These errors can be quite confusing and probably the most common bug + report on our bug tracker. + + .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ + :param bool kw_only: Make all attributes keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed + only once and stored on the object. If this is set to ``True``, + hashing must be either explicitly or implicitly enabled for this + class. If the hash code is cached, avoid any reassignments of + fields involved in hash code computation or mutations of the objects + those fields point to after object creation. If such changes occur, + the behavior of the object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` + (which implicitly includes any subclass of any exception), the + following happens to behave like a well-behaved Python exceptions + class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids (N.B. ``attrs`` will + *not* remove existing implementations of ``__hash__`` or the equality + methods. It just won't add own ones.), + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the ``args`` + attribute, + - the value of *str* is ignored leaving ``__str__`` to base classes. + :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + collects attributes from base classes. The default behavior is + incorrect in certain cases of multiple inheritance. It should be on by + default but is kept off for backward-compatibility. + + See issue `#428 `_ for + more details. + + :param Optional[bool] getstate_setstate: + .. note:: + This is usually only interesting for slotted classes and you should + probably just set *auto_detect* to `True`. + + If `True`, ``__getstate__`` and + ``__setstate__`` are generated and attached to the class. This is + necessary for slotted classes to be pickleable. If left `None`, it's + `True` by default for slotted classes and ``False`` for dict classes. + + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, + and **either** ``__getstate__`` or ``__setstate__`` is detected directly + on the class (i.e. not inherited), it is set to `False` (this is usually + what you want). + + :param on_setattr: A callable that is run whenever the user attempts to set + an attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments + as validators: the instance, the attribute that is being modified, and + the new value. + + If no exception is raised, the attribute is set to the return value of + the callable. + + If a list of callables is passed, they're automatically wrapped in an + `attrs.setters.pipe`. + + :param Optional[callable] field_transformer: + A function that is called with the original class object and all + fields right before ``attrs`` finalizes the class. You can use + this, e.g., to automatically add converters or validators to + fields based on their types. See `transform-fields` for more details. + + :param bool match_args: + If `True` (default), set ``__match_args__`` on the class to support + `PEP 634 `_ (Structural + Pattern Matching). It is a tuple of all positional-only ``__init__`` + parameter names on Python 3.10 and later. Ignored on older Python + versions. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports ``None`` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + """ + if auto_detect and PY2: + raise PythonTooOldError( + "auto_detect only works on Python 3 and later." + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + hash_ = hash # work around the lack of nonlocal + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + + if getattr(cls, "__class__", None) is None: + raise TypeError("attrs only works with new-style classes.") + + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + raise ValueError("Can't freeze a class with a custom __setattr__.") + + builder = _ClassBuilder( + cls, + these, + slots, + is_frozen, + weakref_slot, + _determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_own_setattr, + field_transformer, + ) + if _determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ): + builder.add_repr(repr_ns) + if str is True: + builder.add_str() + + eq = _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + if not is_exc and eq is True: + builder.add_eq() + if not is_exc and _determine_whether_to_implement( + cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") + ): + builder.add_order() + + builder.add_setattr() + + if ( + hash_ is None + and auto_detect is True + and _has_own_attribute(cls, "__hash__") + ): + hash = False + else: + hash = hash_ + if hash is not True and hash is not False and hash is not None: + # Can't use `hash in` because 1 == True for example. + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + elif hash is False or (hash is None and eq is False) or is_exc: + # Don't do anything. Should fall back to __object__'s __hash__ + # which is by id. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + elif hash is True or ( + hash is None and eq is True and is_frozen is True + ): + # Build a __hash__ if told so, or if it's safe. + builder.add_hash() + else: + # Raise TypeError on attempts to hash. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + builder.make_unhashable() + + if _determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ): + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " init must be True." + ) + + if ( + PY310 + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +if PY2: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return ( + getattr(cls.__setattr__, "__module__", None) + == _frozen_setattrs.__module__ + and cls.__setattr__.__name__ == _frozen_setattrs.__name__ + ) + +else: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ == _frozen_setattrs + + +def _generate_unique_filename(cls, func_name): + """ + Create a "filename" suitable for a function being generated. + """ + unique_filename = "".format( + func_name, + cls.__module__, + getattr(cls, "__qualname__", cls.__name__), + ) + return unique_filename + + +def _make_hash(cls, attrs, frozen, cache_hash): + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + unique_filename = _generate_unique_filename(cls, "hash") + type_hash = hash(unique_filename) + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + if not PY2: + hash_def += ", *" + + hash_def += ( + ", _cache_wrapper=" + + "__import__('attr._make')._make._CacheHashWrapper):" + ) + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + " %d," % (type_hash,), + ] + ) + + for a in attrs: + method_lines.append(indent + " self.%s," % a.name) + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + if frozen: + append_hash_computation_lines( + "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + "self.%s = " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab + "return self.%s" % _hash_cache_field) + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return _make_method("__hash__", script, unique_filename) + + +def _add_hash(cls, attrs): + """ + Add a hash method to *cls*. + """ + cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False) + return cls + + +def _make_ne(): + """ + Create __ne__ method. + """ + + def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + return __ne__ + + +def _make_eq(cls, attrs): + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + unique_filename = _generate_unique_filename(cls, "eq") + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + # We can't just do a big self.x = other.x and... clause due to + # irregularities like nan == nan is false but (nan,) == (nan,) is true. + globs = {} + if attrs: + lines.append(" return (") + others = [" ) == ("] + for a in attrs: + if a.eq_key: + cmp_name = "_%s_key" % (a.name,) + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + " %s(self.%s)," + % ( + cmp_name, + a.name, + ) + ) + others.append( + " %s(other.%s)," + % ( + cmp_name, + a.name, + ) + ) + else: + lines.append(" self.%s," % (a.name,)) + others.append(" other.%s," % (a.name,)) + + lines += others + [" )"] + else: + lines.append(" return True") + + script = "\n".join(lines) + + return _make_method("__eq__", script, unique_filename, globs) + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__eq__ = _make_eq(cls, attrs) + cls.__ne__ = _make_ne() + + return cls + + +if HAS_F_STRINGS: + + def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r + for name, r, _ in attr_names_with_reprs + if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name + if i + else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = ( + '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + ) + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + " finally:", + " already_repring.remove(id(self))", + ] + + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) + +else: + + def _make_repr(attrs, ns, _): + """ + Make a repr method that includes relevant *attrs*, adding *ns* to the + full name. + """ + + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, repr if a.repr is True else a.repr) + for a in attrs + if a.repr is not False + ) + + def __repr__(self): + """ + Automatically created by attrs. + """ + try: + already_repring = _compat.repr_context.already_repring + except AttributeError: + already_repring = set() + _compat.repr_context.already_repring = already_repring + + if id(self) in already_repring: + return "..." + real_cls = self.__class__ + if ns is None: + qualname = getattr(real_cls, "__qualname__", None) + if qualname is not None: # pragma: no cover + # This case only happens on Python 3.5 and 3.6. We exclude + # it from coverage, because we don't want to slow down our + # test suite by running them under coverage too for this + # one line. + class_name = qualname.rsplit(">.", 1)[-1] + else: + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # Since 'self' remains on the stack (i.e.: strongly referenced) + # for the duration of this call, it's safe to depend on id(...) + # stability, and not need to track the instance and therefore + # worry about properties like weakref- or hash-ability. + already_repring.add(id(self)) + try: + result = [class_name, "("] + first = True + for name, attr_repr in attr_names_with_reprs: + if first: + first = False + else: + result.append(", ") + result.extend( + (name, "=", attr_repr(getattr(self, name, NOTHING))) + ) + return "".join(result) + ")" + finally: + already_repring.remove(id(self)) + + return __repr__ + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__repr__ = _make_repr(attrs, ns, cls) + return cls + + +def fields(cls): + """ + Return the tuple of ``attrs`` attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of ``attrs`` attributes for a class, whose + keys are the attribute names. + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: an ordered dict where keys are attribute names and values are + `attrs.Attribute`\\ s. This will be a `dict` if it's + naturally ordered like on Python 3.6+ or an + :class:`~collections.OrderedDict` otherwise. + + .. versionadded:: 18.1.0 + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return ordered_dict(((a.name, a) for a in attrs)) + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + :param inst: Instance of a class with ``attrs`` attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_cls(cls): + return "__slots__" in cls.__dict__ + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) + + +def _make_init( + cls, + attrs, + pre_init, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +): + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + unique_filename = _generate_unique_filename(cls, "init") + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + attrs_init, + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr"] = _obj_setattr + + init = _make_method( + "__attrs_init__" if attrs_init else "__init__", + script, + unique_filename, + globs, + ) + init.__annotations__ = annotations + + return init + + +def _setattr(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return "_setattr('%s', %s)" % (attr_name, value_var) + + +def _setattr_with_converter(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return "_setattr('%s', %s(%s))" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +def _assign(attr_name, value, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return "self.%s = %s" % (attr_name, value) + + +def _assign_with_converter(attr_name, value_var, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True) + + return "self.%s = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +if PY2: + + def _unpack_kw_only_py2(attr_name, default=None): + """ + Unpack *attr_name* from _kw_only dict. + """ + if default is not None: + arg_default = ", %s" % default + else: + arg_default = "" + return "%s = _kw_only.pop('%s'%s)" % ( + attr_name, + attr_name, + arg_default, + ) + + def _unpack_kw_only_lines_py2(kw_only_args): + """ + Unpack all *kw_only_args* from _kw_only dict and handle errors. + + Given a list of strings "{attr_name}" and "{attr_name}={default}" + generates list of lines of code that pop attrs from _kw_only dict and + raise TypeError similar to builtin if required attr is missing or + extra key is passed. + + >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) + try: + a = _kw_only.pop('a') + b = _kw_only.pop('b', 42) + except KeyError as _key_error: + raise TypeError( + ... + if _kw_only: + raise TypeError( + ... + """ + lines = ["try:"] + lines.extend( + " " + _unpack_kw_only_py2(*arg.split("=")) + for arg in kw_only_args + ) + lines += """\ +except KeyError as _key_error: + raise TypeError( + '__init__() missing required keyword-only argument: %s' % _key_error + ) +if _kw_only: + raise TypeError( + '__init__() got an unexpected keyword argument %r' + % next(iter(_kw_only)) + ) +""".split( + "\n" + ) + return lines + + +def _attrs_to_init_script( + attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + attrs_init, +): + """ + Return a script of an initializer for *attrs* and a dict of globals. + + The globals are expected by the generated script. + + If *frozen* is True, we cannot set the attributes directly so we use + a cached ``object.__setattr__``. + """ + lines = [] + if pre_init: + lines.append("self.__attrs_pre_init__()") + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr.__get__(self, self.__class__)" + ) + + if frozen is True: + if slots is True: + fmt_setter = _setattr + fmt_setter_with_converter = _setattr_with_converter + else: + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + lines.append("_inst_dict = self.__dict__") + + def fmt_setter(attr_name, value_var, has_on_setattr): + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return "_inst_dict['%s'] = %s" % (attr_name, value_var) + + def fmt_setter_with_converter( + attr_name, value_var, has_on_setattr + ): + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr + ) + + return "_inst_dict['%s'] = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + else: + # Not frozen. + fmt_setter = _assign + fmt_setter_with_converter = _assign_with_converter + + args = [] + kw_only_args = [] + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + arg_name = a.name.lstrip("_") + + has_factory = isinstance(a.default, Factory) + if has_factory and a.default.takes_self: + maybe_self = "self" + else: + maybe_self = "" + + if a.init is False: + if has_factory: + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = "%s=NOTHING" % (arg_name,) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + lines.append("if %s is not NOTHING:" % (arg_name,)) + + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and a.converter is None: + annotations[arg_name] = a.type + elif a.converter is not None and not PY2: + # Try to get the type from the converter. + sig = None + try: + sig = inspect.signature(a.converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + sig_params = list(sig.parameters.values()) + if ( + sig_params + and sig_params[0].annotation + is not inspect.Parameter.empty + ): + annotations[arg_name] = sig_params[0].annotation + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append( + " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) + ) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if post_init: + lines.append("self.__attrs_post_init__()") + + # because this is set only after __attrs_post_init is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to + # field values during post-init combined with post-init accessing the + # hash code would result in silent bugs. + if cache_hash: + if frozen: + if slots: + # if frozen and slots, then _setattr defined above + init_hash_cache = "_setattr('%s', %s)" + else: + # if frozen and not slots, then _inst_dict defined above + init_hash_cache = "_inst_dict['%s'] = %s" + else: + init_hash_cache = "self.%s = %s" + lines.append(init_hash_cache % (_hash_cache_field, "None")) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join("self." + a.name for a in attrs if a.init) + + lines.append("BaseException.__init__(self, %s)" % (vals,)) + + args = ", ".join(args) + if kw_only_args: + if PY2: + lines = _unpack_kw_only_lines_py2(kw_only_args) + lines + + args += "%s**_kw_only" % (", " if args else "",) # leading comma + else: + args += "%s*, %s" % ( + ", " if args else "", # leading comma + ", ".join(kw_only_args), # kw_only args + ) + return ( + """\ +def {init_name}(self, {args}): + {lines} +""".format( + init_name=("__attrs_init__" if attrs_init else "__init__"), + args=args, + lines="\n ".join(lines) if lines else "pass", + ), + names_for_globals, + annotations, + ) + + +class Attribute(object): + """ + *Read-only* representation of an attribute. + + The class has *all* arguments of `attr.ib` (except for ``factory`` + which is only syntactic sugar for ``default=Factory(...)`` plus the + following: + + - ``name`` (`str`): The name of the attribute. + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables + that are used for comparing and ordering objects by this attribute, + respectively. These are set by passing a callable to `attr.ib`'s ``eq``, + ``order``, or ``cmp`` arguments. See also :ref:`comparison customization + `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + + For the full version history of the fields, see `attr.ib`. + """ + + __slots__ = ( + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _obj_setattr.__get__(self, Attribute) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + metadata_proxy(metadata) + if metadata + else _empty_metadata_singleton + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + + def __setattr__(self, name, value): + raise FrozenInstanceError() + + @classmethod + def from_counting_attr(cls, name, ca, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + raise ValueError( + "Type annotation and type argument cannot both be present" + ) + inst_dict = { + k: getattr(ca, k) + for k in Attribute.__slots__ + if k + not in ( + "name", + "validator", + "default", + "type", + "inherited", + ) # exclude methods and deprecated alias + } + return cls( + name=name, + validator=ca._validator, + default=ca._default, + type=type, + cmp=None, + inherited=False, + **inst_dict + ) + + @property + def cmp(self): + """ + Simulate the presence of a cmp attribute and warn. + """ + warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) + + return self.eq and self.order + + # Don't use attr.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attr.evolve` but that function does not work + with ``Attribute``. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + metadata_proxy(value) + if value + else _empty_metadata_singleton, + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr(object): + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "counter", + "_default", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "_validator", + "converter", + "type", + "kw_only", + "on_setattr", + ) + __attrs_attrs__ = tuple( + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + ) + ) + ( + Attribute( + name="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + :raises DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError() + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class Factory(object): + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + :param callable factory: A callable that takes either none or exactly one + mandatory positional argument depending on *takes_self*. + :param bool takes_self: Pass the partially initialized instance that is + being initialized as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + """ + `Factory` is part of the default machinery so if we want a default + value here, we have to implement it ourselves. + """ + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +def make_class(name, attrs, bases=(object,), **attributes_arguments): + """ + A quick way to create a new class called *name* with *attrs*. + + :param str name: The name for the new class. + + :param attrs: A list of names or a dictionary of mappings of names to + attributes. + + If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the names or attributes inside *attrs*. Otherwise the + order of the definition of the attributes is used. + :type attrs: `list` or `dict` + + :param tuple bases: Classes that the new class will subclass. + + :param attributes_arguments: Passed unmodified to `attr.s`. + + :return: A new class with *attrs*. + :rtype: type + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + """ + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = dict((a, attrib()) for a in attrs) + else: + raise TypeError("attrs argument must be a dict or a list.") + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + try: + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + except (AttributeError, ValueError): + pass + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + return _attrs(these=cls_dict, **attributes_arguments)(type_) + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, hash=True) +class _AndValidator(object): + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + :param callables validators: Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if + they have any. + + :param callables converters: Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + def pipe_converter(val): + for converter in converters: + val = converter(val) + + return val + + if not PY2: + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = typing.TypeVar("A") + pipe_converter.__annotations__ = {"val": A, "return": A} + else: + # Get parameter type. + sig = None + try: + sig = inspect.signature(converters[0]) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if ( + params + and params[0].annotation is not inspect.Parameter.empty + ): + pipe_converter.__annotations__["val"] = params[ + 0 + ].annotation + # Get return type. + sig = None + try: + sig = inspect.signature(converters[-1]) + except (ValueError, TypeError): # inspect failed + pass + if sig and sig.return_annotation is not inspect.Signature().empty: + pipe_converter.__annotations__[ + "return" + ] = sig.return_annotation + + return pipe_converter diff --git a/openpype/vendor/python/python_2/attr/_next_gen.py b/openpype/vendor/python/python_2/attr/_next_gen.py new file mode 100644 index 0000000000..068253688c --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_next_gen.py @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: MIT + +""" +These are Python 3.6+-only and keyword-only APIs that call `attr.s` and +`attr.ib` with different default values. +""" + + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + NOTHING, + _frozen_setattrs, + _ng_default_on_setattr, + attrib, + attrs, +) +from .exceptions import UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, +): + r""" + Define an ``attrs`` class. + + Differences to the classic `attr.s` that it uses underneath: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - If *frozen* is `False`, run converters and validators when setting an + attribute by default. + - *slots=True* (see :term:`slotted classes` for potentially surprising + behaviors) + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *match_args=True* + - Some options that were only relevant on Python 2 or were kept around for + backwards-compatibility have been removed. + + Please note that these are all defaults and you can change them as you + wish. + + :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves + exactly like `attr.s`. If left `None`, `attr.s` will try to guess: + + 1. If any attributes are annotated and no unannotated `attrs.fields`\ s + are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.fields`\ s. + + For now, please refer to `attr.s` for the rest of the parameters. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _ng_default_on_setattr + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + raise ValueError( + "Frozen classes can't use on_setattr " + "(frozen-ness was inherited)." + ) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Identical to `attr.ib`, except keyword-only and with some arguments + removed. + + .. versionadded:: 20.1.0 + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) diff --git a/openpype/vendor/python/python_2/attr/_version_info.py b/openpype/vendor/python/python_2/attr/_version_info.py new file mode 100644 index 0000000000..cdaeec37a1 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_version_info.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo(object): + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them diff --git a/openpype/vendor/python/python_2/attr/_version_info.pyi b/openpype/vendor/python/python_2/attr/_version_info.pyi new file mode 100644 index 0000000000..45ced08633 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/openpype/vendor/python/python_2/attr/converters.py b/openpype/vendor/python/python_2/attr/converters.py new file mode 100644 index 0000000000..1fb6c05d7b --- /dev/null +++ b/openpype/vendor/python/python_2/attr/converters.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import PY2 +from ._make import NOTHING, Factory, pipe + + +if not PY2: + import inspect + import typing + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to ``None``. + + Type annotations will be inferred from the wrapped converter's, if it + has any. + + :param callable converter: the converter that is used for non-``None`` + values. + + .. versionadded:: 17.1.0 + """ + + def optional_converter(val): + if val is None: + return None + return converter(val) + + if not PY2: + sig = None + try: + sig = inspect.signature(converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + optional_converter.__annotations__["val"] = typing.Optional[ + params[0].annotation + ] + if sig.return_annotation is not inspect.Signature.empty: + optional_converter.__annotations__["return"] = typing.Optional[ + sig.return_annotation + ] + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace ``None`` values by *default* or the + result of *factory*. + + :param default: Value to be used if ``None`` is passed. Passing an instance + of `attrs.Factory` is supported, however the ``takes_self`` option + is *not*. + :param callable factory: A callable that takes no parameters whose result + is used if ``None`` is passed. + + :raises TypeError: If **neither** *default* or *factory* is passed. + :raises TypeError: If **both** *default* and *factory* are passed. + :raises ValueError: If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + raise TypeError("Must pass either `default` or `factory`.") + + if default is not NOTHING and factory is not None: + raise TypeError( + "Must pass either `default` or `factory` but not both." + ) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + raise ValueError( + "`takes_self` is not supported by default_if_none." + ) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (e.g., from env. vars.) to real booleans. + + Values mapping to :code:`True`: + + - :code:`True` + - :code:`"true"` / :code:`"t"` + - :code:`"yes"` / :code:`"y"` + - :code:`"on"` + - :code:`"1"` + - :code:`1` + + Values mapping to :code:`False`: + + - :code:`False` + - :code:`"false"` / :code:`"f"` + - :code:`"no"` / :code:`"n"` + - :code:`"off"` + - :code:`"0"` + - :code:`0` + + :raises ValueError: for any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + truthy = {True, "true", "t", "yes", "y", "on", "1", 1} + falsy = {False, "false", "f", "no", "n", "off", "0", 0} + try: + if val in truthy: + return True + if val in falsy: + return False + except TypeError: + # Raised when "val" is not hashable (e.g., lists) + pass + raise ValueError("Cannot convert value to bool: {}".format(val)) diff --git a/openpype/vendor/python/python_2/attr/converters.pyi b/openpype/vendor/python/python_2/attr/converters.pyi new file mode 100644 index 0000000000..0f58088a37 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/converters.pyi @@ -0,0 +1,13 @@ +from typing import Callable, Optional, TypeVar, overload + +from . import _ConverterType + +_T = TypeVar("_T") + +def pipe(*validators: _ConverterType) -> _ConverterType: ... +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: _T) -> _ConverterType: ... +@overload +def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... +def to_bool(val: str) -> bool: ... diff --git a/openpype/vendor/python/python_2/attr/exceptions.py b/openpype/vendor/python/python_2/attr/exceptions.py new file mode 100644 index 0000000000..b2f1edc32a --- /dev/null +++ b/openpype/vendor/python/python_2/attr/exceptions.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: MIT + +from __future__ import absolute_import, division, print_function + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + msg = "can't set attribute" + args = [msg] + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An ``attrs`` function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-``attrs`` class has been passed into an ``attrs`` function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set using ``attr.ib()`` and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type + annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an ``attrs`` feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A ``attr.ib()`` requiring a callable has been set with a value + that is not callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/openpype/vendor/python/python_2/attr/exceptions.pyi b/openpype/vendor/python/python_2/attr/exceptions.pyi new file mode 100644 index 0000000000..f2680118b4 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/openpype/vendor/python/python_2/attr/filters.py b/openpype/vendor/python/python_2/attr/filters.py new file mode 100644 index 0000000000..a1978a8775 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/filters.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attr.asdict`. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import isclass +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isclass(cls)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Include *what*. + + :param what: What to include. + :type what: `list` of `type` or `attrs.Attribute`\\ s + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def include_(attribute, value): + return value.__class__ in cls or attribute in attrs + + return include_ + + +def exclude(*what): + """ + Exclude *what*. + + :param what: What to exclude. + :type what: `list` of classes or `attrs.Attribute`\\ s. + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def exclude_(attribute, value): + return value.__class__ not in cls and attribute not in attrs + + return exclude_ diff --git a/openpype/vendor/python/python_2/attr/filters.pyi b/openpype/vendor/python/python_2/attr/filters.pyi new file mode 100644 index 0000000000..993866865e --- /dev/null +++ b/openpype/vendor/python/python_2/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any, Union + +from . import Attribute, _FilterType + +def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/openpype/vendor/python/python_2/attr/py.typed b/openpype/vendor/python/python_2/attr/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/vendor/python/python_2/attr/setters.py b/openpype/vendor/python/python_2/attr/setters.py new file mode 100644 index 0000000000..b1cbb5d83e --- /dev/null +++ b/openpype/vendor/python/python_2/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from __future__ import absolute_import, division, print_function + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError() + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + return c(new_value) + + return new_value + + +NO_OP = object() +""" +Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. + +Does not work in `pipe` or within lists. + +.. versionadded:: 20.1.0 +""" diff --git a/openpype/vendor/python/python_2/attr/setters.pyi b/openpype/vendor/python/python_2/attr/setters.pyi new file mode 100644 index 0000000000..3f5603c2b0 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/setters.pyi @@ -0,0 +1,19 @@ +from typing import Any, NewType, NoReturn, TypeVar, cast + +from . import Attribute, _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/openpype/vendor/python/python_2/attr/validators.py b/openpype/vendor/python/python_2/attr/validators.py new file mode 100644 index 0000000000..0b0c8342f2 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/validators.py @@ -0,0 +1,561 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +from __future__ import absolute_import, division, print_function + +import operator +import re + +from contextlib import contextmanager + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .exceptions import NotCallableError + + +try: + Pattern = re.Pattern +except AttributeError: # Python <3.7 lacks a Pattern type. + Pattern = type(re.compile("")) + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "optional", + "provides", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + :param disabled: If ``True``, disable running all validators. + :type disabled: bool + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + :return: ``True`` if validators are currently disabled. + :rtype: bool + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(False) + try: + yield + finally: + set_run_validators(True) + + +@attrs(repr=False, slots=True, hash=True) +class _InstanceOfValidator(object): + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + raise TypeError( + "'{name}' must be {type!r} (got {value!r} that is a " + "{actual!r}).".format( + name=attr.name, + type=self.type, + actual=value.__class__, + value=value, + ), + attr, + self.type, + value, + ) + + def __repr__(self): + return "".format( + type=self.type + ) + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected type, and the value it + got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator(object): + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + raise ValueError( + "'{name}' must match regex {pattern!r}" + " ({value!r} doesn't)".format( + name=attr.name, pattern=self.pattern.pattern, value=value + ), + attr, + self.pattern, + value, + ) + + def __repr__(self): + return "".format( + pattern=self.pattern + ) + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called + with a string that doesn't match *regex*. + + :param regex: a regex string or precompiled pattern to match against + :param int flags: flags that will be passed to the underlying re function + (default 0) + :param callable func: which underlying `re` function to call (options + are `re.fullmatch`, `re.search`, `re.match`, default + is ``None`` which means either `re.fullmatch` or an emulation of + it on Python 2). For performance reasons, they won't be used directly + but on a pre-`re.compile`\ ed pattern. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + fullmatch = getattr(re, "fullmatch", None) + valid_funcs = (fullmatch, None, re.search, re.match) + if func not in valid_funcs: + raise ValueError( + "'func' must be one of {}.".format( + ", ".join( + sorted( + e and e.__name__ or "None" for e in set(valid_funcs) + ) + ) + ) + ) + + if isinstance(regex, Pattern): + if flags: + raise TypeError( + "'flags' can only be used with a string pattern; " + "pass flags to re.compile() instead" + ) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + elif fullmatch: + match_func = pattern.fullmatch + else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) + pattern = re.compile( + r"(?:{})\Z".format(pattern.pattern), pattern.flags + ) + match_func = pattern.match + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, hash=True) +class _ProvidesValidator(object): + interface = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.interface.providedBy(value): + raise TypeError( + "'{name}' must provide {interface!r} which {value!r} " + "doesn't.".format( + name=attr.name, interface=self.interface, value=value + ), + attr, + self.interface, + value, + ) + + def __repr__(self): + return "".format( + interface=self.interface + ) + + +def provides(interface): + """ + A validator that raises a `TypeError` if the initializer is called + with an object that does not provide the requested *interface* (checks are + performed using ``interface.providedBy(value)`` (see `zope.interface + `_). + + :param interface: The interface to check for. + :type interface: ``zope.interface.Interface`` + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected interface, and the + value it got. + """ + return _ProvidesValidator(interface) + + +@attrs(repr=False, slots=True, hash=True) +class _OptionalValidator(object): + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return "".format( + what=repr(self.validator) + ) + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to ``None`` in addition to satisfying the requirements of + the sub-validator. + + :param validator: A validator (or a list of validators) that is used for + non-``None`` values. + :type validator: callable or `list` of callables. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + """ + if isinstance(validator, list): + return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, hash=True) +class _InValidator(object): + options = attrib() + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + raise ValueError( + "'{name}' must be in {options!r} (got {value!r})".format( + name=attr.name, options=self.options, value=value + ) + ) + + def __repr__(self): + return "".format( + options=self.options + ) + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called + with a value that does not belong in the options provided. The check is + performed using ``value in options``. + + :param options: Allowed options. + :type options: list, tuple, `enum.Enum`, ... + + :raises ValueError: With a human readable error message, the attribute (of + type `attrs.Attribute`), the expected options, and the value it + got. + + .. versionadded:: 17.1.0 + """ + return _InValidator(options) + + +@attrs(repr=False, slots=False, hash=True) +class _IsCallableValidator(object): + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attr.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute + that is not callable. + + .. versionadded:: 19.1.0 + + :raises `attr.exceptions.NotCallableError`: With a human readable error + message containing the attribute (`attrs.Attribute`) name, + and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, hash=True) +class _DeepIterable(object): + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else " {iterable!r}".format(iterable=self.iterable_validator) + ) + return ( + "" + ).format( + iterable_identifier=iterable_identifier, + member=self.member_validator, + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + :param member_validator: Validator to apply to iterable members + :param iterable_validator: Validator to apply to iterable itself + (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, hash=True) +class _DeepMapping(object): + key_validator = attrib(validator=is_callable()) + value_validator = attrib(validator=is_callable()) + mapping_validator = attrib(default=None, validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + self.key_validator(inst, attr, key) + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return ( + "" + ).format(key=self.key_validator, value=self.value_validator) + + +def deep_mapping(key_validator, value_validator, mapping_validator=None): + """ + A validator that performs deep validation of a dictionary. + + :param key_validator: Validator to apply to dictionary keys + :param value_validator: Validator to apply to dictionary values + :param mapping_validator: Validator to apply to top-level mapping + attribute (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator(object): + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + raise ValueError( + "'{name}' must be {op} {bound}: {value}".format( + name=attr.name, + op=self.compare_op, + bound=self.bound, + value=value, + ) + ) + + def __repr__(self): + return "".format( + op=self.compare_op, bound=self.bound + ) + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number larger or equal to *val*. + + :param val: Exclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number greater than *val*. + + :param val: Inclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller than *val*. + + :param val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller or equal to *val*. + + :param val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator(object): + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + raise ValueError( + "Length of '{name}' must be <= {max}: {len}".format( + name=attr.name, max=self.max_length, len=len(value) + ) + ) + + def __repr__(self): + return "".format(max=self.max_length) + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + :param int length: Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) diff --git a/openpype/vendor/python/python_2/attr/validators.pyi b/openpype/vendor/python/python_2/attr/validators.pyi new file mode 100644 index 0000000000..5e00b85433 --- /dev/null +++ b/openpype/vendor/python/python_2/attr/validators.pyi @@ -0,0 +1,78 @@ +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + List, + Mapping, + Match, + Optional, + Pattern, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from . import _ValidatorType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2]] +) -> _ValidatorType[Union[_T1, _T2]]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2], Type[_T3]] +) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... +@overload +def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... +def provides(interface: Any) -> _ValidatorType[Any]: ... +def optional( + validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] +) -> _ValidatorType[Optional[_T]]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Union[Pattern[AnyStr], AnyStr], + flags: int = ..., + func: Optional[ + Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] + ] = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorType[_T], + iterable_validator: Optional[_ValidatorType[_I]] = ..., +) -> _ValidatorType[_I]: ... +def deep_mapping( + key_validator: _ValidatorType[_K], + value_validator: _ValidatorType[_V], + mapping_validator: Optional[_ValidatorType[_M]] = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... diff --git a/openpype/vendor/python/python_2/attrs/__init__.py b/openpype/vendor/python/python_2/attrs/__init__.py new file mode 100644 index 0000000000..a704b8b56b --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/__init__.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: MIT + +from attr import ( + NOTHING, + Attribute, + Factory, + __author__, + __copyright__, + __description__, + __doc__, + __email__, + __license__, + __title__, + __url__, + __version__, + __version_info__, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._next_gen import asdict, astuple + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "Attribute", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "Factory", + "field", + "fields_dict", + "fields", + "filters", + "frozen", + "has", + "make_class", + "mutable", + "NOTHING", + "resolve_types", + "setters", + "validate", + "validators", +] diff --git a/openpype/vendor/python/python_2/attrs/__init__.pyi b/openpype/vendor/python/python_2/attrs/__init__.pyi new file mode 100644 index 0000000000..7426fa5ddb --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/__init__.pyi @@ -0,0 +1,63 @@ +from typing import ( + Any, + Callable, + Dict, + Mapping, + Optional, + Sequence, + Tuple, + Type, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import _FilterType +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import define as define +from attr import evolve as evolve +from attr import Factory as Factory +from attr import exceptions as exceptions +from attr import field as field +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import frozen as frozen +from attr import has as has +from attr import make_class as make_class +from attr import mutable as mutable +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators + +# TODO: see definition of attr.asdict/astuple +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: bool = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... diff --git a/openpype/vendor/python/python_2/attrs/converters.py b/openpype/vendor/python/python_2/attrs/converters.py new file mode 100644 index 0000000000..edfa8d3c16 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/converters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.converters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/exceptions.py b/openpype/vendor/python/python_2/attrs/exceptions.py new file mode 100644 index 0000000000..bd9efed202 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/exceptions.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.exceptions import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/filters.py b/openpype/vendor/python/python_2/attrs/filters.py new file mode 100644 index 0000000000..52959005b0 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/filters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.filters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/py.typed b/openpype/vendor/python/python_2/attrs/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/vendor/python/python_2/attrs/setters.py b/openpype/vendor/python/python_2/attrs/setters.py new file mode 100644 index 0000000000..9b50770804 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/setters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.setters import * # noqa diff --git a/openpype/vendor/python/python_2/attrs/validators.py b/openpype/vendor/python/python_2/attrs/validators.py new file mode 100644 index 0000000000..ab2c9b3024 --- /dev/null +++ b/openpype/vendor/python/python_2/attrs/validators.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.validators import * # noqa From c6383837e0c094a4172c6895db768a3d3ccebc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:00:46 +0200 Subject: [PATCH 0779/1030] :recycle: remove unnecessary type hint Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- igniter/bootstrap_repos.py | 1 - 1 file changed, 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index dfcca2cf33..c5003b062e 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -614,7 +614,6 @@ class OpenPypeVersion(semver.VersionInfo): return None all_versions.sort() - latest_version: OpenPypeVersion return all_versions[-1] @classmethod From e5b1cc59bdccc2175364ae24cdddb7eb40a7c2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:37:40 +0200 Subject: [PATCH 0780/1030] :bug: missing comma Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- 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 cece8ee22b..67b5f2496b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -45,7 +45,7 @@ def get_ocio_config_path(profile_folder): os.environ["OPENPYPE_ROOT"], "vendor", "bin", - "ocioconfig" + "ocioconfig", "OpenColorIOConfigs", profile_folder, "config.ocio" From 4193b54700c42405ffa22e5353985202ce858ee2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 18:36:33 +0200 Subject: [PATCH 0781/1030] added more information when auto sync is turned on/off --- .../event_sync_to_avalon.py | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index a4e791aaf0..738181dc9a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -697,13 +697,22 @@ class SyncToAvalonEvent(BaseEvent): continue auto_sync = changes[CUST_ATTR_AUTO_SYNC]["new"] - if auto_sync == "1": + turned_on = auto_sync == "1" + ft_project = self.cur_project + username = self._get_username(session, event) + message = ( + "Auto sync was turned {} for project \"{}\" by \"{}\"." + ).format( + "on" if turned_on else "off", + ft_project["full_name"], + username + ) + if turned_on: + message += " Triggering syncToAvalon action." + self.log.debug(message) + + if turned_on: # Trigger sync to avalon action if auto sync was turned on - ft_project = self.cur_project - self.log.debug(( - "Auto sync was turned on for project <{}>." - " Triggering syncToAvalon action." - ).format(ft_project["full_name"])) selection = [{ "entityId": ft_project["id"], "entityType": "show" @@ -851,6 +860,26 @@ class SyncToAvalonEvent(BaseEvent): self.report() return True + def _get_username(self, session, event): + username = "Unknown" + event_source = event.get("source") + if not event_source: + return username + user_info = event_source.get("user") + if not user_info: + return username + user_id = user_info.get("id") + if not user_id: + return username + + user_entity = session.query( + "User where id is {}".format(user_id) + ).first() + if user_entity: + username = user_entity["username"] or username + return username + + def process_removed(self): """ Handles removed entities (not removed tasks - handle separately). From 61e8d7e9f1fbffd91e268ef3ff721cc136395f27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:14:01 +0200 Subject: [PATCH 0782/1030] use 'get_projects' instead of 'projects' method on AvalonMongoDB --- .../modules/kitsu/utils/update_zou_with_op.py | 8 +++- .../modules/sync_server/sync_server_module.py | 9 ++-- openpype/tools/launcher/models.py | 3 +- openpype/tools/libraryloader/app.py | 4 +- .../project_manager/project_manager/model.py | 7 +--- openpype/tools/sceneinventory/window.py | 6 +-- openpype/tools/utils/models.py | 41 ++++++++++--------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 57d7094e95..da924aa5ee 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,7 +6,11 @@ from typing import List import gazu from pymongo import UpdateOne -from openpype.client import get_project, get_assets +from openpype.client import ( + get_projects, + get_project, + get_assets, +) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials @@ -37,7 +41,7 @@ def sync_zou(login: str, password: str): dbcon = AvalonMongoDB() dbcon.install() - op_projects = [p for p in dbcon.projects()] + op_projects = list(get_projects()) for project_doc in op_projects: sync_zou_from_op_project(project_doc["name"], dbcon, project_doc) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 8fdfab9c2e..c7f9484e55 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -6,7 +6,7 @@ import platform import copy from collections import deque, defaultdict - +from openpype.client import get_projects from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule from openpype.settings import ( @@ -913,7 +913,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(projection={"name": 1}): + for project in get_projects(fields=["name"]): project_name = project["name"] if self.is_project_enabled(project_name): enabled_projects.append(project_name) @@ -1242,10 +1242,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def _prepare_sync_project_settings(self, exclude_locals): sync_project_settings = {} system_sites = self.get_all_site_configs() - project_docs = self.connection.projects( - projection={"name": 1}, - only_active=True - ) + project_docs = get_projects(fields=["name"]) for project_doc in project_docs: project_name = project_doc["name"] sites = copy.deepcopy(system_sites) # get all configured sites diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 3f899cc05e..6d40d21f96 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -10,6 +10,7 @@ from Qt import QtCore, QtGui import qtawesome from openpype.client import ( + get_projects, get_project, get_assets, ) @@ -527,7 +528,7 @@ class LauncherModel(QtCore.QObject): current_project = self.project_name project_names = set() project_docs_by_name = {} - for project_doc in self._dbcon.projects(only_active=True): + for project_doc in get_projects(): project_name = project_doc["name"] project_names.add(project_name) project_docs_by_name[project_name] = project_doc diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 5f4d10d796..d2af1b7151 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -3,7 +3,7 @@ import sys from Qt import QtWidgets, QtCore, QtGui from openpype import style -from openpype.client import get_project +from openpype.client import get_projects, get_project from openpype.pipeline import AvalonMongoDB from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( @@ -239,7 +239,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): def get_filtered_projects(self): projects = list() - for project in self.dbcon.projects(): + for project in get_projects(fields=["name", "data.library_project"]): is_library = project.get("data", {}).get("library_project", False) if ( (is_library and self.show_libraries) or diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c5bde5aaec..3aaee75698 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -8,6 +8,7 @@ from pymongo import UpdateOne, DeleteOne from Qt import QtCore, QtGui from openpype.client import ( + get_projects, get_project, get_assets, get_asset_ids_with_subsets, @@ -54,12 +55,8 @@ class ProjectModel(QtGui.QStandardItemModel): self._items_by_name[None] = none_project new_project_items.append(none_project) - project_docs = self.dbcon.projects( - projection={"name": 1}, - only_active=True - ) project_names = set() - for project_doc in project_docs: + for project_doc in get_projects(fields=["name"]): project_name = project_doc.get("name") if not project_name: continue diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 054c2a2daa..463280b71c 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -4,8 +4,9 @@ import sys from Qt import QtWidgets, QtCore import qtawesome -from openpype.pipeline import legacy_io from openpype import style +from openpype.client import get_projects +from openpype.pipeline import legacy_io from openpype.tools.utils.delegates import VersionDelegate from openpype.tools.utils.lib import ( qt_app_context, @@ -195,8 +196,7 @@ def show(root=None, debug=False, parent=None, items=None): if not os.environ.get("AVALON_PROJECT"): any_project = next( - project for project in legacy_io.projects() - if project.get("active", True) is not False + project for project in get_projects() ) project_name = any_project["name"] diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 8991614fe1..1faccef4dd 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -3,6 +3,7 @@ import logging import Qt from Qt import QtCore, QtGui +from openpype.client import get_projects from .constants import ( PROJECT_IS_ACTIVE_ROLE, PROJECT_NAME_ROLE, @@ -296,29 +297,29 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item = item project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + project_docs = get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ) + for project_doc in project_docs: + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): From 260ef9999516d437ad399a0b746ff73632106314 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:15:42 +0200 Subject: [PATCH 0783/1030] removed unused code --- openpype/tools/utils/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 2169cf8ef1..99d8c75ab4 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -443,10 +443,6 @@ class FamilyConfigCache: if profiles: # Make sure connection is installed # - accessing attribute which does not have auto-install - self.dbcon.install() - database = getattr(self.dbcon, "database", None) - if database is None: - database = self.dbcon._database asset_doc = get_asset_by_name( project_name, asset_name, fields=["data.tasks"] ) or {} From e86ea84da897f0ecd6608bdfff460c742a10042e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:37 +0200 Subject: [PATCH 0784/1030] use 'get_projects' in standalone publisher --- openpype/tools/standalonepublish/widgets/widget_asset.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 73114f7960..77d756a606 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore import qtawesome from openpype.client import ( + get_projects, get_project, get_asset_by_id, ) @@ -291,9 +292,7 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for doc in self.dbcon.projects(projection={"name": 1}, - only_active=True): - + for doc in get_projects(fields=["name"]): project_name = doc.get("name") if project_name: project_names.append(project_name) @@ -320,8 +319,7 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(projection={"name": 1}, - only_active=True): + for project in get_projects(fields=["name"]): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 68ca5898920d79c93bc51e699f319e5987019063 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:50 +0200 Subject: [PATCH 0785/1030] use 'get_projects' in settings --- openpype/tools/settings/settings/widgets.py | 60 ++++++++------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 88d923c16a..1a4a6877b0 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -3,6 +3,7 @@ import uuid from Qt import QtWidgets, QtCore, QtGui import qtawesome +from openpype.client import get_projects from openpype.pipeline import AvalonMongoDB from openpype.style import get_objected_colors from openpype.tools.utils.widgets import ImageButton @@ -783,8 +784,6 @@ class ProjectModel(QtGui.QStandardItemModel): self.setColumnCount(2) - self.dbcon = None - self._only_active = only_active self._default_item = None self._items_by_name = {} @@ -828,9 +827,6 @@ class ProjectModel(QtGui.QStandardItemModel): index = self.index(index.row(), 0, index.parent()) return super(ProjectModel, self).flags(index) - def set_dbcon(self, dbcon): - self.dbcon = dbcon - def refresh(self): # Change id of versions refresh self._version_refresh_id = uuid.uuid4() @@ -846,31 +842,30 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item.setData("", PROJECT_VERSION_ROLE) project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + for project_doc in get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - item.setData("", PROJECT_VERSION_ROLE) - item.setData(False, PROJECT_IS_SELECTED_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData("", PROJECT_VERSION_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): @@ -1067,8 +1062,6 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_model = project_model self.inactive_chk = inactive_chk - self.dbcon = None - def set_entity(self, entity): self._entity = entity @@ -1211,15 +1204,6 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(PROJECT_NAME_ROLE) break - if not self.dbcon: - try: - self.dbcon = AvalonMongoDB() - self.dbcon.install() - except Exception: - self.dbcon = None - self.current_project = None - - self.project_model.set_dbcon(self.dbcon) self.project_model.refresh() self.project_proxy.sort(0) From bcb15c2fc5b28ff38ea3d5a9beca41186dec1615 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 Aug 2022 21:01:30 +0200 Subject: [PATCH 0786/1030] nuke: validation type mishmash wip --- .../nuke/plugins/publish/validate_write_nodes.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 48dce623a9..9c9b8babaa 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,3 +1,4 @@ +import six import pyblish.api from openpype.api import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( @@ -72,11 +73,21 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): for knob_data in check_knobs: key = knob_data["name"] value = knob_data["value"] + node_value = write_node[key].value() + + # fix type differences + if type(node_value) in (int, float): + value = float(value) + node_value = float(node_value) + else: + value = str(value) + node_value = str(node_value) + self.log.debug("__ key: {} | value: {}".format( key, value )) if ( - str(write_node[key].value()) != str(value) + node_value != value and key != "file" and key != "tile_color" ): From 874d95270f35f305650a05649a6344379ccbe4e1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 16 Aug 2022 10:11:45 +0200 Subject: [PATCH 0787/1030] 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 0788/1030] 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 0789/1030] 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 0790/1030] 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 0791/1030] 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 c9f3340f3c750c0b64790607a254d43e76dfb134 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Aug 2022 12:18:53 +0200 Subject: [PATCH 0792/1030] OP-3723 - resize saved images in PS for ffmpeg Ffmpeg cannot handle pictures higher than 16384x16384. Uses PIL to resize to max size(with aspect ratio). In the future this plugin should be refactored (to use general ExtractThumbnail and ExtractReview). --- .../plugins/publish/extract_review.py | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index d076610ead..7f78a46527 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -1,5 +1,6 @@ import os import shutil +from PIL import Image import openpype.api import openpype.lib @@ -8,10 +9,17 @@ from openpype.hosts.photoshop import api as photoshop class ExtractReview(openpype.api.Extractor): """ - Produce a flattened or sequence image file from all 'image' instances. + Produce a flattened or sequence image files from all 'image' instances. If no 'image' instance is created, it produces flattened image from all visible layers. + + It creates review, thumbnail and mov representations. + + 'review' family could be used in other steps as a reference, as it + contains flattened image by default. (Eg. artist could load this + review as a single item and see full image. In most cases 'image' + family is separated by layers to better usage in animation or comp.) """ label = "Extract Review" @@ -49,7 +57,7 @@ class ExtractReview(openpype.api.Extractor): "stagingDir": staging_dir, "tags": self.jpg_options['tags'], }) - + processed_img_names = img_list else: self.log.info("Extract layers to flatten image.") img_list = self._saves_flattened_layers(staging_dir, layers) @@ -57,26 +65,33 @@ class ExtractReview(openpype.api.Extractor): instance.data["representations"].append({ "name": "jpg", "ext": "jpg", - "files": img_list, + "files": img_list, # cannot be [] for single frame "stagingDir": staging_dir, "tags": self.jpg_options['tags'] }) + processed_img_names = [img_list] ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") instance.data["stagingDir"] = staging_dir - # Generate thumbnail. + source_files_pattern = os.path.join(staging_dir, + self.output_seq_filename) + source_files_pattern = self._check_and_resize(processed_img_names, + source_files_pattern, + staging_dir) + # Generate thumbnail thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") self.log.info(f"Generate thumbnail {thumbnail_path}") args = [ ffmpeg_path, "-y", - "-i", os.path.join(staging_dir, self.output_seq_filename), + "-i", source_files_pattern, "-vf", "scale=300:-1", "-vframes", "1", thumbnail_path ] + self.log.debug("thumbnail args:: {}".format(args)) output = openpype.lib.run_subprocess(args) instance.data["representations"].append({ @@ -94,11 +109,12 @@ class ExtractReview(openpype.api.Extractor): args = [ ffmpeg_path, "-y", - "-i", os.path.join(staging_dir, self.output_seq_filename), + "-i", source_files_pattern, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-vframes", str(img_number), mov_path ] + self.log.debug("mov args:: {}".format(args)) output = openpype.lib.run_subprocess(args) self.log.debug(output) instance.data["representations"].append({ @@ -120,6 +136,33 @@ class ExtractReview(openpype.api.Extractor): self.log.info(f"Extracted {instance} to {staging_dir}") + def _check_and_resize(self, processed_img_names, source_files_pattern, + staging_dir): + """Check if saved image could be used in ffmpeg. + + Ffmpeg has max size 16384x16384. Saved image(s) must be resized to be + used as a source for thumbnail or review mov. + """ + max_ffmpeg_size = 16384 + first_url = os.path.join(staging_dir, processed_img_names[0]) + with Image.open(first_url) as im: + width, height = im.size + + if width > max_ffmpeg_size or height > max_ffmpeg_size: + resized_dir = os.path.join(staging_dir, "resized") + os.mkdir(resized_dir) + source_files_pattern = os.path.join(resized_dir, + self.output_seq_filename) + for file_name in processed_img_names: + source_url = os.path.join(staging_dir, file_name) + with Image.open(source_url) as res_img: + # 'thumbnail' automatically keeps aspect ratio + res_img.thumbnail((max_ffmpeg_size, max_ffmpeg_size), + Image.ANTIALIAS) + res_img.save(os.path.join(resized_dir, file_name)) + + return source_files_pattern + def _get_image_path_from_instances(self, instance): img_list = [] From 5e9d4f7603f887e3eff156e38943c4e46cd4acb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 13:55:41 +0200 Subject: [PATCH 0793/1030] change label of 'IntegrateFtrackInstance' in settings --- .../entities/schemas/projects_schema/schema_project_ftrack.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..6aa8ea9c7d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -948,7 +948,7 @@ { "type": "dict", "key": "IntegrateFtrackInstance", - "label": "IntegrateFtrackInstance", + "label": "Integrate Ftrack Instance", "is_group": true, "children": [ { From a8bc744185e12d3ff65f5760ab9ff98b01069482 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 13:56:40 +0200 Subject: [PATCH 0794/1030] store ftrack task to instance on creation --- .../plugins/publish/integrate_ftrack_api.py | 3 +- .../publish/integrate_hierarchy_ftrack.py | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 20a69e060c..159e60024d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -13,6 +13,7 @@ Provides: import os import sys import collections + import six import pyblish.api import clique @@ -21,7 +22,7 @@ import clique class IntegrateFtrackApi(pyblish.api.InstancePlugin): """ Commit components to server. """ - order = pyblish.api.IntegratorOrder+0.499 + order = pyblish.api.IntegratorOrder + 0.499 label = "Integrate Ftrack Api" families = ["ftrack"] diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index b8855ee2bd..c520c6f2cf 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -153,8 +153,14 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # CUSTOM ATTRIBUTES custom_attributes = entity_data.get('custom_attributes', []) instances = [ - i for i in self.context if i.data['asset'] in entity['name'] + instance + for instance in self.context + if instance.data.get("asset") == entity["name"] ] + + for instance in instances: + instance.data["ftrackEntity"] = entity + for key in custom_attributes: hier_attr = hier_attr_by_key.get(key) # Use simple method if key is not hierarchical @@ -184,9 +190,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): ) ) - for instance in instances: - instance.data['ftrackEntity'] = entity - try: self.session.commit() except Exception: @@ -196,13 +199,22 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) # TASKS + instances_by_task_name = collections.defaultdict(list) + for instance in instances: + task_name = instance.data.get("task") + if task_name: + instances_by_task_name[task_name].append(instance) + tasks = entity_data.get('tasks', []) existing_tasks = [] tasks_to_create = [] for child in entity['children']: - if child.entity_type.lower() == 'task': - existing_tasks.append(child['name'].lower()) - # existing_tasks.append(child['type']['name']) + if child.entity_type.lower() == "task": + task_name_low = child["name"].lower() + existing_tasks.append(task_name_low) + + for instance in instances_by_task_name[task_name_low]: + instance["ftrackTask"] = child for task_name in tasks: task_type = tasks[task_name]["type"] @@ -212,7 +224,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): tasks_to_create.append((task_name, task_type)) for task_name, task_type in tasks_to_create: - self.create_task( + task_entity = self.create_task( name=task_name, task_type=task_type, parent=entity @@ -225,6 +237,9 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session._configure_locations() six.reraise(tp, value, tb) + for instance in instances_by_task_name[task_name.lower()]: + instance.data["ftrackTask"] = task_entity + # Incoming links. self.create_links(project_name, entity_data, entity) try: From 46726e5afedc88ef22b31f515e70b08308643acd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Aug 2022 14:31:18 +0200 Subject: [PATCH 0795/1030] OP-3713 - changed type to tri-state Customer wants to have more granularity, they want to create flatten 'image', but not separate 'image' per layer. --- .../settings/defaults/project_settings/photoshop.json | 2 +- .../projects_schema/schema_project_photoshop.json | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index d9b7a8083f..b08e73f1ee 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -8,7 +8,7 @@ }, "publish": { "CollectColorCodedInstances": { - "create_flatten_image": false, + "create_flatten_image": "no", "flatten_subset_template": "", "color_code_mapping": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index badf94229b..6935ec8e5e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -45,9 +45,15 @@ "label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)" }, { - "type": "boolean", "key": "create_flatten_image", - "label": "Create flatten image" + "label": "Create flatten image", + "type": "enum", + "multiselection": false, + "enum_items": [ + { "yes": "Yes" }, + { "no": "No" }, + { "only": "Only flatten" } + ] }, { "type": "text", From 9bfc1447b7945cd11d11414df3ffad42f6014292 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Aug 2022 14:32:47 +0200 Subject: [PATCH 0796/1030] OP-3713 - implement tri-state logic for create_flatten_image Customer wants to have more granularity, they want to create flatten 'image', but not separate 'image' per layer. --- .../publish/collect_color_coded_instances.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index 71bd2cd854..9adc16d0fd 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -32,7 +32,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): # TODO check if could be set globally, probably doesn't make sense when # flattened template cannot subset_template_name = "" - create_flatten_image = False + create_flatten_image = "no" # probably not possible to configure this globally flatten_subset_template = "" @@ -98,13 +98,16 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): "Subset {} already created, skipping.".format(subset)) continue - instance = self._create_instance(context, layer, resolved_family, - asset_name, subset, task_name) + if self.create_flatten_image != "only": + instance = self._create_instance(context, layer, + resolved_family, + asset_name, subset, task_name) + created_instances.append(instance) + existing_subset_names.append(subset) publishable_layers.append(layer) - created_instances.append(instance) - if self.create_flatten_image and publishable_layers: + if self.create_flatten_image != "no" and publishable_layers: self.log.debug("create_flatten_image") if not self.flatten_subset_template: self.log.warning("No template for flatten image") @@ -116,7 +119,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): first_layer = publishable_layers[0] # dummy layer first_layer.name = subset - family = created_instances[0].data["family"] # inherit family + family = resolved_family # inherit family instance = self._create_instance(context, first_layer, family, asset_name, subset, task_name) From b679a46b9c3b9cbabbf056bf530d26ae6bdb1309 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 14:32:55 +0200 Subject: [PATCH 0797/1030] changed default value of 'asset_versions_status_profiles' to match settings --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index a1e5922730..7caf17c18d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -42,7 +42,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "reference": "reference" } keep_first_subset_name_for_review = True - asset_versions_status_profiles = {} + asset_versions_status_profiles = [] def process(self, instance): self.log.debug("instance {}".format(instance)) From fce758d3fa8bb307ca3d9501ec772617a7a0987e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 14:33:43 +0200 Subject: [PATCH 0798/1030] removed unused settings 'first_version_status' in ftrack --- .../defaults/project_settings/ftrack.json | 4 ---- .../projects_schema/schema_project_ftrack.json | 18 ------------------ 2 files changed, 22 deletions(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..98d1587a35 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -96,10 +96,6 @@ "mapping": {}, "asset_types_to_skip": [] }, - "first_version_status": { - "enabled": true, - "status": "" - }, "next_task_update": { "enabled": true, "mapping": { 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 6aa8ea9c7d..b8a1f011a3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -299,24 +299,6 @@ } ] }, - { - "type": "dict", - "key": "first_version_status", - "label": "Set status on first created version", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "text", - "key": "status", - "label": "Status" - } - ] - }, { "type": "dict", "key": "next_task_update", From cfb14d32b50920d06fbfc6d1f74da2798910b3da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 15:42:25 +0200 Subject: [PATCH 0799/1030] Show dialog if installed version is not compatible in UI mode --- start.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/start.py b/start.py index 084eb7451a..d1198a85e4 100644 --- a/start.py +++ b/start.py @@ -748,12 +748,21 @@ def _find_frozen_openpype(use_version: str = None, _initialize_environment(openpype_version) return version_path + in_headless_mode = os.getenv("OPENPYPE_HEADLESS_MODE") == "1" if not installed_version.is_compatible(openpype_version): - raise OpenPypeVersionIncompatible( - ( - f"Latest version found {openpype_version} is not " - f"compatible with currently running {installed_version}" + message = "Version {} is not compatible with installed version {}." + # Show UI to user + if not in_headless_mode: + igniter.show_message_dialog( + "Incompatible OpenPype installation", + message.format( + "{}".format(openpype_version), + "{}".format(installed_version) + ) ) + # Raise incompatible error + raise OpenPypeVersionIncompatible( + message.format(openpype_version, installed_version) ) # test if latest detected is installed (in user data dir) @@ -768,7 +777,7 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": + if in_headless_mode: version_path = bootstrap.install_version( openpype_version, force=True ) From 141b275fc614aa1456c97bbe16706497524cb0f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Aug 2022 17:02:37 +0200 Subject: [PATCH 0800/1030] OP-3713 - fix missing family Resulted in failure in integrate --- .../plugins/publish/collect_color_coded_instances.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index 9adc16d0fd..7d78140c5b 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -62,6 +62,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): publishable_layers = [] created_instances = [] + family_from_settings = None for layer in layers: self.log.debug("Layer:: {}".format(layer)) if layer.parents: @@ -80,6 +81,9 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): self.log.debug("!!! Not found family or template, skip") continue + if not family_from_settings: + family_from_settings = resolved_family + fill_pairs = { "variant": variant, "family": resolved_family, @@ -119,7 +123,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): first_layer = publishable_layers[0] # dummy layer first_layer.name = subset - family = resolved_family # inherit family + family = family_from_settings # inherit family instance = self._create_instance(context, first_layer, family, asset_name, subset, task_name) From 45368a7ba83f0bdb59e9b4e591e6a1fee0736fff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Aug 2022 17:15:09 +0200 Subject: [PATCH 0801/1030] OP-3713 - added more documentation --- .../publish/collect_color_coded_instances.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index 7d78140c5b..f93ba51574 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -9,14 +9,22 @@ from openpype.settings import get_project_settings class CollectColorCodedInstances(pyblish.api.ContextPlugin): - """Creates instances for configured color code of a layer. + """Creates instances for layers marked by configurable color. Used in remote publishing when artists marks publishable layers by color- - coding. + coding. Top level layers (group) must be marked by specific color to be + published as an instance of 'image' family. Can add group for all publishable layers to allow creation of flattened image. (Cannot contain special background layer as it cannot be grouped!) + Based on value `create_flatten_image` from Settings: + - "yes": create flattened 'image' subset of all publishable layers + create + 'image' subset per publishable layer + - "only": create ONLY flattened 'image' subset of all publishable layers + - "no": do not create flattened 'image' subset at all, + only separate subsets per marked layer. + Identifier: id (str): "pyblish.avalon.instance" """ @@ -33,7 +41,6 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): # flattened template cannot subset_template_name = "" create_flatten_image = "no" - # probably not possible to configure this globally flatten_subset_template = "" def process(self, context): From e0c7ba861733e0cf4ec9087fdadcfe6b0d729aea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:16 +0200 Subject: [PATCH 0802/1030] added new plugin which change task status for instances if they are rendered on farm --- .../publish/integrate_ftrack_farm_status.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py new file mode 100644 index 0000000000..ecf258a870 --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -0,0 +1,129 @@ +import pyblish.api +from openpype.lib import profiles_filtering + + +class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): + """Change task status when should be published on farm. + + Instance which has set "farm" key in data to 'True' is considered as will + be rendered on farm thus it's status should be changed. + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = "Integrate Ftrack Component" + families = ["ftrack"] + + farm_status_profiles = [] + + def process(self, context): + # Quick end + if not self.farm_status_profiles: + project_name = context.data["projectName"] + self.log.info(( + "Status profiles are not filled for project \"{}\". Skipping" + ).format(project_name)) + return + + filtered_instances = self.filter_instances(context) + instances_with_status_names = self.get_instances_with_statuse_names( + context, filtered_instances + ) + if instances_with_status_names: + self.fill_statuses(context, instances_with_status_names) + + def filter_instances(self, context): + filtered_instances = [] + for instance in context: + subset_name = instance.data["subset"] + msg_start = "SKipping instance {}.".format(subset_name) + if not instance.data.get("farm"): + self.log.debug( + "{} Won't be rendered on farm.".format(msg_start) + ) + continue + + task_entity = instance.data.get("ftrackTask") + if not task_entity: + self.log.debug( + "{} Does not have filled task".format(msg_start) + ) + continue + + filtered_instances.append(instance) + return filtered_instances + + def get_instances_with_statuse_names(self, context, instances): + instances_with_status_names = [] + for instance in instances: + family = instance.data["family"] + subset_name = instance.data["subset"] + task_entity = instance.data["ftrackTask"] + host_name = context.data["hostName"] + task_name = task_entity["name"] + task_type = task_entity["type"]["name"] + status_profile = profiles_filtering( + self.farm_status_profiles, + { + "hosts": host_name, + "task_types": task_type, + "task_names": task_name, + "families": family, + "subsets": subset_name, + }, + logger=self.log + ) + if not status_profile: + # There already is log in 'profiles_filtering' + continue + + status_name = status_profile["status_name"] + if status_name: + instances_with_status_names.append((instance, status_name)) + return instances_with_status_names + + def fill_statuses(self, context, instances_with_status_names): + # Prepare available task statuses on the project + project_name = context.data["projectName"] + session = context.data["ftrackSession"] + project_entity = session.query(( + "select project_schema from Project where full_name is \"{}\"" + ).format(project_name)).one() + project_schema = project_entity["project_schema"] + task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + + # Keep track if anything has changed + status_changed = False + found_status_id_by_status_name = {} + for item in instances_with_status_names: + instance, status_name = item + + status_name_low = status_name.lower() + status_id = found_status_id_by_status_name.get(status_name_low) + + if status_id is None: + # Skip if status name was already tried to be found + if status_name_low in found_status_id_by_status_name: + continue + + for status in task_workflow_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break + + # Store the result to be reused in following instances + found_status_id_by_status_name[status_name_low] = status_id + + if status_id is None: + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"" + ).format(status_name, project_name)) + continue + + # Change task status id + task_entity = instance.data["ftrackTask"] + if status_id != task_entity["status_id"]: + task_entity["status_id"] = status_id + status_changed = True + + if status_changed: + session.commit() From 41dd9e84f574663aef840596fa4e4c8a37a6a49b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:27 +0200 Subject: [PATCH 0803/1030] added settings schema for new plugin --- .../defaults/project_settings/ftrack.json | 3 + .../schema_project_ftrack.json | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..610c85d232 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -489,6 +489,9 @@ }, "keep_first_subset_name_for_review": true, "asset_versions_status_profiles": [] + }, + "IntegrateFtrackFarmStatus": { + "farm_status_profiles": [] } } } \ No newline at end of file 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..a821b1de76 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1003,6 +1003,66 @@ } } ] + }, + { + "type": "dict", + "key": "IntegrateFtrackFarmStatus", + "label": "Integrate Ftrack Farm Status", + "children": [ + { + "type": "label", + "label": "Change status of task when it's subset is rendered on farm" + }, + { + "type": "list", + "collapsible": true, + "key": "farm_status_profiles", + "label": "Farm status profiles", + "use_label_wrap": true, + "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" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status_name", + "label": "Status name", + "type": "text" + } + ] + } + } + ] } ] } From b36b8ebee0ae424ab1189bc3d9629ef672097bb4 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 17 Aug 2022 04:12:09 +0000 Subject: [PATCH 0804/1030] [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" From ccaef43535dd0d80c3184a325f51bfaea8409d75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:32:11 +0200 Subject: [PATCH 0805/1030] changed description label --- .../entities/schemas/projects_schema/schema_project_ftrack.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a821b1de76..1967a1150f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1011,7 +1011,7 @@ "children": [ { "type": "label", - "label": "Change status of task when it's subset is rendered on farm" + "label": "Change status of task when it's subset is submitted to farm" }, { "type": "list", From 8d65c65fc9ebcccc8d58fe3b55e7cb81b4706106 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:34:00 +0300 Subject: [PATCH 0806/1030] Remove unused attribute. Co-authored-by: Roy Nieterau --- 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 2f09aaee87..668cb57292 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -71,7 +71,6 @@ class CreateRender(plugin.Creator): label = "Render" family = "rendering" icon = "eye" - enable_all_lights = True _token = None _user = None _password = None From 41ac0d65c4c7fe145cf4347e1faf5af6b5b7dfa6 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:35:10 +0300 Subject: [PATCH 0807/1030] Fix bug in default. Co-authored-by: Roy Nieterau --- 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 668cb57292..5418ec1f2f 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -223,7 +223,7 @@ class CreateRender(plugin.Creator): self._project_settings.get( "maya", {}).get( "RenderSettings", {}).get( - "enable_all_lights", {}) + "enable_all_lights", False) ) # Disable for now as this feature is not working yet # self.data["assScene"] = False From 7deb3079247f56ba606b008c462099f18a73ae74 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:35:26 +0300 Subject: [PATCH 0808/1030] Fix bug in default. Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/validate_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 93ef7d7af7..f19c0bff36 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -245,7 +245,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): settings_lights_flag = instance.context.data["project_settings"].get( "maya", {}).get( "RenderSettings", {}).get( - "enable_all_lights", {}) + "enable_all_lights", False) instance_lights_flag = instance.data.get("renderSetupIncludeLights") if settings_lights_flag != instance_lights_flag: From 2f3e6a73e3f8d130fc639cd1c5c1429e4957ea2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:48:50 +0200 Subject: [PATCH 0809/1030] Change label of plugin --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index ecf258a870..f725de3144 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -10,7 +10,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): """ order = pyblish.api.IntegratorOrder + 0.48 - label = "Integrate Ftrack Component" + label = "Integrate Ftrack Farm Status" families = ["ftrack"] farm_status_profiles = [] @@ -35,7 +35,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: subset_name = instance.data["subset"] - msg_start = "SKipping instance {}.".format(subset_name) + msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): self.log.debug( "{} Won't be rendered on farm.".format(msg_start) From 7095bff502f13498bb1dd7a7a173693bf43e72dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:04:58 +0200 Subject: [PATCH 0810/1030] set "farm" to true in maya render colletor --- 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 c3e6c98020..0d45ad4f9e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -354,6 +354,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance = context.create_instance(expected_layer_name) instance.data["label"] = label + instance.data["farm"] = True instance.data.update(data) self.log.debug("data: {}".format(json.dumps(data, indent=4))) From 58f19f15f4a2c3d4ad6d0dd71089c0357904dcd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:05:04 +0200 Subject: [PATCH 0811/1030] skip disabled instances --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index f725de3144..fcbe71e0ac 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -34,6 +34,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): def filter_instances(self, context): filtered_instances = [] for instance in context: + # Skip disabled instances + if instance.data.get("publish") is False: + continue subset_name = instance.data["subset"] msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): From 346e3b8300e01ac8b3ab4e2c52a7d0c25a169d33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:11:39 +0200 Subject: [PATCH 0812/1030] removed families filter --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index fcbe71e0ac..24f784f83d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -11,7 +11,6 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.48 label = "Integrate Ftrack Farm Status" - families = ["ftrack"] farm_status_profiles = [] From 4dbca722bac4a918fa992a0860361460338ef970 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 17 Aug 2022 11:14:29 +0200 Subject: [PATCH 0813/1030] OP-3713 - refactored keys and labels --- .../plugins/publish/collect_color_coded_instances.py | 2 +- .../schemas/projects_schema/schema_project_photoshop.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index f93ba51574..c157c932fd 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -109,7 +109,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): "Subset {} already created, skipping.".format(subset)) continue - if self.create_flatten_image != "only": + if self.create_flatten_image != "flatten_only": instance = self._create_instance(context, layer, resolved_family, asset_name, subset, task_name) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 6935ec8e5e..db06147a51 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -50,9 +50,9 @@ "type": "enum", "multiselection": false, "enum_items": [ - { "yes": "Yes" }, - { "no": "No" }, - { "only": "Only flatten" } + { "flatten_with_images": "Flatten with images" }, + { "flatten_only": "Flatten only" }, + { "no": "No" } ] }, { From 95c19cc412ecbfd0caf42e06ada91640e8da5885 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:22:56 +0200 Subject: [PATCH 0814/1030] fill context entities in all instances --- .../plugins/publish/collect_ftrack_api.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 14da188150..99a555014e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -105,11 +105,17 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity - self.per_instance_process(context, asset_name, task_name) + self.per_instance_process(context, asset_entity, task_entity) def per_instance_process( - self, context, context_asset_name, context_task_name + self, context, context_asset_entity, context_task_entity ): + context_task_name = None + context_asset_name = None + if context_asset_entity: + context_asset_name = context_asset_entity["name"] + if context_task_entity: + context_task_name = context_task_entity["name"] instance_by_asset_and_task = {} for instance in context: self.log.debug( @@ -120,6 +126,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): if not instance_asset_name and not instance_task_name: self.log.debug("Instance does not have set context keys.") + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue elif instance_asset_name and instance_task_name: @@ -131,6 +139,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context is same as in publish context." " Asset: {} | Task: {}" ).format(context_asset_name, context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = instance_asset_name task_name = instance_task_name @@ -141,6 +151,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context task is same as in publish" " context. Task: {}" ).format(context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = context_asset_name @@ -152,6 +164,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context asset is same as in publish" " context. Asset: {}" ).format(context_asset_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = None continue # Do not use context's task name From b178bb538496123fe748d54dc3271ebabc019cfe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 17 Aug 2022 11:49:50 +0200 Subject: [PATCH 0815/1030] OP-3722 - added check for empty context --- openpype/pype_commands.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index a447aa916b..b266479bb1 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -232,6 +232,11 @@ class PypeCommands: fail_batch(_id, dbcon, msg) print("Another batch running, probably stuck, ask admin for help") + if not task_data["context"]: + msg = "Batch manifest must contain context data" + msg += "Create new batch and set context properly." + fail_batch(_id, dbcon, msg) + asset_name, task_name, task_type = get_batch_asset_task_info( task_data["context"]) From 51c27f28c0791633819f935e230a179ea20ff00b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:20:06 +0200 Subject: [PATCH 0816/1030] added ability to add additional metadata to components --- .../publish/integrate_ftrack_instances.py | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index a1e5922730..3f0cc176a2 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,7 @@ import json import copy import pyblish.api +from openpype.lib.openpype_version import get_openpype_version from openpype.lib.transcoding import ( get_ffprobe_streams, convert_ffprobe_fps_to_float, @@ -20,6 +21,17 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): label = "Integrate Ftrack Component" families = ["ftrack"] + metadata_keys_to_label = { + "openpype_version": "OpenPype version", + "frame_start": "Frame start", + "frame_end": "Frame end", + "duration": "Duration", + "width": "Resolution width", + "height": "Resolution height", + "fps": "FPS", + "code": "Codec" + } + family_mapping = { "camera": "cam", "look": "look", @@ -43,6 +55,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): } keep_first_subset_name_for_review = True asset_versions_status_profiles = {} + additional_metadata_keys = [] def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -105,7 +118,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "component_data": None, "component_path": None, "component_location": None, - "component_location_name": None + "component_location_name": None, + "additional_data": {} } # Filter types of representations @@ -152,6 +166,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "name": "thumbnail" } thumbnail_item["thumbnail"] = True + # Create copy of item before setting location src_components_to_add.append(copy.deepcopy(thumbnail_item)) # Create copy of first thumbnail @@ -248,19 +263,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component[ "asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, repre_path, True - ) - # Change location review_item["component_path"] = repre_path # Change component data review_item["component_data"] = { # Default component name is "main". "name": "ftrackreview-mp4", - "metadata": { - "ftr_meta": json.dumps(component_meta) - } + "metadata": self._prepare_component_metadata( + instance, repre, repre_path, True + ) } if is_first_review_repre: @@ -302,13 +313,9 @@ 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( + component_data["metadata"] = 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 @@ -326,16 +333,12 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ): other_item["asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, published_path, False - ) component_data = { - "name": repre["name"] + "name": repre["name"], + "metadata": self._prepare_component_metadata( + instance, repre, published_path, False + ) } - 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 @@ -354,6 +357,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): )) instance.data["ftrackComponentsList"] = component_list + def _collect_additional_metadata(self, streams): + pass + def _get_repre_path(self, instance, repre, only_published): """Get representation path that can be used for integration. @@ -423,6 +429,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): def _prepare_component_metadata( self, instance, repre, component_path, is_review ): + metadata = {} + if "openpype_version" in self.additional_metadata_keys: + label = self.metadata_keys_to_label["openpype_version"] + metadata[label] = get_openpype_version() + extension = os.path.splitext(component_path)[-1] streams = [] try: @@ -442,13 +453,23 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # - 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 {} + return metadata stream_width = None stream_height = None stream_fps = None frame_out = None + codec_label = None for video_stream in video_streams: + codec_label = video_stream.get("codec_long_name") + if not codec_label: + codec_label = video_stream.get("codec") + + if codec_label: + pix_fmt = video_stream.get("pix_fmt") + if pix_fmt: + codec_label += " ({})".format(pix_fmt) + tmp_width = video_stream.get("width") tmp_height = video_stream.get("height") if tmp_width and tmp_height: @@ -456,8 +477,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 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: + stream_duration = video_stream.get("duration") + if input_framerate is None or stream_duration is None: continue try: stream_fps = convert_ffprobe_fps_to_float( @@ -473,9 +494,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_height = tmp_height self.log.debug("FPS from stream is {} and duration is {}".format( - input_framerate, duration + input_framerate, stream_duration )) - frame_out = float(duration) * stream_fps + frame_out = float(stream_duration) * stream_fps break # Prepare FPS @@ -483,43 +504,58 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 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 + # Prepare frame ranges + 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"] + duration = (frame_end - frame_start) + 1 + + for key, value in [ + ("fps", fps), + ("frame_start", frame_start), + ("frame_end", frame_end), + ("duration", duration), + ("width", stream_width), + ("height", stream_height), + ("fps", fps), + ("code", codec_label) + ]: + if not value or key not in self.additional_metadata_keys: + continue + label = self.metadata_keys_to_label[key] + metadata[label] = value + + if not is_review: + ftr_meta = {} + if fps: + ftr_meta["frameRate"] = fps + + if stream_width and stream_height: + ftr_meta["width"] = int(stream_width) + ftr_meta["height"] = int(stream_height) + metadata["ftr_meta"] = json.dumps(ftr_meta) + return metadata + # 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 + frame_out = duration # 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 = { + metadata["ftr_meta"] = json.dumps({ "frameIn": 0, "frameOut": frame_out, "frameRate": float(fps) - } - - return component_meta + }) + return metadata From b66c8088c3c9fcde06cdcf6cb837c1deb2c5cc1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:24:01 +0200 Subject: [PATCH 0817/1030] added settings for 'additional_metadata_keys' --- .../defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9847e58cfa..952657251c 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -491,7 +491,8 @@ "usd": "usd" }, "keep_first_subset_name_for_review": true, - "asset_versions_status_profiles": [] + "asset_versions_status_profiles": [], + "additional_metadata_keys": [] } } } \ No newline at end of file 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 3f472c6c6a..1a63e589b2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1039,6 +1039,22 @@ } ] } + }, + { + "key": "additional_metadata_keys", + "label": "Additional metadata keys on components", + "type": "enum", + "multiselection": true, + "enum_items": [ + {"openpype_version": "OpenPype version"}, + {"frame_start": "Frame start"}, + {"frame_end": "Frame end"}, + {"duration": "Duration"}, + {"width": "Resolution width"}, + {"height": "Resolution height"}, + {"fps": "FPS"}, + {"code": "Codec"} + ] } ] } From 05f1b732b6edd1732139350528ad614095da5b70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:22:34 +0200 Subject: [PATCH 0818/1030] fill context task entity in collect ftrack api --- openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 99a555014e..e13b7e65cd 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -165,7 +165,7 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): " context. Asset: {}" ).format(context_asset_name)) instance.data["ftrackEntity"] = context_asset_entity - instance.data["ftrackTask"] = None + instance.data["ftrackTask"] = context_task_entity continue # Do not use context's task name From 4dba68c5bdade98048dd1ca15d7f03ac004dcf28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:14 +0200 Subject: [PATCH 0819/1030] fix function import and call --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 24f784f83d..0a7ad0b532 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -1,5 +1,5 @@ import pyblish.api -from openpype.lib import profiles_filtering +from openpype.lib import filter_profiles class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): @@ -63,7 +63,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): host_name = context.data["hostName"] task_name = task_entity["name"] task_type = task_entity["type"]["name"] - status_profile = profiles_filtering( + status_profile = filter_profiles( self.farm_status_profiles, { "hosts": host_name, @@ -75,7 +75,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): logger=self.log ) if not status_profile: - # There already is log in 'profiles_filtering' + # There already is log in 'filter_profiles' continue status_name = status_profile["status_name"] From cb34f4619e54ae887bc1ea38a0e1ec106d228167 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:20 +0200 Subject: [PATCH 0820/1030] log availabl status names --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 0a7ad0b532..8bebfd8485 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -93,6 +93,10 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_schema = project_entity["project_schema"] task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_workflow_statuses + }) # Keep track if anything has changed status_changed = False found_status_id_by_status_name = {} @@ -117,8 +121,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id is None: self.log.warning(( - "Status \"{}\" is not available on project \"{}\"" - ).format(status_name, project_name)) + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id From bc3aa4b1609e067e7b4a31a9874e8a415bcfcc71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:54:06 +0200 Subject: [PATCH 0821/1030] fix getting of task statuses --- .../plugins/publish/integrate_ftrack_farm_status.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 8bebfd8485..658df70895 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,12 +90,17 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() + task_type = session.query( + "select id from ObjectType where name is \"Task\"" + ).first() project_schema = project_entity["project_schema"] - task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + task_statuses = project_schema.get_statuses( + "Task", task_type["id"] + ) joined_status_names = ", ".join({ '"{}"'.format(status["name"]) - for status in task_workflow_statuses + for status in task_statuses }) # Keep track if anything has changed status_changed = False @@ -111,7 +116,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_name_low in found_status_id_by_status_name: continue - for status in task_workflow_statuses: + for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] break From 671cf183fd73629e7a140784e518bc7718fa5431 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:18:13 +0200 Subject: [PATCH 0822/1030] fix statuses lookup by task type --- .../publish/integrate_ftrack_farm_status.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 658df70895..c5fc3dd68f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,49 +90,49 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() - task_type = session.query( - "select id from ObjectType where name is \"Task\"" - ).first() project_schema = project_entity["project_schema"] - task_statuses = project_schema.get_statuses( - "Task", task_type["id"] - ) - joined_status_names = ", ".join({ - '"{}"'.format(status["name"]) - for status in task_statuses - }) + task_type_ids = set() + for item in instances_with_status_names: + instance, _ = item + task_entity = instance.data["ftrackTask"] + task_type_ids.add(task_entity["type"]["id"]) + + task_statuses_by_type_id = { + task_type_id: project_schema.get_statuses("Task", task_type_id) + for task_type_id in task_type_ids + } + # Keep track if anything has changed + skipped_status_names = set() status_changed = False - found_status_id_by_status_name = {} for item in instances_with_status_names: instance, status_name = item - + task_entity = instance.data["ftrackTask"] + task_statuses = task_statuses_by_type_id[task_entity["type"]["id"]] status_name_low = status_name.lower() - status_id = found_status_id_by_status_name.get(status_name_low) + + status_id = None + # Skip if status name was already tried to be found + for status in task_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break if status_id is None: - # Skip if status name was already tried to be found - if status_name_low in found_status_id_by_status_name: - continue - - for status in task_statuses: - if status["name"].lower() == status_name_low: - status_id = status["id"] - break - - # Store the result to be reused in following instances - found_status_id_by_status_name[status_name_low] = status_id - - if status_id is None: - self.log.warning(( - "Status \"{}\" is not available on project \"{}\"." - " Available statuses are {}" - ).format(status_name, project_name, joined_status_names)) + if status_name_low not in skipped_status_names: + skipped_status_names.add(status_name_low) + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_statuses + }) + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id - task_entity = instance.data["ftrackTask"] if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True From 6d4a80cd30b8adf926a44b46c2c8f70ee04217f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:28:27 +0200 Subject: [PATCH 0823/1030] added some logs related to status changes --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index c5fc3dd68f..ab5738c33f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -113,10 +113,12 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): status_name_low = status_name.lower() status_id = None + status_name = None # Skip if status name was already tried to be found for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] + status_name = status["name"] break if status_id is None: @@ -136,6 +138,13 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True + path = "/".join([ + item["name"] + for item in task_entity["link"] + ]) + self.log.debug("Set status \"{}\" to \"{}\"".format( + status_name, path + )) if status_changed: session.commit() From 5a0b15c63b90a97417d43d9a3cfff7ba927dd4e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 15:35:46 +0200 Subject: [PATCH 0824/1030] fix typo in codec --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 3f0cc176a2..1bf4caac77 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -29,7 +29,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "width": "Resolution width", "height": "Resolution height", "fps": "FPS", - "code": "Codec" + "codec": "Codec" } family_mapping = { @@ -526,7 +526,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ("width", stream_width), ("height", stream_height), ("fps", fps), - ("code", codec_label) + ("codec", codec_label) ]: if not value or key not in self.additional_metadata_keys: continue From db6f46895b9c2a3659bfb5803705388b7d2f7dfd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 17 Aug 2022 16:01:31 +0200 Subject: [PATCH 0825/1030] OP-3723 - remove PIL limit High resolution could trigger " could be decompression bomb DOS attack". --- openpype/hosts/photoshop/plugins/publish/extract_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 7f78a46527..151440b914 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -144,6 +144,7 @@ class ExtractReview(openpype.api.Extractor): used as a source for thumbnail or review mov. """ max_ffmpeg_size = 16384 + Image.MAX_IMAGE_PIXELS = None first_url = os.path.join(staging_dir, processed_img_names[0]) with Image.open(first_url) as im: width, height = im.size From 5484c083230cbc3090db1b2dba6d582d21a1f849 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 16:58:41 +0200 Subject: [PATCH 0826/1030] context label collector does not require 'currentFile' to be filled --- .../plugins/publish/collect_context_label.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/collect_context_label.py b/openpype/plugins/publish/collect_context_label.py index 8cf71882aa..0ca19b28c1 100644 --- a/openpype/plugins/publish/collect_context_label.py +++ b/openpype/plugins/publish/collect_context_label.py @@ -1,5 +1,6 @@ """ -Requires: +Optional: + context -> hostName (str) context -> currentFile (str) Provides: context -> label (str) @@ -16,16 +17,16 @@ class CollectContextLabel(pyblish.api.ContextPlugin): label = "Context Label" def process(self, context): + host_name = context.data.get("hostName") + if not host_name: + host_name = pyblish.api.registered_hosts()[-1] + # Use host name as base for label + label = host_name.title() - # Get last registered host - host = pyblish.api.registered_hosts()[-1] - - # Get scene name from "currentFile" - path = context.data.get("currentFile") or "" - base = os.path.basename(path) + # Get scene name from "currentFile" and use basename as ending of label + path = context.data.get("currentFile") + if path: + label += " - {}".format(os.path.basename(path)) # Set label - label = "{host} - {scene}".format(host=host.title(), scene=base) - if host == "standalonepublisher": - label = host.title() context.data["label"] = label From e3c43d22159b78428c7ee7f75d89f793dc86dba7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 16:59:12 +0200 Subject: [PATCH 0827/1030] it is possible to have set custom context label and in that case the plugin is skipped --- openpype/plugins/publish/collect_context_label.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/collect_context_label.py b/openpype/plugins/publish/collect_context_label.py index 0ca19b28c1..1dec0b380b 100644 --- a/openpype/plugins/publish/collect_context_label.py +++ b/openpype/plugins/publish/collect_context_label.py @@ -17,6 +17,12 @@ class CollectContextLabel(pyblish.api.ContextPlugin): label = "Context Label" def process(self, context): + # Add ability to use custom context label + context_label = context.data.get("contextLabel") + if context_label: + context.data["label"] = context_label + return + host_name = context.data.get("hostName") if not host_name: host_name = pyblish.api.registered_hosts()[-1] From 09af23e2d789dcb02450cbd6eed9be53c062c416 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Aug 2022 17:26:10 +0200 Subject: [PATCH 0828/1030] resolve: fixing import in collector --- .../hosts/resolve/plugins/publish/precollect_workfile.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 53e67aee0e..0f94216556 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,11 +1,9 @@ import pyblish.api from pprint import pformat -from importlib import reload -from openpype.hosts import resolve +from openpype.hosts.resolve import api as rapi from openpype.pipeline import legacy_io from openpype.hosts.resolve.otio import davinci_export -reload(davinci_export) class PrecollectWorkfile(pyblish.api.ContextPlugin): @@ -18,9 +16,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): asset = legacy_io.Session["AVALON_ASSET"] subset = "workfile" - project = resolve.get_current_project() + project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") - video_tracks = resolve.get_video_track_names() + video_tracks = rapi.get_video_track_names() # adding otio timeline to context otio_timeline = davinci_export.create_otio_timeline(project) From ebdf8a348a3eb4e34162564c719a872b5e30b71e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 17 Aug 2022 17:41:23 +0200 Subject: [PATCH 0829/1030] OP-3723 - changed max limit Official 16384x16384 actually didn't work because int overflow. 16000 tested and worked. --- openpype/hosts/photoshop/plugins/publish/extract_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 151440b914..64decbb957 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -143,7 +143,8 @@ class ExtractReview(openpype.api.Extractor): Ffmpeg has max size 16384x16384. Saved image(s) must be resized to be used as a source for thumbnail or review mov. """ - max_ffmpeg_size = 16384 + # 16384x16384 actually didn't work because int overflow + max_ffmpeg_size = 16000 Image.MAX_IMAGE_PIXELS = None first_url = os.path.join(staging_dir, processed_img_names[0]) with Image.open(first_url) as im: From fe278d7135998a368db562eabeb5a636ce56e0ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 18:05:42 +0200 Subject: [PATCH 0830/1030] Don't force to have label on all instances and in context. --- openpype/tools/pyblish_pype/control.py | 1 - openpype/tools/pyblish_pype/model.py | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openpype/tools/pyblish_pype/control.py b/openpype/tools/pyblish_pype/control.py index f657936b79..05e53a989a 100644 --- a/openpype/tools/pyblish_pype/control.py +++ b/openpype/tools/pyblish_pype/control.py @@ -244,7 +244,6 @@ class Controller(QtCore.QObject): self.context.optional = False self.context.data["publish"] = True - self.context.data["label"] = "Context" self.context.data["name"] = "context" self.context.data["host"] = reversed(pyblish.api.registered_hosts()) diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 31aa63677e..309126a884 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -596,11 +596,6 @@ class InstanceItem(QtGui.QStandardItem): instance._logs = [] instance.optional = getattr(instance, "optional", True) instance.data["publish"] = instance.data.get("publish", True) - instance.data["label"] = ( - instance.data.get("label") - or getattr(instance, "label", None) - or instance.data["name"] - ) family = self.data(Roles.FamiliesRole)[0] self.setData( @@ -616,9 +611,19 @@ class InstanceItem(QtGui.QStandardItem): def data(self, role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.DisplayRole: + label = None if settings.UseLabel: - return self.instance.data["label"] - return self.instance.data["name"] + label = ( + self.instance.data.get("label") + or getattr(self.instance, "label", None) + ) + + if not label: + if self.is_context: + label = "Context" + else: + label = self.instance.data["name"] + return label if role == QtCore.Qt.DecorationRole: icon_name = self.instance.data.get("icon") or "file" From 55bf1bea91bda6c01a7d47f7c2437b80a4ccfc58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 18:06:05 +0200 Subject: [PATCH 0831/1030] change label access in report --- openpype/tools/publisher/publish_report_viewer/report_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/report_items.py b/openpype/tools/publisher/publish_report_viewer/report_items.py index 8a01569723..206f999bac 100644 --- a/openpype/tools/publisher/publish_report_viewer/report_items.py +++ b/openpype/tools/publisher/publish_report_viewer/report_items.py @@ -79,7 +79,7 @@ class PublishReport: context_data = data["context"] context_data["name"] = "context" - context_data["label"] = context_data["label"] or "Context" + context_data["label"] = context_data.get("label") or "Context" logs = [] plugins_items_by_id = {} From 6761aa7d68016ad0e319ddae56c956d656c8bd44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 18:07:30 +0200 Subject: [PATCH 0832/1030] Change the check of "label" key --- openpype/plugins/publish/collect_context_label.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_context_label.py b/openpype/plugins/publish/collect_context_label.py index 1dec0b380b..6cdeba8418 100644 --- a/openpype/plugins/publish/collect_context_label.py +++ b/openpype/plugins/publish/collect_context_label.py @@ -18,9 +18,11 @@ class CollectContextLabel(pyblish.api.ContextPlugin): def process(self, context): # Add ability to use custom context label - context_label = context.data.get("contextLabel") - if context_label: - context.data["label"] = context_label + label = context.data.get("label") + if label: + self.log.debug("Context label is already set to \"{}\"".format( + label + )) return host_name = context.data.get("hostName") @@ -36,3 +38,6 @@ class CollectContextLabel(pyblish.api.ContextPlugin): # Set label context.data["label"] = label + self.log.debug("Context label is changed to \"{}\"".format( + label + )) From da3268c9a75e04a8464589fc1c1153e264fec60a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 09:51:45 +0200 Subject: [PATCH 0833/1030] resave default settings --- openpype/settings/defaults/project_settings/ftrack.json | 2 +- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 58b6a55958..2d5f889aa5 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -498,4 +498,4 @@ "farm_status_profiles": [] } } -} +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 83b6f69074..774bce714b 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -19,4 +19,4 @@ "step": "step" } } -} +} \ No newline at end of file From ec405eb9130c6fe9c8b13dde34983fb43341507d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:52:46 +0200 Subject: [PATCH 0834/1030] prepared some classes to handle settings locks --- openpype/settings/handlers.py | 162 ++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 15ae2351fd..8d13875d0b 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -22,6 +22,168 @@ from .constants import ( ) +class SettingsStateInfo: + """Helper state information for Settings state. + + Is used to hold information about last save and last opened UI. Keep + information about the time when that happened and on which machine under + which user. + + To create currrent machine and time information use 'create_new' method. + """ + + timestamp_format = "%Y-%m-%d %H:%M:%S.%f" + + def __init__( + self, timestamp, hostname, hostip, username, system_name, local_id + ): + self.timestamp = timestamp + self._timestamp_obj = datetime.datetime.strptime( + timestamp, self.timestamp_format + ) + self.hostname = hostname + self.hostip = hostip + self.username = username + self.system_name = system_name + self.local_id = local_id + + def copy(self): + return self.from_data(self.to_data()) + + @property + def timestamp_obj(self): + return self._timestamp_obj + + @classmethod + def create_new(cls): + """Create information about this machine for current time.""" + + from openpype.lib.pype_info import get_workstation_info + + now = datetime.datetime.now() + workstation_info = get_workstation_info() + + return cls( + now.strftime(cls.timestamp_format), + workstation_info["hostname"], + workstation_info["hostip"], + workstation_info["username"], + workstation_info["system_name"], + workstation_info["local_id"] + ) + + @classmethod + def from_data(cls, data): + """Create object from data.""" + + return cls( + data["timestamp"], + data["hostname"], + data["hostip"], + data["username"], + data["system_name"], + data["local_id"] + ) + + def to_data(self): + return { + "timestamp": self.timestamp, + "hostname": self.hostname, + "hostip": self.hostip, + "username": self.username, + "system_name": self.system_name, + "local_id": self.local_id, + } + + def __eq__(self, other): + if not isinstance(other, SettingsStateInfo): + return False + + if other.timestamp_obj != self.timestamp_obj: + return False + + return ( + self.hostname == other.hostname + and self.hostip == other.hostip + and self.username == other.username + and self.system_name == other.system_name + and self.local_id == other.local_id + ) + + +class SettingsState: + """State of settings with last saved and last opened. + + Args: + openpype_version (str): OpenPype version string. + settings_type (str): Type of settings. System or project settings. + last_saved_info (Union[None, SettingsStateInfo]): Information about + machine and time when were settings saved last time. + last_opened_info (Union[None, SettingsStateInfo]): This is settings UI + specific information similar to last saved describes who had opened + settings as last. + project_name (Union[None, str]): Identifier for project settings. + """ + + def __init__( + self, + openpype_version, + settings_type, + last_saved_info, + last_opened_info, + project_name=None + ): + self.openpype_version = openpype_version + self.settings_type = settings_type + self.last_saved_info = last_saved_info + self.last_opened_info = last_opened_info + self.project_name = project_name + + def __eq__(self, other): + if not isinstance(other, SettingsState): + return False + + return ( + self.openpype_version == other.openpype_version + and self.settings_type == other.settings_type + and self.last_saved_info == other.last_saved_info + and self.last_opened_info == other.last_opened_info + and self.project_name == other.project_name + ) + + def copy(self): + return self.__class__( + self.openpype_version, + self.settings_type, + self.last_saved_info.copy(), + self.last_opened_info.copy(), + self.project_name + ) + + def on_save(self, openpype_version): + self.openpype_version = openpype_version + self.last_saved_info = SettingsStateInfo.create_new() + + @classmethod + def from_document(cls, openpype_version, settings_type, document): + document = document or {} + last_saved_info = document.get("last_saved_info") + if last_saved_info: + last_saved_info = SettingsStateInfo.from_data(last_saved_info) + + last_opened_info = document.get("last_opened_info") + if last_opened_info: + last_opened_info = SettingsStateInfo.from_data(last_opened_info) + + return cls( + openpype_version, + settings_type, + last_saved_info, + last_opened_info, + document.get("project_name") + ) + + @six.add_metaclass(ABCMeta) class SettingsHandler: @abstractmethod From 17957a760c4c75ded01e944605f164ce058b252c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:53:24 +0200 Subject: [PATCH 0835/1030] 'update_data' and 'update_from_document' always require version --- openpype/settings/handlers.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 8d13875d0b..5a0a30e4a6 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -453,13 +453,12 @@ class CacheValues: return {} return copy.deepcopy(self.data) - def update_data(self, data, version=None): + def update_data(self, data, version): self.data = data self.creation_time = datetime.datetime.now() - if version is not None: - self.version = version + self.version = version - def update_from_document(self, document, version=None): + def update_from_document(self, document, version): data = {} if document: if "data" in document: @@ -468,9 +467,9 @@ class CacheValues: value = document["value"] if value: data = json.loads(value) + self.data = data - if version is not None: - self.version = version + self.version = version def to_json_string(self): return json.dumps(self.data or {}) @@ -1567,7 +1566,7 @@ class MongoLocalSettingsHandler(LocalSettingsHandler): """ data = data or {} - self.local_settings_cache.update_data(data) + self.local_settings_cache.update_data(data, None) self.collection.replace_one( { @@ -1590,6 +1589,6 @@ class MongoLocalSettingsHandler(LocalSettingsHandler): "site_id": self.local_site_id }) - self.local_settings_cache.update_from_document(document) + self.local_settings_cache.update_from_document(document, None) return self.local_settings_cache.data_copy() From 0dc4f1a78622edf932d8623b8ecafdae8c18afc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 11:53:49 +0200 Subject: [PATCH 0836/1030] cache also can have settings state --- openpype/settings/handlers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 5a0a30e4a6..0ee1f74692 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -447,6 +447,7 @@ class CacheValues: self.data = None self.creation_time = None self.version = None + self.settings_state = None def data_copy(self): if not self.data: @@ -458,6 +459,9 @@ class CacheValues: self.creation_time = datetime.datetime.now() self.version = version + def update_settings_state(self, settings_state): + self.settings_state = settings_state + def update_from_document(self, document, version): data = {} if document: From 2b62f28e903e524e66d9520a86ea21ae3df81283 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:27:13 +0200 Subject: [PATCH 0837/1030] fix 'get_representations_parents' function to be able handle hero versions --- openpype/client/entities.py | 88 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 67ddb09ddb..f1f1d30214 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1259,58 +1259,64 @@ def get_representations_parents(project_name, representations): dict[ObjectId, tuple]: Parents by representation id. """ - repres_by_version_id = collections.defaultdict(list) - versions_by_version_id = {} - versions_by_subset_id = collections.defaultdict(list) - subsets_by_subset_id = {} - subsets_by_asset_id = collections.defaultdict(list) + repre_docs_by_version_id = collections.defaultdict(list) + version_docs_by_version_id = {} + version_docs_by_subset_id = collections.defaultdict(list) + subset_docs_by_subset_id = {} + subset_docs_by_asset_id = collections.defaultdict(list) output = {} - for representation in representations: - repre_id = representation["_id"] + for repre_doc in representations: + repre_id = repre_doc["_id"] + version_id = repre_doc["parent"] output[repre_id] = (None, None, None, None) - version_id = representation["parent"] - repres_by_version_id[version_id].append(representation) + repre_docs_by_version_id[version_id].append(repre_doc) - versions = get_versions( - project_name, version_ids=repres_by_version_id.keys() + version_docs = get_versions( + project_name, + version_ids=repre_docs_by_version_id.keys(), + hero=True ) - for version in versions: - version_id = version["_id"] - subset_id = version["parent"] - versions_by_version_id[version_id] = version - versions_by_subset_id[subset_id].append(version) + for version_doc in version_docs: + version_id = version_doc["_id"] + subset_id = version_doc["parent"] + version_docs_by_version_id[version_id] = version_doc + version_docs_by_subset_id[subset_id].append(version_doc) - subsets = get_subsets( - project_name, subset_ids=versions_by_subset_id.keys() + subset_docs = get_subsets( + project_name, subset_ids=version_docs_by_subset_id.keys() ) - for subset in subsets: - subset_id = subset["_id"] - asset_id = subset["parent"] - subsets_by_subset_id[subset_id] = subset - subsets_by_asset_id[asset_id].append(subset) + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + asset_id = subset_doc["parent"] + subset_docs_by_subset_id[subset_id] = subset_doc + subset_docs_by_asset_id[asset_id].append(subset_doc) - assets = get_assets(project_name, asset_ids=subsets_by_asset_id.keys()) - assets_by_id = { - asset["_id"]: asset - for asset in assets + asset_docs = get_assets( + project_name, asset_ids=subset_docs_by_asset_id.keys() + ) + asset_docs_by_id = { + asset_doc["_id"]: asset_doc + for asset_doc in asset_docs } - project = get_project(project_name) + project_doc = get_project(project_name) - for version_id, representations in repres_by_version_id.items(): - asset = None - subset = None - version = versions_by_version_id.get(version_id) - if version: - subset_id = version["parent"] - subset = subsets_by_subset_id.get(subset_id) - if subset: - asset_id = subset["parent"] - asset = assets_by_id.get(asset_id) + for version_id, repre_docs in repre_docs_by_version_id.items(): + asset_doc = None + subset_doc = None + version_doc = version_docs_by_version_id.get(version_id) + if version_doc: + subset_id = version_doc["parent"] + subset_doc = subset_docs_by_subset_id.get(subset_id) + if subset_doc: + asset_id = subset_doc["parent"] + asset_doc = asset_docs_by_id.get(asset_id) - for representation in representations: - repre_id = representation["_id"] - output[repre_id] = (version, subset, asset, project) + for repre_doc in repre_docs: + repre_id = repre_doc["_id"] + output[repre_id] = ( + version_doc, subset_doc, asset_doc, project_doc + ) return output From ce746737154e5c7b12f9a0da5ef47b0edd911f64 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:28:10 +0200 Subject: [PATCH 0838/1030] Be explicit in error message what is missing --- openpype/pipeline/load/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 9945e1fce4..99d6876d4b 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -222,13 +222,20 @@ def get_representation_context(representation): project_name, representation ) + if not representation: + raise AssertionError("Representation was not found in database") + version, subset, asset, project = get_representation_parents( project_name, representation ) - - assert all([representation, version, subset, asset, project]), ( - "This is a bug" - ) + if not version: + raise AssertionError("Version was not found in database") + if not subset: + raise AssertionError("Subset was not found in database") + if not asset: + raise AssertionError("Asset was not found in database") + if not project: + raise AssertionError("Project was not found in database") context = { "project": { From 9d54333e93afe14b3686cc429009632cf1f24f00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:28:54 +0200 Subject: [PATCH 0839/1030] load error can handle invalid hero version --- openpype/tools/loader/widgets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 2d8b4b048d..597c35e89b 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -1547,6 +1547,11 @@ def _load_representations_by_loader(loader, repre_contexts, return for repre_context in repre_contexts.values(): + version_doc = repre_context["version"] + if version_doc["type"] == "hero_version": + version_name = "Hero" + else: + version_name = version_doc.get("name") try: if data_by_repre_id: _id = repre_context["representation"]["_id"] @@ -1564,7 +1569,7 @@ def _load_representations_by_loader(loader, repre_contexts, None, repre_context["representation"]["name"], repre_context["subset"]["name"], - repre_context["version"]["name"] + version_name )) except Exception as exc: @@ -1577,7 +1582,7 @@ def _load_representations_by_loader(loader, repre_contexts, formatted_traceback, repre_context["representation"]["name"], repre_context["subset"]["name"], - repre_context["version"]["name"] + version_name )) return error_info From ac6de74b76dd741152c11d71c2262f605847acd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:33:09 +0200 Subject: [PATCH 0840/1030] handle hero version type in load clip --- openpype/hosts/nuke/plugins/load/load_clip.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index b2dc4a52d7..346773b5af 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -162,7 +162,15 @@ class LoadClip(plugin.NukeLoader): data_imprint = {} for k in add_keys: if k == 'version': - data_imprint[k] = context["version"]['name'] + version_doc = context["version"] + if version_doc["type"] == "hero_version": + version = "hero" + else: + version = version_doc.get("name") + + if version: + data_imprint[k] = version + elif k == 'colorspace': colorspace = repre["data"].get(k) colorspace = colorspace or version_data.get(k) From 0e6ff4a21d224ed188cdf076a4fd00a1a8f696ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 15:59:51 +0200 Subject: [PATCH 0841/1030] cache can be set to outdated --- openpype/settings/handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 0ee1f74692..f6e81a7d0a 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -485,6 +485,9 @@ class CacheValues: delta = (datetime.datetime.now() - self.creation_time).seconds return delta > self.cache_lifetime + def set_outdated(self): + self.create_time = None + class MongoSettingsHandler(SettingsHandler): """Settings handler that use mongo for storing and loading of settings.""" From 4a322f1ceda11843c13627ee5c1f1d9070c82f12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:00:23 +0200 Subject: [PATCH 0842/1030] removed 'SettingsState' and kept only 'SettingsStateInfo' --- openpype/settings/handlers.py | 152 +++++++++++++++------------------- 1 file changed, 66 insertions(+), 86 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index f6e81a7d0a..e4b4bc3dc4 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -23,11 +23,11 @@ from .constants import ( class SettingsStateInfo: - """Helper state information for Settings state. + """Helper state information about some settings state. - Is used to hold information about last save and last opened UI. Keep + Is used to hold information about last saved and last opened UI. Keep information about the time when that happened and on which machine under - which user. + which user and on which openpype version. To create currrent machine and time information use 'create_new' method. """ @@ -35,12 +35,28 @@ class SettingsStateInfo: timestamp_format = "%Y-%m-%d %H:%M:%S.%f" def __init__( - self, timestamp, hostname, hostip, username, system_name, local_id + self, + openpype_version, + settings_type, + project_name, + timestamp, + hostname, + hostip, + username, + system_name, + local_id ): + self.openpype_version = openpype_version + self.settings_type = settings_type + self.project_name = project_name + + timestamp_obj = None + if timestamp: + timestamp_obj = datetime.datetime.strptime( + timestamp, self.timestamp_format + ) self.timestamp = timestamp - self._timestamp_obj = datetime.datetime.strptime( - timestamp, self.timestamp_format - ) + self.timestamp_obj = timestamp_obj self.hostname = hostname self.hostip = hostip self.username = username @@ -50,12 +66,8 @@ class SettingsStateInfo: def copy(self): return self.from_data(self.to_data()) - @property - def timestamp_obj(self): - return self._timestamp_obj - @classmethod - def create_new(cls): + def create_new(cls, openpype_version, settings_type, project_name): """Create information about this machine for current time.""" from openpype.lib.pype_info import get_workstation_info @@ -64,6 +76,9 @@ class SettingsStateInfo: workstation_info = get_workstation_info() return cls( + openpype_version, + settings_type, + project_name, now.strftime(cls.timestamp_format), workstation_info["hostname"], workstation_info["hostip"], @@ -77,6 +92,9 @@ class SettingsStateInfo: """Create object from data.""" return cls( + data["openpype_version"], + data["settings_type"], + data["project_name"], data["timestamp"], data["hostname"], data["hostip"], @@ -86,6 +104,40 @@ class SettingsStateInfo: ) def to_data(self): + data = self.to_document_data() + data.update({ + "openpype_version": self.openpype_version, + "settings_type": self.settings_type, + "project_name": self.project_name + }) + return data + + @classmethod + def from_document(cls, openpype_version, settings_type, document): + document = document or {} + project_name = document.get("project_name") + last_saved_info = document.get("last_saved_info") + if last_saved_info: + copy_last_saved_info = copy.deepcopy(last_saved_info) + copy_last_saved_info.update({ + "openpype_version": openpype_version, + "settings_type": settings_type, + "project_name": project_name, + }) + return cls.from_data(copy_last_saved_info) + return cls( + openpype_version, + settings_type, + project_name, + None, + None, + None, + None, + None, + None + ) + + def to_document_data(self): return { "timestamp": self.timestamp, "hostname": self.hostname, @@ -103,7 +155,8 @@ class SettingsStateInfo: return False return ( - self.hostname == other.hostname + self.openpype_version == other.openpype_version + and self.hostname == other.hostname and self.hostip == other.hostip and self.username == other.username and self.system_name == other.system_name @@ -111,79 +164,6 @@ class SettingsStateInfo: ) -class SettingsState: - """State of settings with last saved and last opened. - - Args: - openpype_version (str): OpenPype version string. - settings_type (str): Type of settings. System or project settings. - last_saved_info (Union[None, SettingsStateInfo]): Information about - machine and time when were settings saved last time. - last_opened_info (Union[None, SettingsStateInfo]): This is settings UI - specific information similar to last saved describes who had opened - settings as last. - project_name (Union[None, str]): Identifier for project settings. - """ - - def __init__( - self, - openpype_version, - settings_type, - last_saved_info, - last_opened_info, - project_name=None - ): - self.openpype_version = openpype_version - self.settings_type = settings_type - self.last_saved_info = last_saved_info - self.last_opened_info = last_opened_info - self.project_name = project_name - - def __eq__(self, other): - if not isinstance(other, SettingsState): - return False - - return ( - self.openpype_version == other.openpype_version - and self.settings_type == other.settings_type - and self.last_saved_info == other.last_saved_info - and self.last_opened_info == other.last_opened_info - and self.project_name == other.project_name - ) - - def copy(self): - return self.__class__( - self.openpype_version, - self.settings_type, - self.last_saved_info.copy(), - self.last_opened_info.copy(), - self.project_name - ) - - def on_save(self, openpype_version): - self.openpype_version = openpype_version - self.last_saved_info = SettingsStateInfo.create_new() - - @classmethod - def from_document(cls, openpype_version, settings_type, document): - document = document or {} - last_saved_info = document.get("last_saved_info") - if last_saved_info: - last_saved_info = SettingsStateInfo.from_data(last_saved_info) - - last_opened_info = document.get("last_opened_info") - if last_opened_info: - last_opened_info = SettingsStateInfo.from_data(last_opened_info) - - return cls( - openpype_version, - settings_type, - last_saved_info, - last_opened_info, - document.get("project_name") - ) - - @six.add_metaclass(ABCMeta) class SettingsHandler: @abstractmethod From 20509b4610250c6d6725c50659b0ce3a065b0e92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:02:46 +0200 Subject: [PATCH 0843/1030] changed 'settings_state' to 'last_saved_info' in cache --- openpype/settings/handlers.py | 44 ++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index e4b4bc3dc4..e34a4c3540 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -368,7 +368,7 @@ class SettingsHandler: """OpenPype versions that have any studio project anatomy overrides. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ pass @@ -379,7 +379,7 @@ class SettingsHandler: """OpenPype versions that have any studio project settings overrides. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ pass @@ -393,8 +393,39 @@ class SettingsHandler: project_name(str): Name of project. Returns: - list: OpenPype versions strings. + List[str]: OpenPype versions strings. """ + + pass + + @abstractmethod + def get_system_last_saved_info(self): + """State of last system settings overrides at the moment when called. + + This method must provide most recent data so using cached data is not + the way. + + Returns: + SettingsStateInfo: Information about system settings overrides. + """ + + pass + + @abstractmethod + def get_project_last_saved_info(self, project_name): + """State of last project settings overrides at the moment when called. + + This method must provide most recent data so using cached data is not + the way. + + Args: + project_name (Union[None, str]): Project name for which state + should be returned. + + Returns: + SettingsStateInfo: Information about project settings overrides. + """ + pass @@ -427,7 +458,7 @@ class CacheValues: self.data = None self.creation_time = None self.version = None - self.settings_state = None + self.last_saved_info = None def data_copy(self): if not self.data: @@ -439,8 +470,8 @@ class CacheValues: self.creation_time = datetime.datetime.now() self.version = version - def update_settings_state(self, settings_state): - self.settings_state = settings_state + def update_last_saved_info(self, last_saved_info): + self.last_saved_info = last_saved_info def update_from_document(self, document, version): data = {} @@ -1288,6 +1319,7 @@ class MongoSettingsHandler(SettingsHandler): self.project_anatomy_cache[project_name].update_from_document( document, version ) + else: project_doc = get_project(project_name) self.project_anatomy_cache[project_name].update_data( From ba434d5f713de2eb478bd1cea533163d482d4033 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:03:49 +0200 Subject: [PATCH 0844/1030] changed how update of settings happens --- openpype/settings/handlers.py | 49 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index e34a4c3540..43b1d37c34 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -696,20 +696,28 @@ class MongoSettingsHandler(SettingsHandler): system_settings_data ) - # Store system settings - self.collection.replace_one( + system_settings_doc = self.collection.find_one( { "type": self._system_settings_key, "version": self._current_version }, - { - "type": self._system_settings_key, - "data": system_settings_data, - "version": self._current_version - }, - upsert=True + {"_id": True} ) + # Store system settings + new_system_settings_doc = { + "type": self._system_settings_key, + "version": self._current_version, + "data": system_settings_data, + } + if not system_settings_doc: + self.collections.insert_one(new_system_settings_doc) + else: + self.collections.update_one( + {"_id": system_settings_doc["_id"]}, + {"$set": new_system_settings_doc} + ) + # Store global settings self.collection.replace_one( { @@ -844,26 +852,33 @@ class MongoSettingsHandler(SettingsHandler): def _save_project_data(self, project_name, doc_type, data_cache): is_default = bool(project_name is None) - replace_filter = { + query_filter = { "type": doc_type, "is_default": is_default, "version": self._current_version } - replace_data = { + last_saved_info = data_cache.last_saved_info + new_project_settings_doc = { "type": doc_type, "data": data_cache.data, "is_default": is_default, - "version": self._current_version + "version": self._current_version, } if not is_default: - replace_filter["project_name"] = project_name - replace_data["project_name"] = project_name + query_filter["project_name"] = project_name + new_project_settings_doc["project_name"] = project_name - self.collection.replace_one( - replace_filter, - replace_data, - upsert=True + project_settings_doc = self.collection.find_one( + query_filter, + {"_id": True} ) + if project_settings_doc: + self.collection.update_one( + {"_id": project_settings_doc["_id"]}, + new_project_settings_doc + ) + else: + self.collection.insert_one(new_project_settings_doc) def _get_versions_order_doc(self, projection=None): # TODO cache From 6c9d6b3865cfdce385aba868d61b9cb09985ab88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:05:15 +0200 Subject: [PATCH 0845/1030] added helper methods for query of override documents --- openpype/settings/handlers.py | 57 +++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 43b1d37c34..6080b5e77f 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -1205,18 +1205,7 @@ class MongoSettingsHandler(SettingsHandler): globals_document = self.collection.find_one({ "type": GLOBAL_SETTINGS_KEY }) - document = ( - self._get_studio_system_settings_overrides_for_version() - ) - if document is None: - document = self._find_closest_system_settings() - - version = None - if document: - if document["type"] == self._system_settings_key: - version = document["version"] - else: - version = LEGACY_SETTINGS_VERSION + document, version = self._get_system_settings_overrides_doc() merged_document = self._apply_global_settings( document, globals_document @@ -1232,21 +1221,27 @@ class MongoSettingsHandler(SettingsHandler): return data, cache.version return data + def _get_system_settings_overrides_doc(self): + document = ( + self._get_studio_system_settings_overrides_for_version() + ) + if document is None: + document = self._find_closest_system_settings() + + version = None + if document: + if document["type"] == self._system_settings_key: + version = document["version"] + else: + version = LEGACY_SETTINGS_VERSION + + return document, version + def _get_project_settings_overrides(self, project_name, return_version): if self.project_settings_cache[project_name].is_outdated: - document = self._get_project_settings_overrides_for_version( + document, version = self._get_project_settings_overrides_doc( project_name ) - if document is None: - document = self._find_closest_project_settings(project_name) - - version = None - if document: - if document["type"] == self._project_settings_key: - version = document["version"] - else: - version = LEGACY_SETTINGS_VERSION - self.project_settings_cache[project_name].update_from_document( document, version ) @@ -1257,6 +1252,22 @@ class MongoSettingsHandler(SettingsHandler): return data, cache.version return data + def _get_project_settings_overrides_doc(self, project_name): + document = self._get_project_settings_overrides_for_version( + project_name + ) + if document is None: + document = self._find_closest_project_settings(project_name) + + version = None + if document: + if document["type"] == self._project_settings_key: + version = document["version"] + else: + version = LEGACY_SETTINGS_VERSION + + return document, version + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" return self._get_project_settings_overrides(None, return_version) From 8f121275bdbb08eb686ce482f529b97c96dfe1cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:06:11 +0200 Subject: [PATCH 0846/1030] implemented methods to get last saved information --- openpype/settings/handlers.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 6080b5e77f..af2bf104de 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -688,6 +688,15 @@ class MongoSettingsHandler(SettingsHandler): # Update cache self.system_settings_cache.update_data(data, self._current_version) + last_saved_info = SettingsStateInfo.create_new( + self._current_version, + SYSTEM_SETTINGS_KEY, + None + ) + self.system_settings_cache.update_last_saved_info( + last_saved_info + ) + # Get copy of just updated cache system_settings_data = self.system_settings_cache.data_copy() @@ -709,6 +718,7 @@ class MongoSettingsHandler(SettingsHandler): "type": self._system_settings_key, "version": self._current_version, "data": system_settings_data, + "last_saved_info": last_saved_info.to_document_data() } if not system_settings_doc: self.collections.insert_one(new_system_settings_doc) @@ -749,6 +759,14 @@ class MongoSettingsHandler(SettingsHandler): data_cache = self.project_settings_cache[project_name] data_cache.update_data(overrides, self._current_version) + last_saved_info = SettingsStateInfo.create_new( + self._current_version, + PROJECT_SETTINGS_KEY, + project_name + ) + + data_cache.update_last_saved_info(last_saved_info) + self._save_project_data( project_name, self._project_settings_key, data_cache ) @@ -863,6 +881,7 @@ class MongoSettingsHandler(SettingsHandler): "data": data_cache.data, "is_default": is_default, "version": self._current_version, + "last_saved_info": last_saved_info.to_data() } if not is_default: query_filter["project_name"] = project_name @@ -1207,6 +1226,9 @@ class MongoSettingsHandler(SettingsHandler): }) document, version = self._get_system_settings_overrides_doc() + last_saved_info = SettingsStateInfo.from_document( + version, SYSTEM_SETTINGS_KEY, document + ) merged_document = self._apply_global_settings( document, globals_document ) @@ -1214,6 +1236,9 @@ class MongoSettingsHandler(SettingsHandler): self.system_settings_cache.update_from_document( merged_document, version ) + self.system_settings_cache.update_last_saved_info( + last_saved_info + ) cache = self.system_settings_cache data = cache.data_copy() @@ -1237,6 +1262,13 @@ class MongoSettingsHandler(SettingsHandler): return document, version + def get_system_last_saved_info(self): + # Make sure settings are recaches + self.system_settings_cache.set_outdated() + self.get_studio_system_settings_overrides(False) + + return self.system_settings_cache.last_saved_info.copy() + def _get_project_settings_overrides(self, project_name, return_version): if self.project_settings_cache[project_name].is_outdated: document, version = self._get_project_settings_overrides_doc( @@ -1245,6 +1277,12 @@ class MongoSettingsHandler(SettingsHandler): self.project_settings_cache[project_name].update_from_document( document, version ) + last_saved_info = SettingsStateInfo.from_document( + version, PROJECT_SETTINGS_KEY, document + ) + self.project_settings_cache[project_name].update_last_saved_info( + last_saved_info + ) cache = self.project_settings_cache[project_name] data = cache.data_copy() @@ -1268,6 +1306,13 @@ class MongoSettingsHandler(SettingsHandler): return document, version + def get_project_last_saved_info(self, project_name): + # Make sure settings are recaches + self.project_settings_cache[project_name].set_outdated() + self._get_project_settings_overrides(project_name, False) + + return self.project_settings_cache[project_name].last_saved_info.copy() + def get_studio_project_settings_overrides(self, return_version): """Studio overrides of default project settings.""" return self._get_project_settings_overrides(None, return_version) From 9ea46a2a3690df28c2781cdd0919ed44e0d275fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 16:45:37 +0200 Subject: [PATCH 0847/1030] make available api functions in settings to have access to lock information and last saved information --- openpype/settings/handlers.py | 127 +++++++++++++++++++++++++++++++++- openpype/settings/lib.py | 25 +++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index af2bf104de..3dc33503ea 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -67,7 +67,9 @@ class SettingsStateInfo: return self.from_data(self.to_data()) @classmethod - def create_new(cls, openpype_version, settings_type, project_name): + def create_new( + cls, openpype_version, settings_type=None, project_name=None + ): """Create information about this machine for current time.""" from openpype.lib.pype_info import get_workstation_info @@ -112,6 +114,20 @@ class SettingsStateInfo: }) return data + @classmethod + def create_new_empty(cls, openpype_version, settings_type=None): + return cls( + openpype_version, + settings_type, + None, + None, + None, + None, + None, + None, + None + ) + @classmethod def from_document(cls, openpype_version, settings_type, document): document = document or {} @@ -428,6 +444,54 @@ class SettingsHandler: pass + # UI related calls + @abstractmethod + def get_last_opened_info(self): + """Get information about last opened UI. + + Last opened UI is empty if there is noone who would have opened UI at + the moment when called. + + Returns: + Union[None, SettingsStateInfo]: Information about machine who had + opened Settings UI. + """ + + pass + + @abstractmethod + def opened_ui(self): + """Callback called when settings UI is opened. + + Information about this machine must be available when + 'get_last_opened_info' is called from anywhere until 'closed_ui' is + called again. + + Returns: + SettingsStateInfo: Object representing information about this + machine. Must be passed to 'closed_ui' when finished. + """ + + pass + + @abstractmethod + def closed_ui(self, info_obj): + """Callback called when settings UI is closed. + + From the moment this method is called the information about this + machine is removed and no more available when 'get_last_opened_info' + is called. + + Callback should validate if this machine is still stored as opened ui + before changing any value. + + Args: + info_obj (SettingsStateInfo): Object created when 'opened_ui' was + called. + """ + + pass + @six.add_metaclass(ABCMeta) class LocalSettingsHandler: @@ -690,8 +754,7 @@ class MongoSettingsHandler(SettingsHandler): last_saved_info = SettingsStateInfo.create_new( self._current_version, - SYSTEM_SETTINGS_KEY, - None + SYSTEM_SETTINGS_KEY ) self.system_settings_cache.update_last_saved_info( last_saved_info @@ -1610,6 +1673,64 @@ class MongoSettingsHandler(SettingsHandler): return output return self._sort_versions(output) + def get_last_opened_info(self): + doc = self.collection.find_one({ + "type": "last_opened_settings_ui", + "version": self._current_version + }) or {} + info_data = doc.get("info") + if not info_data: + return SettingsStateInfo.create_new_empty(self._current_version) + + # Fill not available information + info_data["openpype_version"] = self._current_version + info_data["settings_type"] = None + info_data["project_name"] = None + return SettingsStateInfo.from_data(info_data) + + def opened_ui(self): + doc_filter = { + "type": "last_opened_settings_ui", + "version": self._current_version + } + + opened_info = SettingsStateInfo.create_new(self._current_version) + new_doc_data = copy.deepcopy(doc_filter) + new_doc_data["info"] = opened_info.to_document_data() + + doc = self.collection.find_one( + doc_filter, + {"_id": True} + ) + if doc: + self.collection.update_one( + {"_id": doc["_id"]}, + {"$set": new_doc_data} + ) + else: + self.collection.insert_one(new_doc_data) + return opened_info + + def closed_ui(self, info_obj): + doc_filter = { + "type": "last_opened_settings_ui", + "version": self._current_version + } + doc = self.collection.find_one(doc_filter) or {} + info_data = doc.get("info") + if not info_data: + return + + info_data["openpype_version"] = self._current_version + info_data["settings_type"] = None + info_data["project_name"] = None + current_info = SettingsStateInfo.from_data(info_data) + if current_info == info_obj: + self.collection.update_one( + {"_id": doc["_id"]}, + {"$set": {"info": None}} + ) + class MongoLocalSettingsHandler(LocalSettingsHandler): """Settings handler that use mongo for store and load local settings. diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 6df41112c8..58cfd3862c 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -91,6 +91,31 @@ def calculate_changes(old_value, new_value): return changes +@require_handler +def get_system_last_saved_info(): + return _SETTINGS_HANDLER.get_system_last_saved_info() + + +@require_handler +def get_project_last_saved_info(project_name): + return _SETTINGS_HANDLER.get_project_last_saved_info(project_name) + + +@require_handler +def get_last_opened_info(): + return _SETTINGS_HANDLER.get_last_opened_info() + + +@require_handler +def opened_ui(): + return _SETTINGS_HANDLER.opened_ui() + + +@require_handler +def closed_ui(info_obj): + return _SETTINGS_HANDLER.closed_ui(info_obj) + + @require_handler def save_studio_settings(data): """Save studio overrides of system settings. From 2b6d705c441fb147945a53d435895e919dc0111b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Aug 2022 17:16:51 +0200 Subject: [PATCH 0848/1030] Added better logging when DL fails In some specific cases DL sends broken json payload even if response.ok. Handle parsing of broken json better. --- openpype/modules/deadline/abstract_submit_deadline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 3f54273a56..5cf8222b1c 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -4,6 +4,7 @@ It provides Deadline JobInfo data class. """ +import json.decoder import os from abc import abstractmethod import platform @@ -627,7 +628,12 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): self.log.debug(payload) raise RuntimeError(response.text) - result = response.json() + try: + result = response.json() + except json.decoder.JSONDecodeError: + self.log.warning("Broken response {}".format(response)) + raise RuntimeError("Broken response from DL") + # for submit publish job self._instance.data["deadlineSubmissionJob"] = result From beedfd2ecee833854db505e0566de377a8243649 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Aug 2022 17:22:28 +0200 Subject: [PATCH 0849/1030] Added better logging when DL fails In some specific cases DL sends broken json payload even if response.ok. Handle parsing of broken json better. --- openpype/modules/deadline/abstract_submit_deadline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 5cf8222b1c..c38f16149e 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -631,7 +631,9 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): try: result = response.json() except json.decoder.JSONDecodeError: - self.log.warning("Broken response {}".format(response)) + msg = "Broken response {}. ".format(response) + msg += "Try restarting DL webservice" + self.log.warning() raise RuntimeError("Broken response from DL") # for submit publish job From 9251a0fd4294725a5fb1dfbef68788c409554230 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:09:55 +0200 Subject: [PATCH 0850/1030] changed function names --- openpype/settings/handlers.py | 20 ++++++++++---------- openpype/settings/lib.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 3dc33503ea..1b59531943 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -460,22 +460,22 @@ class SettingsHandler: pass @abstractmethod - def opened_ui(self): + def opened_settings_ui(self): """Callback called when settings UI is opened. Information about this machine must be available when - 'get_last_opened_info' is called from anywhere until 'closed_ui' is - called again. + 'get_last_opened_info' is called from anywhere until + 'closed_settings_ui' is called again. Returns: SettingsStateInfo: Object representing information about this - machine. Must be passed to 'closed_ui' when finished. + machine. Must be passed to 'closed_settings_ui' when finished. """ pass @abstractmethod - def closed_ui(self, info_obj): + def closed_settings_ui(self, info_obj): """Callback called when settings UI is closed. From the moment this method is called the information about this @@ -486,8 +486,8 @@ class SettingsHandler: before changing any value. Args: - info_obj (SettingsStateInfo): Object created when 'opened_ui' was - called. + info_obj (SettingsStateInfo): Object created when + 'opened_settings_ui' was called. """ pass @@ -1680,7 +1680,7 @@ class MongoSettingsHandler(SettingsHandler): }) or {} info_data = doc.get("info") if not info_data: - return SettingsStateInfo.create_new_empty(self._current_version) + return None # Fill not available information info_data["openpype_version"] = self._current_version @@ -1688,7 +1688,7 @@ class MongoSettingsHandler(SettingsHandler): info_data["project_name"] = None return SettingsStateInfo.from_data(info_data) - def opened_ui(self): + def opened_settings_ui(self): doc_filter = { "type": "last_opened_settings_ui", "version": self._current_version @@ -1711,7 +1711,7 @@ class MongoSettingsHandler(SettingsHandler): self.collection.insert_one(new_doc_data) return opened_info - def closed_ui(self, info_obj): + def closed_settings_ui(self, info_obj): doc_filter = { "type": "last_opened_settings_ui", "version": self._current_version diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 58cfd3862c..5eaddf6e6e 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -107,13 +107,13 @@ def get_last_opened_info(): @require_handler -def opened_ui(): - return _SETTINGS_HANDLER.opened_ui() +def opened_settings_ui(): + return _SETTINGS_HANDLER.opened_settings_ui() @require_handler -def closed_ui(info_obj): - return _SETTINGS_HANDLER.closed_ui(info_obj) +def closed_settings_ui(info_obj): + return _SETTINGS_HANDLER.closed_settings_ui(info_obj) @require_handler From baa2505fbfa2055aded2e2c439805d44a3a82347 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:10:32 +0200 Subject: [PATCH 0851/1030] show a dialog if someone else has opened settings UI --- openpype/tools/settings/settings/window.py | 136 ++++++++++++++++++++- 1 file changed, 131 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 22778e4a5b..96f11f3932 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -1,4 +1,16 @@ from Qt import QtWidgets, QtGui, QtCore + +from openpype import style + +from openpype.lib import is_admin_password_required +from openpype.widgets import PasswordDialog + +from openpype.settings.lib import ( + get_last_opened_info, + opened_settings_ui, + closed_settings_ui, +) + from .categories import ( CategoryState, SystemWidget, @@ -10,10 +22,6 @@ from .widgets import ( SettingsTabWidget ) from .search_dialog import SearchEntitiesDialog -from openpype import style - -from openpype.lib import is_admin_password_required -from openpype.widgets import PasswordDialog class MainWidget(QtWidgets.QWidget): @@ -25,6 +33,10 @@ class MainWidget(QtWidgets.QWidget): def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) + # Object referencing to this machine and time when UI was opened + # - is used on close event + self._last_opened_info = None + self._user_passed = False self._reset_on_show = reset_on_show @@ -74,7 +86,7 @@ class MainWidget(QtWidgets.QWidget): self._on_restart_required ) tab_widget.reset_started.connect(self._on_reset_started) - tab_widget.reset_started.connect(self._on_reset_finished) + tab_widget.reset_finished.connect(self._on_reset_finished) tab_widget.full_path_requested.connect(self._on_full_path_request) header_tab_widget.context_menu_requested.connect( @@ -131,11 +143,38 @@ class MainWidget(QtWidgets.QWidget): def showEvent(self, event): super(MainWidget, self).showEvent(event) + if self._reset_on_show: self._reset_on_show = False # Trigger reset with 100ms delay QtCore.QTimer.singleShot(100, self.reset) + elif not self._last_opened_info: + self._check_on_ui_open() + + def _check_on_ui_open(self): + last_opened_info = get_last_opened_info() + if last_opened_info is not None: + if self._last_opened_info != last_opened_info: + self._last_opened_info = None + else: + self._last_opened_info = opened_settings_ui() + + if self._last_opened_info is not None: + return + + dialog = SettingsUIOpenedElsewhere(last_opened_info, self) + dialog.exec_() + if dialog.result() == 1: + self._last_opened_info = opened_settings_ui() + return + + def closeEvent(self, event): + if self._last_opened_info: + closed_settings_ui(self._last_opened_info) + self._last_opened_info = None + super(MainWidget, self).closeEvent(event) + def _show_password_dialog(self): if self._password_dialog: self._password_dialog.open() @@ -221,6 +260,8 @@ class MainWidget(QtWidgets.QWidget): if current_widget is widget: self._update_search_dialog() + self._check_on_ui_open() + def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): # todo: search in all widgets (or in active)? @@ -231,3 +272,88 @@ class MainWidget(QtWidgets.QWidget): return return super(MainWidget, self).keyPressEvent(event) + + +class SettingsUIOpenedElsewhere(QtWidgets.QDialog): + def __init__(self, info_obj, parent=None): + super(SettingsUIOpenedElsewhere, self).__init__(parent) + + self._result = 0 + + self.setWindowTitle("Someone else has opened Settings UI") + + message_label = QtWidgets.QLabel(( + "Someone else has opened Settings UI. That may cause data loss." + " Please contact the person on the other side." + "

    You can open the UI in view-only mode or take" + " the control which will cause the other settings won't be able" + " to save changes.
    " + ), self) + message_label.setWordWrap(True) + + separator_widget_1 = QtWidgets.QFrame(self) + separator_widget_2 = QtWidgets.QFrame(self) + for separator_widget in ( + separator_widget_1, + separator_widget_2 + ): + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(1) + separator_widget.setMaximumHeight(1) + + other_information = QtWidgets.QWidget(self) + other_information_layout = QtWidgets.QFormLayout(other_information) + other_information_layout.setContentsMargins(0, 0, 0, 0) + for label, value in ( + ("Username", info_obj.username), + ("Host name", info_obj.hostname), + ("Host IP", info_obj.hostip), + ("System name", info_obj.system_name), + ("Local ID", info_obj.local_id), + ("Time Stamp", info_obj.timestamp), + ): + other_information_layout.addRow( + label, + QtWidgets.QLabel(value, other_information) + ) + + footer_widget = QtWidgets.QWidget(self) + buttons_widget = QtWidgets.QWidget(footer_widget) + + take_control_btn = QtWidgets.QPushButton( + "Take control", buttons_widget + ) + view_mode_btn = QtWidgets.QPushButton( + "View only", buttons_widget + ) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addWidget(take_control_btn, 1) + buttons_layout.addWidget(view_mode_btn, 1) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(buttons_widget, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(separator_widget_1, 0) + layout.addWidget(other_information, 1, QtCore.Qt.AlignHCenter) + layout.addWidget(separator_widget_2, 0) + layout.addWidget(footer_widget, 0) + + take_control_btn.clicked.connect(self._on_take_control) + view_mode_btn.clicked.connect(self._on_view_mode) + + def result(self): + return self._result + + def _on_take_control(self): + self._result = 1 + self.close() + + def _on_view_mode(self): + self._result = 0 + self.close() From 936363a6608aed3bf4bc4b2a2b6977b9f6d03142 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 18:45:54 +0200 Subject: [PATCH 0852/1030] settings can take go to view mode or take control --- .../tools/settings/settings/categories.py | 19 ++++++++++ openpype/tools/settings/settings/window.py | 38 +++++++++++++++---- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index f42027d9e2..0410fa1810 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -121,6 +121,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.user_role = user_role self.entity = None + self._edit_mode = None self._state = CategoryState.Idle @@ -191,6 +192,21 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) raise TypeError("Unknown type: {}".format(label)) + def set_edit_mode(self, enabled): + if enabled is self._edit_mode: + return + + self.save_btn.setEnabled(enabled) + if enabled: + tooltip = ( + "Someone else has opened settings UI." + "\nTry hit refresh to check if settings are already available." + ) + else: + tooltip = "Save settings" + + self.save_btn.setToolTip(tooltip) + @property def state(self): return self._state @@ -434,6 +450,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.set_state(CategoryState.Idle) def save(self): + if not self._edit_mode: + return + if not self.items_are_valid(): return diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 96f11f3932..013a273e98 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -36,6 +36,8 @@ class MainWidget(QtWidgets.QWidget): # Object referencing to this machine and time when UI was opened # - is used on close event self._last_opened_info = None + self._edit_mode = None + self._main_reset = False self._user_passed = False self._reset_on_show = reset_on_show @@ -152,6 +154,12 @@ class MainWidget(QtWidgets.QWidget): elif not self._last_opened_info: self._check_on_ui_open() + def closeEvent(self, event): + if self._last_opened_info: + closed_settings_ui(self._last_opened_info) + self._last_opened_info = None + super(MainWidget, self).closeEvent(event) + def _check_on_ui_open(self): last_opened_info = get_last_opened_info() if last_opened_info is not None: @@ -161,19 +169,27 @@ class MainWidget(QtWidgets.QWidget): self._last_opened_info = opened_settings_ui() if self._last_opened_info is not None: + if self._edit_mode is not True: + self._set_edit_mode(True) + return + + if self._edit_mode is False: return dialog = SettingsUIOpenedElsewhere(last_opened_info, self) dialog.exec_() - if dialog.result() == 1: + edit_enabled = dialog.result() == 1 + if edit_enabled: self._last_opened_info = opened_settings_ui() + self._set_edit_mode(edit_enabled) + + def _set_edit_mode(self, mode): + if self._edit_mode is mode: return - def closeEvent(self, event): - if self._last_opened_info: - closed_settings_ui(self._last_opened_info) - self._last_opened_info = None - super(MainWidget, self).closeEvent(event) + self._edit_mode = mode + for tab_widget in self.tab_widgets: + tab_widget.set_edit_mode(mode) def _show_password_dialog(self): if self._password_dialog: @@ -215,8 +231,11 @@ class MainWidget(QtWidgets.QWidget): if self._reset_on_show: self._reset_on_show = False + self._main_reset = True for tab_widget in self.tab_widgets: tab_widget.reset() + self._main_reset = False + self._check_on_ui_open() def _update_search_dialog(self, clear=False): if self._search_dialog.isVisible(): @@ -260,7 +279,8 @@ class MainWidget(QtWidgets.QWidget): if current_widget is widget: self._update_search_dialog() - self._check_on_ui_open() + if not self._main_reset: + self._check_on_ui_open() def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): @@ -340,7 +360,9 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(message_label, 0) layout.addWidget(separator_widget_1, 0) - layout.addWidget(other_information, 1, QtCore.Qt.AlignHCenter) + layout.addStretch(1) + layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) + layout.addStretch(1) layout.addWidget(separator_widget_2, 0) layout.addWidget(footer_widget, 0) From 70cfa733f3e7e985580ec8fff8520c31ec5184c8 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 18 Aug 2022 18:34:34 +0000 Subject: [PATCH 0853/1030] [Automated] Bump version --- CHANGELOG.md | 27 +++++++++++++++++++-------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80673e9f8a..b192d26250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,25 @@ # Changelog -## [3.13.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) +**🆕 New features** + +- Maya: Build workfile by template [\#3578](https://github.com/pypeclub/OpenPype/pull/3578) + +**🚀 Enhancements** + +- Ftrack: Addiotional component metadata [\#3685](https://github.com/pypeclub/OpenPype/pull/3685) +- Ftrack: Set task status on farm publishing [\#3680](https://github.com/pypeclub/OpenPype/pull/3680) +- Ftrack: Set task status on task creation in integrate hierarchy [\#3675](https://github.com/pypeclub/OpenPype/pull/3675) +- Maya: Disable rendering of all lights for render instances submitted through Deadline. [\#3661](https://github.com/pypeclub/OpenPype/pull/3661) +- General: Optimized OCIO configs [\#3650](https://github.com/pypeclub/OpenPype/pull/3650) + **🐛 Bug fixes** +- General: Switch from hero version to versioned works [\#3691](https://github.com/pypeclub/OpenPype/pull/3691) +- General: Fix finding of last version [\#3656](https://github.com/pypeclub/OpenPype/pull/3656) - 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) @@ -14,8 +28,12 @@ **🔀 Refactored code** +- General: Use client projects getter [\#3673](https://github.com/pypeclub/OpenPype/pull/3673) +- Resolve: Match folder structure to other hosts [\#3653](https://github.com/pypeclub/OpenPype/pull/3653) +- Maya: Hosts as modules [\#3647](https://github.com/pypeclub/OpenPype/pull/3647) - 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) +- General: Workfiles builder using query functions [\#3598](https://github.com/pypeclub/OpenPype/pull/3598) **Merged pull requests:** @@ -89,7 +107,6 @@ **🚀 Enhancements** - 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) **🐛 Bug fixes** @@ -100,16 +117,10 @@ - General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) - 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) -- General: Create workfile documents works again [\#3538](https://github.com/pypeclub/OpenPype/pull/3538) **🔀 Refactored code** - 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) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 9ae52e8370..38723ed123 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.3" +__version__ = "3.14.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 287a3c78f0..4d4aff01a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.1-nightly.3" # OpenPype +version = "3.14.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9f879bb22a2a01fe17adc1b7e9e61df8603e6537 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 18 Aug 2022 18:47:09 +0000 Subject: [PATCH 0854/1030] [Automated] Release --- CHANGELOG.md | 6 +++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b192d26250..e19993ad75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.14.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...3.14.0) **🆕 New features** @@ -25,6 +25,7 @@ - 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) +- Integrator: Don't force to have dot before frame [\#3611](https://github.com/pypeclub/OpenPype/pull/3611) **🔀 Refactored code** @@ -69,7 +70,6 @@ - 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) diff --git a/openpype/version.py b/openpype/version.py index 38723ed123..c28b480940 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.0-nightly.1" +__version__ = "3.14.0" diff --git a/pyproject.toml b/pyproject.toml index 4d4aff01a2..e670d0a2ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.14.0-nightly.1" # OpenPype +version = "3.14.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 407b6b518e0da63e8efd021952c01b6311cf0640 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 12:05:38 +0200 Subject: [PATCH 0855/1030] use controlled to handle last opened info --- .../tools/settings/settings/categories.py | 23 ++- openpype/tools/settings/settings/window.py | 136 ++++++++++++------ 2 files changed, 114 insertions(+), 45 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 0410fa1810..2e3c6d9dda 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -115,13 +115,19 @@ class SettingsCategoryWidget(QtWidgets.QWidget): "settings to update them to you current running OpenPype version." ) - def __init__(self, user_role, parent=None): + def __init__(self, controller, parent=None): super(SettingsCategoryWidget, self).__init__(parent) - self.user_role = user_role + self._controller = controller + controller.event_system.add_callback( + "edit.mode.changed", + self._edit_mode_changed + ) self.entity = None self._edit_mode = None + self._last_saved_info = None + self._reset_crashed = False self._state = CategoryState.Idle @@ -192,11 +198,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) raise TypeError("Unknown type: {}".format(label)) + def _edit_mode_changed(self, event): + self.set_edit_mode(event["edit_mode"]) + def set_edit_mode(self, enabled): if enabled is self._edit_mode: return - self.save_btn.setEnabled(enabled) + self._edit_mode = enabled + + self.save_btn.setEnabled(enabled and not self._reset_crashed) if enabled: tooltip = ( "Someone else has opened settings UI." @@ -302,7 +313,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(5, 5, 5, 5) - if self.user_role == "developer": + if self._controller.user_role == "developer": self._add_developer_ui(footer_layout, footer_widget) footer_layout.addWidget(empty_label, 1) @@ -683,14 +694,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) def _on_reset_crash(self): + self._reset_crashed = True self.save_btn.setEnabled(False) if self.breadcrumbs_model is not None: self.breadcrumbs_model.set_entity(None) def _on_reset_success(self): + self._reset_crashed = True if not self.save_btn.isEnabled(): - self.save_btn.setEnabled(True) + self.save_btn.setEnabled(self._edit_mode) if self.breadcrumbs_model is not None: path = self.breadcrumbs_bar.path() diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 013a273e98..612975e30a 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtGui, QtCore from openpype import style from openpype.lib import is_admin_password_required +from openpype.lib.events import EventSystem from openpype.widgets import PasswordDialog from openpype.settings.lib import ( @@ -24,6 +25,81 @@ from .widgets import ( from .search_dialog import SearchEntitiesDialog +class SettingsController: + """Controller for settings tools. + + Added when tool was finished for checks of last opened in settings + categories and being able communicated with main widget logic. + """ + + def __init__(self, user_role): + self._user_role = user_role + self._event_system = EventSystem() + + self._opened_info = None + self._last_opened_info = None + self._edit_mode = None + + @property + def user_role(self): + return self._user_role + + @property + def event_system(self): + return self._event_system + + @property + def opened_info(self): + return self._opened_info + + @property + def last_opened_info(self): + return self._last_opened_info + + @property + def edit_mode(self): + return self._edit_mode + + def ui_closed(self): + if self._opened_info is not None: + closed_settings_ui(self._opened_info) + + self._opened_info = None + self._edit_mode = None + + def set_edit_mode(self, enabled): + if self._edit_mode is enabled: + return + + opened_info = None + if enabled: + opened_info = opened_settings_ui() + self._last_opened_info = opened_info + + self._opened_info = opened_info + self._edit_mode = enabled + + self.event_system.emit( + "edit.mode.changed", + {"edit_mode": enabled}, + "controller" + ) + + def update_last_opened_info(self): + print("update_last_opened_info") + last_opened_info = get_last_opened_info() + enabled = False + if ( + last_opened_info is None + or self._opened_info == last_opened_info + ): + enabled = True + + self._last_opened_info = last_opened_info + + self.set_edit_mode(enabled) + + class MainWidget(QtWidgets.QWidget): trigger_restart = QtCore.Signal() @@ -33,11 +109,12 @@ class MainWidget(QtWidgets.QWidget): def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) + controller = SettingsController(user_role) + # Object referencing to this machine and time when UI was opened # - is used on close event - self._last_opened_info = None - self._edit_mode = None self._main_reset = False + self._controller = controller self._user_passed = False self._reset_on_show = reset_on_show @@ -55,8 +132,8 @@ class MainWidget(QtWidgets.QWidget): header_tab_widget = SettingsTabWidget(parent=self) - studio_widget = SystemWidget(user_role, header_tab_widget) - project_widget = ProjectWidget(user_role, header_tab_widget) + studio_widget = SystemWidget(controller, header_tab_widget) + project_widget = ProjectWidget(controller, header_tab_widget) tab_widgets = [ studio_widget, @@ -151,45 +228,24 @@ class MainWidget(QtWidgets.QWidget): # Trigger reset with 100ms delay QtCore.QTimer.singleShot(100, self.reset) - elif not self._last_opened_info: - self._check_on_ui_open() - def closeEvent(self, event): - if self._last_opened_info: - closed_settings_ui(self._last_opened_info) - self._last_opened_info = None + self._controller.ui_closed() + super(MainWidget, self).closeEvent(event) - def _check_on_ui_open(self): - last_opened_info = get_last_opened_info() - if last_opened_info is not None: - if self._last_opened_info != last_opened_info: - self._last_opened_info = None - else: - self._last_opened_info = opened_settings_ui() - - if self._last_opened_info is not None: - if self._edit_mode is not True: - self._set_edit_mode(True) + def _check_on_reset(self): + self._controller.update_last_opened_info() + if self._controller.edit_mode: return - if self._edit_mode is False: - return + # if self._edit_mode is False: + # return - dialog = SettingsUIOpenedElsewhere(last_opened_info, self) + dialog = SettingsUIOpenedElsewhere( + self._controller.last_opened_info, self + ) dialog.exec_() - edit_enabled = dialog.result() == 1 - if edit_enabled: - self._last_opened_info = opened_settings_ui() - self._set_edit_mode(edit_enabled) - - def _set_edit_mode(self, mode): - if self._edit_mode is mode: - return - - self._edit_mode = mode - for tab_widget in self.tab_widgets: - tab_widget.set_edit_mode(mode) + self._controller.set_edit_mode(dialog.result() == 1) def _show_password_dialog(self): if self._password_dialog: @@ -235,7 +291,7 @@ class MainWidget(QtWidgets.QWidget): for tab_widget in self.tab_widgets: tab_widget.reset() self._main_reset = False - self._check_on_ui_open() + self._check_on_reset() def _update_search_dialog(self, clear=False): if self._search_dialog.isVisible(): @@ -280,7 +336,7 @@ class MainWidget(QtWidgets.QWidget): self._update_search_dialog() if not self._main_reset: - self._check_on_ui_open() + self._check_on_reset() def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Find): @@ -306,8 +362,8 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): "Someone else has opened Settings UI. That may cause data loss." " Please contact the person on the other side." "

    You can open the UI in view-only mode or take" - " the control which will cause the other settings won't be able" - " to save changes.
    " + " the control which will cause settings on the other side" + " won't be able to save changes.
    " ), self) message_label.setWordWrap(True) From 496728e9abc54dbce957127e8e8134f629ed3ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 19 Aug 2022 12:09:52 +0200 Subject: [PATCH 0856/1030] :recycle: handle host name that is not set --- openpype/plugins/publish/extract_review.py | 2 ++ openpype/plugins/publish/integrate_subset_group.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e16f324e0a..27117510b2 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1459,6 +1459,8 @@ class ExtractReview(pyblish.api.InstancePlugin): output = -1 regexes = self.compile_list_of_regexes(in_list) for regex in regexes: + if not value: + continue if re.match(regex, value): output = 1 break diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 910cb060a6..79dd10fb8f 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -93,6 +93,6 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["app"], + "hosts": anatomy_data.get("app"), "task_types": task.get("type") } From 6d056c774d7011b8810a66bd63073df02596b50c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 19 Aug 2022 12:24:18 +0200 Subject: [PATCH 0857/1030] Small grammar fixes Just ran through google spellcheck. --- website/docs/dev_settings.md | 77 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/website/docs/dev_settings.md b/website/docs/dev_settings.md index 492d25930d..94590345e8 100644 --- a/website/docs/dev_settings.md +++ b/website/docs/dev_settings.md @@ -4,26 +4,26 @@ title: Settings sidebar_label: Settings --- -Settings gives ability to change how OpenPype behaves in certain situations. Settings are split into 3 categories **system settings**, **project anatomy** and **project settings**. Project anatomy and project settings are in grouped into single category but there is a technical difference (explained later). Only difference in system and project settings is that system settings can't be technically handled on a project level or their values must be available no matter in which project are values received. Settings have headless entities or settings UI. +Settings give the ability to change how OpenPype behaves in certain situations. Settings are split into 3 categories **system settings**, **project anatomy** and **project settings**. Project anatomy and project settings are grouped into a single category but there is a technical difference (explained later). Only difference in system and project settings is that system settings can't be technically handled on a project level or their values must be available no matter in which project the values are received. Settings have headless entities or settings UI. There is one more category **local settings** but they don't have ability to be changed or defined easily. Local settings can change how settings work per machine, can affect both system and project settings but they're hardcoded for predefined values at this moment. ## Settings schemas -System and project settings are defined by settings schemas. Schema define structure of output value, what value types output will contain, how settings are stored and how it's UI input will look. +System and project settings are defined by settings schemas. Schema defines the structure of output value, what value types output will contain, how settings are stored and how its UI input will look. ## Settings values -Output of settings is a json serializable value. There are 3 possible types of value **default values**, **studio overrides** and **project overrides**. Default values must be always available for all settings schemas, their values are stored to code. Default values is what everyone who just installed OpenPype will use as default values. It is good practice to set example values but they should be relevant. +Output of settings is a json serializable value. There are 3 possible types of value **default values**, **studio overrides** and **project overrides**. Default values must be always available for all settings schemas, their values are stored to code. Default values are what everyone who just installed OpenPype will use as default values. It is good practice to set example values but they should be actually relevant. -Setting overrides is what makes settings powerful tool. Overrides contain only a part of settings with additional metadata which describe which parts of settings values that should be replaced from overrides values. Using overrides gives ability to save only specific values and use default values for rest. It is super useful in project settings which have up to 2 levels of overrides. In project settings are used **default values** as base on which are applied **studio overrides** and then **project overrides**. In practice it is possible to save only studio overrides which affect all projects. Changes in studio overrides are then propagated to all projects without project overrides. But values can be locked on project level so studio overrides are not used. +Setting overrides is what makes settings a powerful tool. Overrides contain only a part of settings with additional metadata that describe which parts of settings values should be replaced from overrides values. Using overrides gives the ability to save only specific values and use default values for rest. It is super useful in project settings which have up to 2 levels of overrides. In project settings are used **default values** as base on which are applied **studio overrides** and then **project overrides**. In practice it is possible to save only studio overrides which affect all projects. Changes in studio overrides are then propagated to all projects without project overrides. But values can be locked on project level so studio overrides are not used. ## Settings storage -As was mentined default values are stored into repository files. Overrides are stored to Mongo database. The value in mongo contain only overrides with metadata so their content on it's own is useless and must be used with combination of default values. System settings and project settings are stored into special collection. Single document represents one set of overrides with OpenPype version for which is stored. Settings are versioned and are loaded in specific order - current OpenPype version overrides or first lower available. If there are any overrides with same or lower version then first higher version is used. If there are any overrides then no overrides are applied. +As was mentioned default values are stored into repository files. Overrides are stored in the Mongo database. The value in mongo contain only overrides with metadata so their content on it's own is useless and must be used with combination of default values. System settings and project settings are stored into special collection. Single document represents one set of overrides with OpenPype version for which is stored. Settings are versioned and are loaded in specific order - current OpenPype version overrides or first lower available. If there are any overrides with the same or lower version then the first higher version is used. If there are any overrides then no overrides are applied. -Project anatomy is stored into project document thus is not versioned and it's values are always overriden. Any changes in anatomy schema may have drastic effect on production and OpenPype updates. +Project anatomy is stored into a project document thus is not versioned and its values are always overridden. Any changes in anatomy schema may have a drastic effect on production and OpenPype updates. ## Settings schema items As was mentioned schema items define output type of values, how they are stored and how they look in UI. -- schemas are (by default) defined by a json files +- schemas are (by default) defined by json files - OpenPype core system settings schemas are stored in `~/openpype/settings/entities/schemas/system_schema/` and project settings in `~/openpype/settings/entities/schemas/projects_schema/` - both contain `schema_main.json` which are entry points - OpenPype modules/addons can define their settings schemas using `BaseModuleSettingsDef` in that case some functionality may be slightly modified @@ -31,20 +31,21 @@ As was mentioned schema items define output type of values, how they are stored - **type** is only common key which is required for all schema items - each item may have "input modifiers" (other keys in dictionary) and they may be required or optional based on the type - there are special keys across all items - - `"is_file"` - this key is used when defaults values are stored which define that this key is a filename where it's values are stored - - key is validated must be once in hierarchy else it won't be possible to store default values - - make sense to fill it only if it's value if `true` - - `"is_group"` - define that all values under a key in settings hierarchy will be overridden if any value is modified - - this key is not allowed for all inputs as they may not have technical ability to handle it - - key is validated can be only once in hierarchy and is automatically filled on last possible item if is not defined in schemas - - make sense to fill it only if it's value if `true` + - `"is_file"` - this key is used when defaults values are stored in the file. Its value matches the filename where values are stored + - key is validated, must be unique in hierarchy otherwise it won't be possible to store default values + - make sense to fill it only if it's value if `true` + + - `"is_group"` - define that all values under a key in settings hierarchy will be overridden if any value is modified + - this key is not allowed for all inputs as they may not have technical ability to handle it + - key is validated, must be unique in hierarchy and is automatically filled on last possible item if is not defined in schemas + - make sense to fill it only if it's value if `true` - all entities can have set `"tooltip"` key with description which will be shown in UI on hover ### Inner schema -Settings schemas are big json files which would became unmanageable if would be in single file. To be able to split them into multiple files to help organize them special types `schema` and `template` were added. Both types are relating to a different file by filename. If json file contains dictionary it is considered as `schema` if contains list it is considered as `template`. +Settings schemas are big json files which would become unmanageable if they were in a single file. To be able to split them into multiple files to help organize them special types `schema` and `template` were added. Both types are related to a different file by filename. If a json file contains a dictionary it is considered as `schema` if it contains a list it is considered as a `template`. #### schema -Schema item is replaced by content of entered schema name. It is recommended that schema file is used only once in settings hierarchy. Templates are meant for reusing. +Schema item is replaced by content of entered schema name. It is recommended that the schema file is used only once in settings hierarchy. Templates are meant for reusing. - schema must have `"name"` key which is name of schema that should be used ```javascript @@ -156,7 +157,7 @@ Templates are almost the same as schema items but can contain one or more items } ``` -Template data can be used only to fill templates in values but not in keys. It is also possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above). +Template data can be used only to fill templates in values but not in keys. It is also possible to define default values for unfilled fields to do so one of the items in the list must be a dictionary with key "__default_values__"` and value as dictionary with default key: values (as in example above). ```javascript { ... @@ -169,7 +170,7 @@ Template data can be used only to fill templates in values but not in keys. It i } ``` -Because formatting value can be only string it is possible to use formatting values which are replaced with different type. +Because formatting values can be only string it is possible to use formatting values which are replaced with different types. ```javascript // Template data { @@ -201,7 +202,7 @@ Dynamic schema item marks a place in settings schema where schemas defined by `B "name": "project_settings/global" } ``` -- `BaseModuleSettingsDef` with implemented `get_settings_schemas` can return a dictionary where key define a dynamic schema name and value schemas that will be put there +- `BaseModuleSettingsDef` with implemented `get_settings_schemas` can return a dictionary where key defines a dynamic schema name and value schemas that will be put there - dynamic schemas work almost the same way as templates - one item can be replaced by multiple items (or by 0 items) - goal is to dynamically load settings of OpenPype modules without having their schemas or default values in core repository @@ -215,12 +216,12 @@ These inputs wraps another inputs into {key: value} relation #### dict - this is dictionary type wrapping more inputs with keys defined in schema - may be used as dynamic children (e.g. in [list](#list) or [dict-modifiable](#dict-modifiable)) - - in that case the only key modifier is `children` which is list of it's keys - - USAGE: e.g. List of dictionaries where each dictionary have same structure. + - in that case the only key modifier is `children` which is a list of its keys + - USAGE: e.g. List of dictionaries where each dictionary has the same structure. - if is not used as dynamic children then must have defined `"key"` under which are it's values stored - may be with or without `"label"` (only for GUI) - - `"label"` must be set to be able mark item as group with `"is_group"` key set to True -- item with label can visually wrap it's children + - `"label"` must be set to be able to mark item as group with `"is_group"` key set to True +- item with label can visually wrap its children - this option is enabled by default to turn off set `"use_label_wrap"` to `False` - label wrap is by default collapsible - that can be set with key `"collapsible"` to `True`/`False` @@ -314,16 +315,16 @@ These inputs wraps another inputs into {key: value} relation - entity must have defined `"label"` if is not used as widget - is set as group if any parent is not group (can't have children as group) - may be with or without `"label"` (only for GUI) - - `"label"` must be set to be able mark item as group with `"is_group"` key set to True -- item with label can visually wrap it's children - - this option is enabled by default to turn off set `"use_label_wrap"` to `False` - - label wrap is by default collapsible - - that can be set with key `"collapsible"` to `True`/`False` - - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) - - it is possible to add lighter background with `"highlight_content"` (Default: `False`) - - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color -- for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`) - - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`) + - `"label"` must be set to be able to mark item as group with `"is_group"` key set to True +- item with label can visually wrap its children + - this option is enabled by default to turn off set `"use_label_wrap"` to `False` + - label wrap is by default collapsible + - that can be set with key `"collapsible"` to `True`/`False` + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add lighter background with `"highlight_content"` (Default: `False`) + - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color +- for UI purposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`) + - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`) - output is dictionary `{the "key": children values}` - using this type as template item for list type can be used to create infinite hierarchies @@ -795,7 +796,7 @@ How output of the schema could look like on save: ``` #### color -- preimplemented entity to store and load color values +- pre implemented entity to store and load color values - entity store and expect list of 4 integers in range 0-255 - integers represents rgba [Red, Green, Blue, Alpha] - has modifier `"use_alpha"` which can be `True`/`False` @@ -842,9 +843,9 @@ Items used only for UI purposes. ``` ### Proxy wrappers -- should wraps multiple inputs only visually -- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled -- can't be used as widget (first item in e.g. `list`, `dict-modifiable`, etc.) +- should wrap multiple inputs only visually +- these do not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled +- can't be used as a widget (first item in e.g. `list`, `dict-modifiable`, etc.) #### form - wraps inputs into form look layout @@ -893,6 +894,6 @@ Items used only for UI purposes. ## How to add new settings -Always start with modifying or adding new schema and don't worry about values. When you think schema is ready to use launch OpenPype settings in development mode using `poetry run python ./start.py settings --dev` or prepared script in `~/openpype/tools/run_settings(.sh|.ps1)`. Settings opened in development mode have checkbox `Modify defaults` available in bottom left corner. When checked default values are modified and saved on `Save`. This is recommended approach how default settings should be created instead of direct modification of files. +Always start with modifying or adding a new schema and don't worry about values. When you think schema is ready to use launch OpenPype settings in development mode using `poetry run python ./start.py settings --dev` or prepared script in `~/openpype/tools/run_settings(.sh|.ps1)`. Settings opened in development mode have the checkbox `Modify defaults` available in the bottom left corner. When checked default values are modified and saved on `Save`. This is a recommended approach on how default settings should be created instead of direct modification of files. ![Modify default settings](assets/settings_dev.png) From 4f03f2dd09f1eed5a0f23d2d9c9a428ae7656560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 12:33:32 +0200 Subject: [PATCH 0858/1030] modified dialog --- openpype/tools/settings/settings/window.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 612975e30a..fcbcd129d0 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -359,11 +359,12 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): self.setWindowTitle("Someone else has opened Settings UI") message_label = QtWidgets.QLabel(( - "Someone else has opened Settings UI. That may cause data loss." + "Someone else has opened Settings UI which could cause data loss." " Please contact the person on the other side." - "

    You can open the UI in view-only mode or take" - " the control which will cause settings on the other side" - " won't be able to save changes.
    " + "

    You can open the UI in view-only mode." + " All changes in view mode will be lost." + "

    You can take the control which will cause that" + " all changes of settings on the other side will be lost.
    " ), self) message_label.setWordWrap(True) @@ -435,3 +436,7 @@ class SettingsUIOpenedElsewhere(QtWidgets.QDialog): def _on_view_mode(self): self._result = 0 self.close() + + def showEvent(self, event): + super(SettingsUIOpenedElsewhere, self).showEvent(event) + self.resize(600, 400) From fc4db8802d260d62a6cd93ea914bb5558151ba0c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 19 Aug 2022 13:01:46 +0200 Subject: [PATCH 0859/1030] Fixed issues after code review Warning should print exception. JSONDecoder is not in Pype2 --- .../deadline/abstract_submit_deadline.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index c38f16149e..9d952586d2 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -16,7 +16,12 @@ import attr import requests import pyblish.api -from openpype.pipeline.publish import AbstractMetaInstancePlugin +from openpype.pipeline.publish import ( + AbstractMetaInstancePlugin, + KnownPublishError +) + +JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) def requests_post(*args, **kwargs): @@ -616,7 +621,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): str: resulting Deadline job id. Throws: - RuntimeError: if submission fails. + KnownPublishError: if submission fails. """ url = "{}/api/jobs".format(self._deadline_url) @@ -626,15 +631,15 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): self.log.error(response.status_code) self.log.error(response.content) self.log.debug(payload) - raise RuntimeError(response.text) + raise KnownPublishError(response.text) try: result = response.json() except json.decoder.JSONDecodeError: msg = "Broken response {}. ".format(response) - msg += "Try restarting DL webservice" - self.log.warning() - raise RuntimeError("Broken response from DL") + msg += "Try restarting the Deadline Webservice." + self.log.warning(msg, exc_info=True) + raise KnownPublishError("Broken response from DL") # for submit publish job self._instance.data["deadlineSubmissionJob"] = result From 8cd15708b65213092924263b0386f8bec28dc7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:07:52 +0200 Subject: [PATCH 0860/1030] :bug: use the right key Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/integrate_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 79dd10fb8f..a24ebba3a5 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -93,6 +93,6 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data.get("app"), + "hosts": instance.context.data["hostName"], "task_types": task.get("type") } From 04397ccd2f8791a54917d505d1453a7a7e7e74cf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 19 Aug 2022 13:10:31 +0200 Subject: [PATCH 0861/1030] OP-3723 - changed source files to 8K 16K was causing memory issues on some machines. --- openpype/hosts/photoshop/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 64decbb957..60ae575b0a 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -144,7 +144,7 @@ class ExtractReview(openpype.api.Extractor): used as a source for thumbnail or review mov. """ # 16384x16384 actually didn't work because int overflow - max_ffmpeg_size = 16000 + max_ffmpeg_size = 8192 Image.MAX_IMAGE_PIXELS = None first_url = os.path.join(staging_dir, processed_img_names[0]) with Image.open(first_url) as im: From d4bfbe3b9e1510d358f162628170cd29c145a198 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 19 Aug 2022 14:03:06 +0200 Subject: [PATCH 0862/1030] Updated missed occurence Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 9d952586d2..0bad981fdf 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -635,7 +635,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): try: result = response.json() - except json.decoder.JSONDecodeError: + except JSONDecodeError: msg = "Broken response {}. ".format(response) msg += "Try restarting the Deadline Webservice." self.log.warning(msg, exc_info=True) From ba45c7b1694a27005c7f78a47f2e90179bdd11b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 16:14:41 +0200 Subject: [PATCH 0863/1030] improving code readability --- openpype/plugins/publish/collect_otio_subset_resources.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 9c19f8a78e..3387cd1176 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -121,10 +121,8 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): otio.schema.ImageSequenceReference ): is_sequence = True - else: - # for OpenTimelineIO 0.12 and older - if metadata.get("padding"): - is_sequence = True + elif metadata.get("padding"): + is_sequence = True self.log.info( "frame_start-frame_end: {}-{}".format(frame_start, frame_end)) From 102965ea69f3ae738dd92af2c39ec9bc8ae577d4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 16:15:16 +0200 Subject: [PATCH 0864/1030] editorial fixing handles to int and adding speed attribute --- openpype/pipeline/editorial.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/editorial.py b/openpype/pipeline/editorial.py index f62a1842e0..564d78ea6f 100644 --- a/openpype/pipeline/editorial.py +++ b/openpype/pipeline/editorial.py @@ -263,16 +263,17 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "retime": True, "speed": time_scalar, "timewarps": time_warp_nodes, - "handleStart": round(handle_start), - "handleEnd": round(handle_end) + "handleStart": int(round(handle_start)), + "handleEnd": int(round(handle_end)) } } returning_dict = { "mediaIn": media_in_trimmed, "mediaOut": media_out_trimmed, - "handleStart": round(handle_start), - "handleEnd": round(handle_end) + "handleStart": int(round(handle_start)), + "handleEnd": int(round(handle_end)), + "speed": time_scalar } # add version data only if retime From 869c9255ff266e90ec2f95abae67c234263beefb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 16:15:43 +0200 Subject: [PATCH 0865/1030] flame: improving extractor of subsets --- .../publish/extract_subset_resources.py | 124 ++++++++++++++++-- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index d34f5d5854..432bc3b500 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -8,6 +8,9 @@ import pyblish.api import openpype.api from openpype.hosts.flame import api as opfapi from openpype.hosts.flame.api import MediaInfoFile +from openpype.pipeline.editorial import ( + get_media_range_with_retimes +) import flame @@ -65,20 +68,50 @@ class ExtractSubsetResources(openpype.api.Extractor): # get configured workfile frame start/end (handles excluded) frame_start = instance.data["frameStart"] # get media source first frame - source_first_frame = instance.data["sourceFirstFrame"] + source_first_frame = instance.data["sourceFirstFrame"] # 1001 # get timeline in/out of segment clip_in = instance.data["clipIn"] clip_out = instance.data["clipOut"] + # get retimed attributres + retimed_data = self._get_retimed_attributes(instance) + self.log.debug("_ retimed_data: {}".format( + pformat(retimed_data) + )) + # get individual keys + r_handle_start = retimed_data["handle_start"] + r_handle_end = retimed_data["handle_end"] + r_source_dur = retimed_data["source_duration"] + r_speed = retimed_data["speed"] + r_handles = max(r_handle_start, r_handle_end) + # get handles value - take only the max from both handle_start = instance.data["handleStart"] - handle_end = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] handles = max(handle_start, handle_end) + include_handles = instance.data.get("includeHandles") + self.log.debug("_ include_handles: {}".format(include_handles)) # get media source range with handles source_start_handles = instance.data["sourceStartH"] source_end_handles = instance.data["sourceEndH"] + # retime if needed + if r_speed != 1.0: + source_start_handles = ( + instance.data["sourceStart"] - r_handle_start) + source_end_handles = ( + source_start_handles + # TODO: duration exclude 1 - might be problem + + (r_source_dur - 1) + + r_handle_start + + r_handle_end + ) + + self.log.debug("_ source_start_handles: {}".format( + source_start_handles)) + self.log.debug("_ source_end_handles: {}".format( + source_end_handles)) # create staging dir path staging_dir = self.staging_dir(instance) @@ -93,6 +126,19 @@ class ExtractSubsetResources(openpype.api.Extractor): } export_presets.update(self.export_presets_mapping) + # set versiondata if any retime + version_data = retimed_data.get("version_data") + + if version_data: + instance.data["versionData"].update(version_data) + + if instance.data.get("versionData"): + if r_speed != 1.0: + instance.data["versionData"].update({ + "frameStart": source_start_handles + r_handle_start, + "frameEnd": source_end_handles - r_handle_end, + }) + # loop all preset names and for unique_name, preset_config in export_presets.items(): modify_xml_data = {} @@ -117,14 +163,22 @@ class ExtractSubsetResources(openpype.api.Extractor): # get frame range with handles for representation range frame_start_handle = frame_start - handle_start + if include_handles: + if r_speed == 1.0: + frame_start_handle = frame_start + else: + frame_start_handle = ( + frame_start - handle_start) + r_handle_start + + self.log.debug("_ frame_start_handle: {}".format( + frame_start_handle)) # calculate duration with handles source_duration_handles = ( - source_end_handles - source_start_handles) + source_end_handles - source_start_handles) + 1 - # define in/out marks - in_mark = (source_start_handles - source_first_frame) + 1 - out_mark = in_mark + source_duration_handles + self.log.debug("_ source_duration_handles: {}".format( + source_duration_handles)) exporting_clip = None name_patern_xml = "_{}.".format( @@ -142,19 +196,28 @@ class ExtractSubsetResources(openpype.api.Extractor): "__{}.").format( unique_name) - # change in/out marks to timeline in/out + # only for h264 with baked retime in_mark = clip_in - out_mark = clip_out + out_mark = clip_out + 1 + + modify_xml_data["nbHandles"] = handles else: + in_mark = (source_start_handles - source_first_frame) + 1 + out_mark = in_mark + source_duration_handles exporting_clip = self.import_clip(clip_path) exporting_clip.name.set_value("{}_{}".format( asset_name, segment_name)) + modify_xml_data["nbHandles"] = ( + handles if r_speed == 1.0 else r_handles) # add xml tags modifications modify_xml_data.update({ + # TODO: handles only to Sequence preset + # TODO: enable Start frame attribute "exportHandles": True, - "nbHandles": handles, - "startFrame": frame_start, + "startFrame": frame_start_handle, + # enum position low start from 0 + "frameIndex": 0, "namePattern": name_patern_xml }) @@ -162,6 +225,12 @@ class ExtractSubsetResources(openpype.api.Extractor): # add any xml overrides collected form segment.comment modify_xml_data.update(instance.data["xml_overrides"]) + self.log.debug(pformat(modify_xml_data)) + self.log.debug("_ sequence publish {}".format( + export_type == "Sequence Publish")) + self.log.debug("_ in_mark: {}".format(in_mark)) + self.log.debug("_ out_mark: {}".format(out_mark)) + export_kwargs = {} # validate xml preset file is filled if preset_file == "": @@ -283,7 +352,7 @@ class ExtractSubsetResources(openpype.api.Extractor): representation_data.update({ "frameStart": frame_start_handle, "frameEnd": ( - frame_start_handle + source_duration_handles), + frame_start_handle + source_duration_handles) - 1, "fps": instance.data["fps"] }) @@ -303,6 +372,39 @@ class ExtractSubsetResources(openpype.api.Extractor): self.log.debug("All representations: {}".format( pformat(instance.data["representations"]))) + def _get_retimed_attributes(self, instance): + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + include_handles = instance.data.get("includeHandles") + self.log.debug("_ include_handles: {}".format(include_handles)) + + # get basic variables + otio_clip = instance.data["otioClip"] + otio_avalable_range = otio_clip.available_range() + available_duration = otio_avalable_range.duration.value + self.log.debug( + ">> available_duration: {}".format(available_duration)) + + # get available range trimmed with processed retimes + retimed_attributes = get_media_range_with_retimes( + otio_clip, handle_start, handle_end) + self.log.debug( + ">> retimed_attributes: {}".format(retimed_attributes)) + + r_media_in = int(retimed_attributes["mediaIn"]) + r_media_out = int(retimed_attributes["mediaOut"]) + version_data = retimed_attributes.get("versionData") + + return { + "version_data": version_data, + "handle_start": int(retimed_attributes["handleStart"]), + "handle_end": int(retimed_attributes["handleEnd"]), + "source_duration": ( + (r_media_out - r_media_in) + 1 + ), + "speed": float(retimed_attributes["speed"]) + } + def _should_skip(self, preset_config, clip_path, unique_name): # get activating attributes activated_preset = preset_config["active"] From faec36f1d65292121af9be129b2857d7d100a60f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 16:34:37 +0200 Subject: [PATCH 0866/1030] code cleanup --- .../plugins/publish/extract_subset_resources.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 432bc3b500..2f4f90fe55 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -84,7 +84,6 @@ class ExtractSubsetResources(openpype.api.Extractor): r_handle_end = retimed_data["handle_end"] r_source_dur = retimed_data["source_duration"] r_speed = retimed_data["speed"] - r_handles = max(r_handle_start, r_handle_end) # get handles value - take only the max from both handle_start = instance.data["handleStart"] @@ -183,6 +182,7 @@ class ExtractSubsetResources(openpype.api.Extractor): exporting_clip = None name_patern_xml = "_{}.".format( unique_name) + if export_type == "Sequence Publish": # change export clip to sequence exporting_clip = flame.duplicate(sequence_clip) @@ -199,25 +199,22 @@ class ExtractSubsetResources(openpype.api.Extractor): # only for h264 with baked retime in_mark = clip_in out_mark = clip_out + 1 - - modify_xml_data["nbHandles"] = handles + modify_xml_data.update({ + "exportHandles": True, + "nbHandles": handles + }) else: in_mark = (source_start_handles - source_first_frame) + 1 out_mark = in_mark + source_duration_handles exporting_clip = self.import_clip(clip_path) exporting_clip.name.set_value("{}_{}".format( asset_name, segment_name)) - modify_xml_data["nbHandles"] = ( - handles if r_speed == 1.0 else r_handles) # add xml tags modifications modify_xml_data.update({ - # TODO: handles only to Sequence preset - # TODO: enable Start frame attribute - "exportHandles": True, - "startFrame": frame_start_handle, # enum position low start from 0 "frameIndex": 0, + "startFrame": frame_start_handle, "namePattern": name_patern_xml }) From 8d08d5966a5eb213d4a8de57bf497cda83ccb631 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 17:39:59 +0200 Subject: [PATCH 0867/1030] cleaning code --- .../publish/extract_subset_resources.py | 48 ++++--------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 2f4f90fe55..ddf126c445 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -50,7 +50,6 @@ class ExtractSubsetResources(openpype.api.Extractor): export_presets_mapping = {} def process(self, instance): - if not self.keep_original_representation: # remove previeous representation if not needed instance.data["representations"] = [] @@ -68,7 +67,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # get configured workfile frame start/end (handles excluded) frame_start = instance.data["frameStart"] # get media source first frame - source_first_frame = instance.data["sourceFirstFrame"] # 1001 + source_first_frame = instance.data["sourceFirstFrame"] # get timeline in/out of segment clip_in = instance.data["clipIn"] @@ -76,9 +75,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # get retimed attributres retimed_data = self._get_retimed_attributes(instance) - self.log.debug("_ retimed_data: {}".format( - pformat(retimed_data) - )) + # get individual keys r_handle_start = retimed_data["handle_start"] r_handle_end = retimed_data["handle_end"] @@ -90,7 +87,6 @@ class ExtractSubsetResources(openpype.api.Extractor): handle_end = instance.data["handleEnd"] handles = max(handle_start, handle_end) include_handles = instance.data.get("includeHandles") - self.log.debug("_ include_handles: {}".format(include_handles)) # get media source range with handles source_start_handles = instance.data["sourceStartH"] @@ -101,17 +97,11 @@ class ExtractSubsetResources(openpype.api.Extractor): instance.data["sourceStart"] - r_handle_start) source_end_handles = ( source_start_handles - # TODO: duration exclude 1 - might be problem + (r_source_dur - 1) + r_handle_start + r_handle_end ) - self.log.debug("_ source_start_handles: {}".format( - source_start_handles)) - self.log.debug("_ source_end_handles: {}".format( - source_end_handles)) - # create staging dir path staging_dir = self.staging_dir(instance) @@ -125,18 +115,20 @@ class ExtractSubsetResources(openpype.api.Extractor): } export_presets.update(self.export_presets_mapping) + if not instance.data.get("versionData"): + instance.data["versionData"] = {} + # set versiondata if any retime version_data = retimed_data.get("version_data") if version_data: instance.data["versionData"].update(version_data) - if instance.data.get("versionData"): - if r_speed != 1.0: - instance.data["versionData"].update({ - "frameStart": source_start_handles + r_handle_start, - "frameEnd": source_end_handles - r_handle_end, - }) + if r_speed != 1.0: + instance.data["versionData"].update({ + "frameStart": source_start_handles + r_handle_start, + "frameEnd": source_end_handles - r_handle_end, + }) # loop all preset names and for unique_name, preset_config in export_presets.items(): @@ -176,9 +168,6 @@ class ExtractSubsetResources(openpype.api.Extractor): source_duration_handles = ( source_end_handles - source_start_handles) + 1 - self.log.debug("_ source_duration_handles: {}".format( - source_duration_handles)) - exporting_clip = None name_patern_xml = "_{}.".format( unique_name) @@ -222,9 +211,6 @@ class ExtractSubsetResources(openpype.api.Extractor): # add any xml overrides collected form segment.comment modify_xml_data.update(instance.data["xml_overrides"]) - self.log.debug(pformat(modify_xml_data)) - self.log.debug("_ sequence publish {}".format( - export_type == "Sequence Publish")) self.log.debug("_ in_mark: {}".format(in_mark)) self.log.debug("_ out_mark: {}".format(out_mark)) @@ -264,7 +250,6 @@ class ExtractSubsetResources(openpype.api.Extractor): thumb_frame_number = int(in_mark + ( source_duration_handles / 2)) - self.log.debug("__ in_mark: {}".format(in_mark)) self.log.debug("__ thumb_frame_number: {}".format( thumb_frame_number )) @@ -276,9 +261,6 @@ class ExtractSubsetResources(openpype.api.Extractor): "out_mark": out_mark }) - self.log.debug("__ modify_xml_data: {}".format( - pformat(modify_xml_data) - )) preset_path = opfapi.modify_preset_file( preset_orig_xml_path, staging_dir, modify_xml_data) @@ -366,21 +348,13 @@ class ExtractSubsetResources(openpype.api.Extractor): # at the end remove the duplicated clip flame.delete(exporting_clip) - self.log.debug("All representations: {}".format( - pformat(instance.data["representations"]))) def _get_retimed_attributes(self, instance): handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - include_handles = instance.data.get("includeHandles") - self.log.debug("_ include_handles: {}".format(include_handles)) # get basic variables otio_clip = instance.data["otioClip"] - otio_avalable_range = otio_clip.available_range() - available_duration = otio_avalable_range.duration.value - self.log.debug( - ">> available_duration: {}".format(available_duration)) # get available range trimmed with processed retimes retimed_attributes = get_media_range_with_retimes( @@ -412,8 +386,6 @@ class ExtractSubsetResources(openpype.api.Extractor): unique_name, activated_preset, filter_path_regex ) ) - self.log.debug( - "__ clip_path: `{}`".format(clip_path)) # skip if not activated presete if not activated_preset: From b15471501a44a09e5e205460aea6b6fc0c365e91 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Aug 2022 17:41:11 +0200 Subject: [PATCH 0868/1030] hound suggestions --- openpype/hosts/flame/api/lib.py | 1 - openpype/hosts/flame/plugins/publish/extract_subset_resources.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index a5ae3c4468..94c46fe937 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1,7 +1,6 @@ import sys import os import re -import sys import json import pickle import clique diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index ddf126c445..8a03ba119c 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -348,7 +348,6 @@ class ExtractSubsetResources(openpype.api.Extractor): # at the end remove the duplicated clip flame.delete(exporting_clip) - def _get_retimed_attributes(self, instance): handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] From 5d14fdd1cef6eb2d40925efeadfbcab3af219ca5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 19:02:10 +0200 Subject: [PATCH 0869/1030] fix _reset_crashed --- openpype/tools/settings/settings/categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 2e3c6d9dda..fd95b4ca71 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -701,7 +701,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.breadcrumbs_model.set_entity(None) def _on_reset_success(self): - self._reset_crashed = True + self._reset_crashed = False if not self.save_btn.isEnabled(): self.save_btn.setEnabled(self._edit_mode) From 890d1becaa8a4fcc597977d6b0cbe25e21bf34d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Aug 2022 19:11:00 +0200 Subject: [PATCH 0870/1030] moved dialog to separated file --- openpype/tools/settings/settings/dialogs.py | 115 ++++++++++++++++++++ openpype/tools/settings/settings/window.py | 93 +--------------- 2 files changed, 116 insertions(+), 92 deletions(-) create mode 100644 openpype/tools/settings/settings/dialogs.py diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py new file mode 100644 index 0000000000..dea056b89d --- /dev/null +++ b/openpype/tools/settings/settings/dialogs.py @@ -0,0 +1,115 @@ +from Qt import QtWidgets, QtCore + + +class BaseInfoDialog(QtWidgets.QDialog): + width = 600 + height = 400 + + def __init__(self, message, title, info_obj, parent=None): + super(BaseInfoDialog, self).__init__(parent) + self._result = 0 + self._info_obj = info_obj + + self.setWindowTitle(title) + + message_label = QtWidgets.QLabel(message, self) + message_label.setWordWrap(True) + + separator_widget_1 = QtWidgets.QFrame(self) + separator_widget_2 = QtWidgets.QFrame(self) + for separator_widget in ( + separator_widget_1, + separator_widget_2 + ): + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(1) + separator_widget.setMaximumHeight(1) + + other_information = QtWidgets.QWidget(self) + other_information_layout = QtWidgets.QFormLayout(other_information) + other_information_layout.setContentsMargins(0, 0, 0, 0) + for label, value in ( + ("Username", info_obj.username), + ("Host name", info_obj.hostname), + ("Host IP", info_obj.hostip), + ("System name", info_obj.system_name), + ("Local ID", info_obj.local_id), + ("Time Stamp", info_obj.timestamp), + ): + other_information_layout.addRow( + label, + QtWidgets.QLabel(value, other_information) + ) + + footer_widget = QtWidgets.QWidget(self) + buttons_widget = QtWidgets.QWidget(footer_widget) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons = self.get_buttons(buttons_widget) + for button in buttons: + buttons_layout.addWidget(button, 1) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(buttons_widget, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(separator_widget_1, 0) + layout.addStretch(1) + layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) + layout.addStretch(1) + layout.addWidget(separator_widget_2, 0) + layout.addWidget(footer_widget, 0) + + def showEvent(self, event): + super(BaseInfoDialog, self).showEvent(event) + self.resize(self.width, self.height) + + def result(self): + return self._result + + def get_buttons(self, parent): + return [] + + +class SettingsUIOpenedElsewhere(BaseInfoDialog): + def __init__(self, info_obj, parent=None): + title = "Someone else has opened Settings UI" + message = ( + "Someone else has opened Settings UI which could cause data loss." + " Please contact the person on the other side." + "

    You can continue in view-only mode." + " All changes in view mode will be lost." + "

    You can take control which will cause that" + " all changes of settings on the other side will be lost.
    " + ) + super(SettingsUIOpenedElsewhere, self).__init__( + message, title, info_obj, parent + ) + + def _on_take_control(self): + self._result = 1 + self.close() + + def _on_view_mode(self): + self._result = 0 + self.close() + + def get_buttons(self, parent): + take_control_btn = QtWidgets.QPushButton( + "Take control", parent + ) + view_mode_btn = QtWidgets.QPushButton( + "View only", parent + ) + + take_control_btn.clicked.connect(self._on_take_control) + view_mode_btn.clicked.connect(self._on_view_mode) + + return [ + take_control_btn, + view_mode_btn + ] diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index fcbcd129d0..2750785535 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -12,6 +12,7 @@ from openpype.settings.lib import ( closed_settings_ui, ) +from .dialogs import SettingsUIOpenedElsewhere from .categories import ( CategoryState, SystemWidget, @@ -348,95 +349,3 @@ class MainWidget(QtWidgets.QWidget): return return super(MainWidget, self).keyPressEvent(event) - - -class SettingsUIOpenedElsewhere(QtWidgets.QDialog): - def __init__(self, info_obj, parent=None): - super(SettingsUIOpenedElsewhere, self).__init__(parent) - - self._result = 0 - - self.setWindowTitle("Someone else has opened Settings UI") - - message_label = QtWidgets.QLabel(( - "Someone else has opened Settings UI which could cause data loss." - " Please contact the person on the other side." - "

    You can open the UI in view-only mode." - " All changes in view mode will be lost." - "

    You can take the control which will cause that" - " all changes of settings on the other side will be lost.
    " - ), self) - message_label.setWordWrap(True) - - separator_widget_1 = QtWidgets.QFrame(self) - separator_widget_2 = QtWidgets.QFrame(self) - for separator_widget in ( - separator_widget_1, - separator_widget_2 - ): - separator_widget.setObjectName("Separator") - separator_widget.setMinimumHeight(1) - separator_widget.setMaximumHeight(1) - - other_information = QtWidgets.QWidget(self) - other_information_layout = QtWidgets.QFormLayout(other_information) - other_information_layout.setContentsMargins(0, 0, 0, 0) - for label, value in ( - ("Username", info_obj.username), - ("Host name", info_obj.hostname), - ("Host IP", info_obj.hostip), - ("System name", info_obj.system_name), - ("Local ID", info_obj.local_id), - ("Time Stamp", info_obj.timestamp), - ): - other_information_layout.addRow( - label, - QtWidgets.QLabel(value, other_information) - ) - - footer_widget = QtWidgets.QWidget(self) - buttons_widget = QtWidgets.QWidget(footer_widget) - - take_control_btn = QtWidgets.QPushButton( - "Take control", buttons_widget - ) - view_mode_btn = QtWidgets.QPushButton( - "View only", buttons_widget - ) - - buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.addWidget(take_control_btn, 1) - buttons_layout.addWidget(view_mode_btn, 1) - - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addStretch(1) - footer_layout.addWidget(buttons_widget, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_label, 0) - layout.addWidget(separator_widget_1, 0) - layout.addStretch(1) - layout.addWidget(other_information, 0, QtCore.Qt.AlignHCenter) - layout.addStretch(1) - layout.addWidget(separator_widget_2, 0) - layout.addWidget(footer_widget, 0) - - take_control_btn.clicked.connect(self._on_take_control) - view_mode_btn.clicked.connect(self._on_view_mode) - - def result(self): - return self._result - - def _on_take_control(self): - self._result = 1 - self.close() - - def _on_view_mode(self): - self._result = 0 - self.close() - - def showEvent(self, event): - super(SettingsUIOpenedElsewhere, self).showEvent(event) - self.resize(600, 400) From 2ea277992ea217ff9d4b65cd3b36dd5ad2218f8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Aug 2022 21:06:05 +0200 Subject: [PATCH 0871/1030] update ftrack cli commands --- website/docs/module_ftrack.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 667782754f..ad9cf75e8f 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -13,7 +13,7 @@ Ftrack is currently the main project management option for OpenPype. This docume ## Prepare Ftrack for OpenPype ### Server URL -If you want to connect Ftrack to OpenPype you might need to make few changes in Ftrack settings. These changes would take a long time to do manually, so we prepared a few Ftrack actions to help you out. First, you'll need to launch OpenPype settings, enable [Ftrack module](admin_settings_system.md#Ftrack), and enter the address to your Ftrack server. +If you want to connect Ftrack to OpenPype you might need to make few changes in Ftrack settings. These changes would take a long time to do manually, so we prepared a few Ftrack actions to help you out. First, you'll need to launch OpenPype settings, enable [Ftrack module](admin_settings_system.md#Ftrack), and enter the address to your Ftrack server. ### Login Once your server is configured, restart OpenPype and you should be prompted to enter your [Ftrack credentials](artist_ftrack.md#How-to-use-Ftrack-in-OpenPype) to be able to run our Ftrack actions. If you are already logged in to Ftrack in your browser, it is enough to press `Ftrack login` and it will connect automatically. @@ -26,7 +26,7 @@ You can only use our Ftrack Actions and publish to Ftrack if each artist is logg ### Custom Attributes After successfully connecting OpenPype with you Ftrack, you can right click on any project in Ftrack and you should see a bunch of actions available. The most important one is called `OpenPype Admin` and contains multiple options inside. -To prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Create/Update Custom Attributes](manager_ftrack_actions.md#create-update-avalon-attributes), which creates and sets the Custom Attributes necessary for OpenPype to function. +To prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Create/Update Custom Attributes](manager_ftrack_actions.md#create-update-avalon-attributes), which creates and sets the Custom Attributes necessary for OpenPype to function. @@ -34,7 +34,7 @@ To prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Ftrack Event Server is the key to automation of many tasks like _status change_, _thumbnail update_, _automatic synchronization to Avalon database_ and many more. Event server should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with enough certainty. ### Running event server -There are specific launch arguments for event server. With `openpype_console eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. +There are specific launch arguments for event server. With `openpype_console module ftrack eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. @@ -53,7 +53,7 @@ There are specific launch arguments for event server. With `openpype_console eve - **`--ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee"`** : User's API key - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ -So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. +So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe module ftrack eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe module ftrack eventserver`. @@ -72,7 +72,7 @@ We do not recommend setting your Ftrack user and api key environments in a persi ### Where to run event server -We recommend you to run event server on stable server machine with ability to connect to Avalon database and Ftrack web server. Best practice we recommend is to run event server as service. It can be Windows or Linux. +We recommend you to run event server on stable server machine with ability to connect to Avalon database and Ftrack web server. Best practice we recommend is to run event server as service. It can be Windows or Linux. :::important Event server should **not** run more than once! It may cause major issues. @@ -99,11 +99,10 @@ Event server should **not** run more than once! It may cause major issues. - add content to the file: ```sh #!/usr/bin/env bash -export OPENPYPE_DEBUG=1 export OPENPYPE_MONGO= pushd /mnt/path/to/openpype -./openpype_console eventserver --ftrack-user --ftrack-api-key +./openpype_console module ftrack eventserver --ftrack-user --ftrack-api-key --debug ``` - change file permission: `sudo chmod 0755 /opt/openpype/run_event_server.sh` @@ -140,14 +139,13 @@ WantedBy=multi-user.target - create service file: `openpype-ftrack-eventserver.bat` -- add content to the service file: +- add content to the service file: ```sh @echo off -set OPENPYPE_DEBUG=1 set OPENPYPE_MONGO= pushd \\path\to\openpype -openpype_console.exe eventserver --ftrack-user --ftrack-api-key +openpype_console.exe module ftrack eventserver --ftrack-user --ftrack-api-key --debug ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual @@ -174,7 +172,7 @@ This event updates entities on their changes Ftrack. When new entity is created Deleting an entity by Ftrack's default is not processed for security reasons _(to delete entity use [Delete Asset/Subset action](manager_ftrack_actions.md#delete-asset-subset))_. ::: -### Synchronize Hierarchical and Entity Attributes +### Synchronize Hierarchical and Entity Attributes Auto-synchronization of hierarchical attributes from Ftrack entities. @@ -190,7 +188,7 @@ Change status of next task from `Not started` to `Ready` when previous task is a Multiple detailed rules for next task update can be configured in the settings. -### Delete Avalon ID from new entity +### Delete Avalon ID from new entity Is used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is created. @@ -215,7 +213,7 @@ This event handler allows setting of different status to a first created Asset V This is useful for example if first version publish doesn't contain any actual reviewable work, but is only used for roundtrip conform check, in which case this version could receive status `pending conform` instead of standard `pending review` ### Update status on next task -Change status on next task by task types order when task status state changed to "Done". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored. +Change status on next task by task types order when task status state changed to "Done". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored. ## Publish plugins @@ -238,7 +236,7 @@ Add Ftrack Family: enabled #### Advanced adding if additional families present -In special cases adding 'ftrack' based on main family ('Families' set higher) is not enough. +In special cases adding 'ftrack' based on main family ('Families' set higher) is not enough. (For example upload to Ftrack for 'plate' main family should only happen if 'review' is contained in instance 'families', not added in other cases. ) -![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png) \ No newline at end of file +![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png) From 752f1cde1c307398fa56ada929f8299ee97face5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Aug 2022 21:10:28 +0200 Subject: [PATCH 0872/1030] removed outdated repositories information --- website/docs/system_introduction.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/website/docs/system_introduction.md b/website/docs/system_introduction.md index 71c5d64aa8..db297b527a 100644 --- a/website/docs/system_introduction.md +++ b/website/docs/system_introduction.md @@ -48,24 +48,3 @@ to the table - Some DCCs do not support using Environment variables in file paths. This will make it very hard to maintain full multiplatform compatibility as well variable storage roots. - Relying on VPN connection and using it to work directly of network storage will be painfully slow. - - -## Repositories - -### [OpenPype](https://github.com/pypeclub/pype) - -This is where vast majority of the code that works with your data lives. It acts -as Avalon-Config, if we're speaking in avalon terms. - -Avalon gives us the ability to work with a certain host, say Maya, in a standardized manner, but OpenPype defines **how** we work with all the data, allows most of the behavior to be configured on a very granular level and provides a comprehensive build and installation tools for it. - -Thanks to that, we are able to maintain one codebase for vast majority of the features across all our clients deployments while keeping the option to tailor the pipeline to each individual studio. - -### [Avalon-core](https://github.com/pypeclub/avalon-core) - -Avalon-core is the heart of OpenPype. It provides the base functionality including key GUIs (albeit expanded and modified by us), database connection, standards for data structures, working with entities and some universal tools. - -Avalon is being actively developed and maintained by a community of studios and TDs from around the world, with Pype Club team being an active contributor as well. - -Due to the extensive work we've done on OpenPype and the need to react quickly to production needs, we -maintain our own fork of avalon-core, which is kept up to date with upstream changes as much as possible. From 40efddb9b40a3d74d136dd9a06647fd2b73ded91 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Aug 2022 21:21:28 +0200 Subject: [PATCH 0873/1030] removed eventserver from global cli commands --- website/docs/admin_openpype_commands.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 53fc12410f..cf45138fa9 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -40,7 +40,6 @@ For more information [see here](admin_use.md#run-openpype). | module | Run command line arguments for modules. | | | repack-version | Tool to re-create version zip. | [📑](#repack-version-arguments) | | tray | Launch OpenPype Tray. | [📑](#tray-arguments) -| eventserver | This should be ideally used by system service (such as systemd or upstart on linux and window service). | [📑](#eventserver-arguments) | | launch | Launch application in Pype environment. | [📑](#launch-arguments) | | publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) | | extractenvironments | Extract environment variables for entered context to a json file. | [📑](#extractenvironments-arguments) | @@ -57,25 +56,7 @@ For more information [see here](admin_use.md#run-openpype). openpype_console tray ``` --- -### `launch` arguments {#eventserver-arguments} -You have to set either proper environment variables to provide URL and credentials or use -option to specify them. -| Argument | Description | -| --- | --- | -| `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) | -| `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) | -| `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) | -| `--legacy` | run event server without mongo storing | -| `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) | -| `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) | - -To run ftrack event server: -```shell -openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= -``` - ---- ### `launch` arguments {#launch-arguments} | Argument | Description | From ce4c00d20a677c08bcd046ec24a0a3ba6c9e4792 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Aug 2022 21:46:21 +0200 Subject: [PATCH 0874/1030] add information about naming limitations to key concepts --- website/docs/artist_concepts.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/docs/artist_concepts.md b/website/docs/artist_concepts.md index 9005cffe87..f67ab89b9c 100644 --- a/website/docs/artist_concepts.md +++ b/website/docs/artist_concepts.md @@ -10,6 +10,8 @@ sidebar_label: Key Concepts In our pipeline all the main entities the project is made from are internally considered *'Assets'*. Episode, sequence, shot, character, prop, etc. All of these behave identically in the pipeline. Asset names need to be absolutely unique within the project because they are their key identifier. +OpenPype has limitation regarging duplicated names. Name of assets must be unique across whole project. + ### Subset Usually, an asset needs to be created in multiple *'flavours'*. A character might have multiple different looks, model needs to be published in different resolutions, a standard animation rig might not be usable in a crowd system and so on. 'Subsets' are here to accommodate all this variety that might be needed within a single asset. A model might have subset: *'main'*, *'proxy'*, *'sculpt'*, while data of *'look'* family could have subsets *'main'*, *'dirty'*, *'damaged'*. Subsets have some recommendations for their names, but ultimately it's up to the artist to use them for separation of publishes when needed. @@ -24,6 +26,11 @@ A numbered iteration of a given subset. Each version contains at least one [repr Each published variant can come out of the software in multiple representations. All of them hold exactly the same data, but in different formats. A model, for example, might be saved as `.OBJ`, Alembic, Maya geometry or as all of them, to be ready for pickup in any other applications supporting these formats. + +#### Naming convention + +At this moment names of assets, tasks, subsets or representations can contain only letters, numbers and underscore. + ### Family Each published [subset][3b89d8e0] can have exactly one family assigned to it. Family determines the type of data that the subset holds. Family doesn't dictate the file type, but can enforce certain technical specifications. For example OpenPype default configuration expects `model` family to only contain geometry without any shaders or joints when it is published. From 2591c1fad5355b824198389dae7c918061e67125 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Aug 2022 21:46:36 +0200 Subject: [PATCH 0875/1030] add link to key concepts into system introduction --- website/docs/system_introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/system_introduction.md b/website/docs/system_introduction.md index db297b527a..b8a2cea487 100644 --- a/website/docs/system_introduction.md +++ b/website/docs/system_introduction.md @@ -17,7 +17,7 @@ various usage scenarios. You can find detailed breakdown of technical requirements [here](dev_requirements), but in general OpenPype should be able to operate in most studios fairly quickly. The main obstacles are usually related to workflows and habits, that -might now be fully compatible with what OpenPype is expecting or enforcing. +might now be fully compatible with what OpenPype is expecting or enforcing. It is recommended to go through artists [key concepts](artist_concepts) to get idea about basics. Keep in mind that if you run into any workflows that are not supported, it's usually just because we haven't hit that particular case and it can most likely be added upon request. From 2b4e3ef9fad40d9573b3dd37b73d68dba0ad1d3e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 20 Aug 2022 03:56:03 +0000 Subject: [PATCH 0876/1030] [Automated] Bump version --- CHANGELOG.md | 16 ++++++++++++++-- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19993ad75..65a3cb27e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog +## [3.14.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.0...HEAD) + +**🚀 Enhancements** + +- Ftrack: More logs related to auto sync value change [\#3671](https://github.com/pypeclub/OpenPype/pull/3671) + +**🐛 Bug fixes** + +- RoyalRender: handle host name that is not set [\#3695](https://github.com/pypeclub/OpenPype/pull/3695) + ## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...3.14.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.0-nightly.1...3.14.0) **🆕 New features** @@ -25,7 +37,6 @@ - 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) -- Integrator: Don't force to have dot before frame [\#3611](https://github.com/pypeclub/OpenPype/pull/3611) **🔀 Refactored code** @@ -70,6 +81,7 @@ - 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) diff --git a/openpype/version.py b/openpype/version.py index c28b480940..174aca1e6c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.0" +__version__ = "3.14.1-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index e670d0a2ff..e01cc71201 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.14.0" # OpenPype +version = "3.14.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 3beca31c61317c068e9d667c8b0817265c3e26f3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 10:57:54 +0200 Subject: [PATCH 0877/1030] OP-3723 - introduced max_downscale_size value to Settings Studios might want to set maximum size to resize for ffmpeg to work based on OS or resources. --- .../hosts/photoshop/plugins/publish/extract_review.py | 8 ++++---- .../settings/defaults/project_settings/photoshop.json | 1 + .../projects_schema/schema_project_photoshop.json | 9 +++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 60ae575b0a..5d37c86ed8 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -30,6 +30,7 @@ class ExtractReview(openpype.api.Extractor): jpg_options = None mov_options = None make_image_sequence = None + max_downscale_size = 8192 def process(self, instance): staging_dir = self.staging_dir(instance) @@ -143,14 +144,12 @@ class ExtractReview(openpype.api.Extractor): Ffmpeg has max size 16384x16384. Saved image(s) must be resized to be used as a source for thumbnail or review mov. """ - # 16384x16384 actually didn't work because int overflow - max_ffmpeg_size = 8192 Image.MAX_IMAGE_PIXELS = None first_url = os.path.join(staging_dir, processed_img_names[0]) with Image.open(first_url) as im: width, height = im.size - if width > max_ffmpeg_size or height > max_ffmpeg_size: + if width > self.max_downscale_size or height > self.max_downscale_size: resized_dir = os.path.join(staging_dir, "resized") os.mkdir(resized_dir) source_files_pattern = os.path.join(resized_dir, @@ -159,7 +158,8 @@ class ExtractReview(openpype.api.Extractor): source_url = os.path.join(staging_dir, file_name) with Image.open(source_url) as res_img: # 'thumbnail' automatically keeps aspect ratio - res_img.thumbnail((max_ffmpeg_size, max_ffmpeg_size), + res_img.thumbnail((self.max_downscale_size, + self.max_downscale_size), Image.ANTIALIAS) res_img.save(os.path.join(resized_dir, file_name)) diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index d9b7a8083f..758ac64a35 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -32,6 +32,7 @@ }, "ExtractReview": { "make_image_sequence": false, + "max_downscale_size": 8192, "jpg_options": { "tags": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index badf94229b..49860301b6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -186,6 +186,15 @@ "key": "make_image_sequence", "label": "Makes an image sequence instead of a flatten image" }, + { + "type": "number", + "key": "max_downscale_size", + "label": "Maximum size of sources for review", + "tooltip": "FFMpeg can only handle limited resolution for creation of review and/or thumbnail", + "minimum": 300, + "maximum": 16384, + "decimal": 0 + }, { "type": "dict", "collapsible": false, From f4106714aeabd13eec6d8085cb3f3be39feb4f00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:27:19 +0200 Subject: [PATCH 0878/1030] added dialogs for 2 other cases --- openpype/tools/settings/settings/dialogs.py | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py index dea056b89d..a3eed68ae3 100644 --- a/openpype/tools/settings/settings/dialogs.py +++ b/openpype/tools/settings/settings/dialogs.py @@ -113,3 +113,67 @@ class SettingsUIOpenedElsewhere(BaseInfoDialog): take_control_btn, view_mode_btn ] + + +class SettingsLastSavedChanged(BaseInfoDialog): + width = 500 + height = 300 + + def __init__(self, info_obj, parent=None): + title = "Settings has changed" + message = ( + "Settings has changed while you had opened this settings session." + "

    It is recommended to refresh settings" + " and re-apply changes in the new session." + ) + super(SettingsLastSavedChanged, self).__init__( + message, title, info_obj, parent + ) + + def _on_save(self): + self._result = 1 + self.close() + + def _on_close(self): + self._result = 0 + self.close() + + def get_buttons(self, parent): + close_btn = QtWidgets.QPushButton( + "Close", parent + ) + save_btn = QtWidgets.QPushButton( + "Save anyway", parent + ) + + close_btn.clicked.connect(self._on_close) + save_btn.clicked.connect(self._on_save) + + return [ + close_btn, + save_btn + ] + + +class SettingsControlTaken(BaseInfoDialog): + width = 500 + height = 300 + + def __init__(self, info_obj, parent=None): + title = "Settings control taken" + message = ( + "Someone took control over your settings." + "

    It is not possible to save changes of currently" + " opened session. Copy changes you want to keep and hit refresh." + ) + super(SettingsControlTaken, self).__init__( + message, title, info_obj, parent + ) + + def _on_confirm(self): + self.close() + + def get_buttons(self, parent): + confirm_btn = QtWidgets.QPushButton("Understand", parent) + confirm_btn.clicked.connect(self._on_confirm) + return [confirm_btn] From 537bfaa8d683213f03a78b962d1767698947c390 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:27:45 +0200 Subject: [PATCH 0879/1030] removed print --- openpype/tools/settings/settings/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 2750785535..a907a034d1 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -87,7 +87,6 @@ class SettingsController: ) def update_last_opened_info(self): - print("update_last_opened_info") last_opened_info = get_last_opened_info() enabled = False if ( From 80b5c0c064c47d4bab6e870759a2fc4caf54cfd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:28:00 +0200 Subject: [PATCH 0880/1030] categories have checks related to last saved settings --- .../tools/settings/settings/categories.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index fd95b4ca71..f4b2c13a12 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -36,6 +36,11 @@ from openpype.settings.entities.op_version_entity import ( ) from openpype.settings import SaveWarningExc +from openpype.settings.lib import ( + get_system_last_saved_info, + get_project_last_saved_info, +) +from .dialogs import SettingsLastSavedChanged, SettingsControlTaken from .widgets import ( ProjectListWidget, VersionAction @@ -205,6 +210,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if enabled is self._edit_mode: return + was_false = self._edit_mode is False self._edit_mode = enabled self.save_btn.setEnabled(enabled and not self._reset_crashed) @@ -218,6 +224,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.save_btn.setToolTip(tooltip) + # Reset when last saved information has changed + if was_false and not self._check_last_saved_info(): + self.reset() + @property def state(self): return self._state @@ -748,7 +758,24 @@ class SettingsCategoryWidget(QtWidgets.QWidget): """Callback on any tab widget save.""" return + def _check_last_saved_info(self): + raise NotImplementedError(( + "{} does not have implemented '_check_last_saved_info'" + ).format(self.__class__.__name__)) + def _save(self): + self._controller.update_last_opened_info() + if not self._controller.opened_info: + dialog = SettingsControlTaken(self._last_saved_info, self) + dialog.exec_() + return + + if not self._check_last_saved_info(): + dialog = SettingsLastSavedChanged(self._last_saved_info, self) + dialog.exec_() + if dialog.result() == 0: + return + # Don't trigger restart if defaults are modified if self.is_modifying_defaults: require_restart = False @@ -807,6 +834,13 @@ class SystemWidget(SettingsCategoryWidget): self._actions = [] super(SystemWidget, self).__init__(*args, **kwargs) + def _check_last_saved_info(self): + if self.is_modifying_defaults: + return True + + last_saved_info = get_system_last_saved_info() + return self._last_saved_info == last_saved_info + def contain_category_key(self, category): if category == "system_settings": return True @@ -821,6 +855,10 @@ class SystemWidget(SettingsCategoryWidget): ) entity.on_change_callbacks.append(self._on_entity_change) self.entity = entity + last_saved_info = None + if not self.is_modifying_defaults: + last_saved_info = get_system_last_saved_info() + self._last_saved_info = last_saved_info try: if self.is_modifying_defaults: entity.set_defaults_state() @@ -854,6 +892,13 @@ class ProjectWidget(SettingsCategoryWidget): def __init__(self, *args, **kwargs): super(ProjectWidget, self).__init__(*args, **kwargs) + def _check_last_saved_info(self): + if self.is_modifying_defaults: + return True + + last_saved_info = get_project_last_saved_info(self.project_name) + return self._last_saved_info == last_saved_info + def contain_category_key(self, category): if category in ("project_settings", "project_anatomy"): return True @@ -933,6 +978,11 @@ class ProjectWidget(SettingsCategoryWidget): entity.on_change_callbacks.append(self._on_entity_change) self.project_list_widget.set_entity(entity) self.entity = entity + + last_saved_info = None + if not self.is_modifying_defaults: + last_saved_info = get_project_last_saved_info(self.project_name) + self._last_saved_info = last_saved_info try: if self.is_modifying_defaults: self.entity.set_defaults_state() From 7cd6a423ead42bae21a2fa2e491bf32870402fe8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 11:55:04 +0200 Subject: [PATCH 0881/1030] fix attribute name --- openpype/settings/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 1b59531943..09f36aa16e 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -784,9 +784,9 @@ class MongoSettingsHandler(SettingsHandler): "last_saved_info": last_saved_info.to_document_data() } if not system_settings_doc: - self.collections.insert_one(new_system_settings_doc) + self.collection.insert_one(new_system_settings_doc) else: - self.collections.update_one( + self.collection.update_one( {"_id": system_settings_doc["_id"]}, {"$set": new_system_settings_doc} ) From c3fe68c58a40fcca6f998b1d17e67aa690404349 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 12:02:21 +0200 Subject: [PATCH 0882/1030] converted unreal to openpype module --- openpype/hosts/unreal/__init__.py | 26 ++++----------------- openpype/hosts/unreal/module.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 openpype/hosts/unreal/module.py diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 10e9c5100e..41222f4f94 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -1,24 +1,6 @@ -import os -import openpype.hosts -from openpype.lib.applications import Application +from .module import UnrealModule -def add_implementation_envs(env: dict, _app: Application) -> None: - """Modify environments to contain all required for implementation.""" - # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - - ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7" - unreal_plugin_path = os.path.join( - os.path.dirname(os.path.abspath(openpype.hosts.__file__)), - "unreal", "integration", ue_plugin - ) - if not env.get("OPENPYPE_UNREAL_PLUGIN"): - env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path - - # Set default environments if are not set via settings - defaults = { - "OPENPYPE_LOG_NO_COLORS": "True" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value +__all__ = ( + "UnrealModule", +) diff --git a/openpype/hosts/unreal/module.py b/openpype/hosts/unreal/module.py new file mode 100644 index 0000000000..a30c9e9e36 --- /dev/null +++ b/openpype/hosts/unreal/module.py @@ -0,0 +1,39 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class UnrealModule(OpenPypeModule, IHostModule): + name = "unreal" + host_name = "unreal" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, app) -> None: + """Modify environments to contain all required for implementation.""" + # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation + + ue_plugin = "UE_5.0" if app.name[:1] == "5" else "UE_4.7" + unreal_plugin_path = os.path.join( + UNREAL_ROOT_DIR, "integration", ue_plugin + ) + if not env.get("OPENPYPE_UNREAL_PLUGIN"): + env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path + + # Set default environments if are not set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(UNREAL_ROOT_DIR, "hooks") + ] From 44b9146e4fa43188a3065090797a05b319d58e59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 12:16:50 +0200 Subject: [PATCH 0883/1030] use Unreal host as class instead of module --- openpype/hosts/unreal/api/__init__.py | 4 ++- openpype/hosts/unreal/api/pipeline.py | 27 +++++++++++++++++++ .../UE_4.7/Content/Python/init_unreal.py | 4 ++- .../UE_5.0/Content/Python/init_unreal.py | 4 ++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index ede71aa218..870982f5f9 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -19,6 +19,7 @@ from .pipeline import ( show_tools_dialog, show_tools_popup, instantiate, + UnrealHost, ) __all__ = [ @@ -36,5 +37,6 @@ __all__ = [ "show_experimental_tools", "show_tools_dialog", "show_tools_popup", - "instantiate" + "instantiate", + "UnrealHost", ] diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index bbca7916d3..ee4282e357 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -14,6 +14,7 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools import openpype.hosts.unreal +from openpypr.host import HostBase, ILoadHost import unreal # noqa @@ -29,6 +30,32 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +class UnrealHost(HostBase, ILoadHost): + """Unreal host implementation. + + For some time this class will re-use functions from module based + implementation for backwards compatibility of older unreal projects. + """ + + name = "unreal" + + def install(self): + install() + + def get_containers(self): + return ls() + + def show_tools_popup(self): + """Show tools popup with actions leading to show other tools.""" + + show_tools_popup() + + def show_tools_dialog(self): + """Show tools dialog with actions leading to show other tools.""" + + show_tools_dialog() + + def install(): """Install Unreal configuration for OpenPype.""" print("-=" * 40) diff --git a/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py index 4bb03b07ed..b85f970699 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py @@ -3,7 +3,9 @@ import unreal openpype_detected = True try: from openpype.pipeline import install_host - from openpype.hosts.unreal import api as openpype_host + from openpype.hosts.unreal.api import UnrealHost + + openpype_host = UnrealHost() except ImportError as exc: openpype_host = None openpype_detected = False diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 4bb03b07ed..b85f970699 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -3,7 +3,9 @@ import unreal openpype_detected = True try: from openpype.pipeline import install_host - from openpype.hosts.unreal import api as openpype_host + from openpype.hosts.unreal.api import UnrealHost + + openpype_host = UnrealHost() except ImportError as exc: openpype_host = None openpype_detected = False From d98bc3509057f2c59a278656ba6360a10825b57d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 13:22:38 +0200 Subject: [PATCH 0884/1030] implemented 'get_workfile_extensions' in unreal module --- openpype/hosts/unreal/module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/unreal/module.py b/openpype/hosts/unreal/module.py index a30c9e9e36..aa08c8c130 100644 --- a/openpype/hosts/unreal/module.py +++ b/openpype/hosts/unreal/module.py @@ -37,3 +37,6 @@ class UnrealModule(OpenPypeModule, IHostModule): return [ os.path.join(UNREAL_ROOT_DIR, "hooks") ] + + def get_workfile_extensions(self): + return [".uproject"] From 969241426ad6d53e916b2c8d140742b9bb80f635 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:19:17 +0200 Subject: [PATCH 0885/1030] moved and modified 'compute_session_changes' into context tools --- openpype/pipeline/context_tools.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 5f763cd249..66bf33e821 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -445,3 +445,60 @@ def get_custom_workfile_template_from_session( session["AVALON_APP"], project_settings=project_settings ) + + +def compute_session_changes( + session, asset_doc, task_name, template_key=None +): + """Compute the changes for a session object on task under asset. + + Function does not change the session object, only returns changes. + + Args: + session (Dict[str, str]): The initial session to compute changes to. + This is required for computing the full Work Directory, as that + also depends on the values that haven't changed. + asset_doc (Dict[str, Any]): Asset document to switch to. + task_name (str): Name of task to switch to. + template_key (Union[str, None]): Prepare workfile template key in + anatomy templates. + + Returns: + Dict[str, str]: Changes in the Session dictionary. + """ + + changes = {} + + # Get asset document and asset + if not asset_doc: + task_name = None + asset_name = None + else: + asset_name = asset_doc["name"] + + # Detect any changes compared session + mapping = { + "AVALON_ASSET": asset_name, + "AVALON_TASK": task_name, + } + changes = { + key: value + for key, value in mapping.items() + if value != session.get(key) + } + if not changes: + return changes + + # Compute work directory (with the temporary changed session so far) + changed_session = session.copy() + changed_session.update(changes) + + workdir = None + if asset_doc: + workdir = get_workdir_from_session( + changed_session, template_key + ) + + changes["AVALON_WORKDIR"] = workdir + + return changes From 097546e429fa7c7afdb4abfb19ef8458979173c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:19:53 +0200 Subject: [PATCH 0886/1030] moved 'update_current_task' to context tools and renamed to 'change_current_context' --- openpype/pipeline/context_tools.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 66bf33e821..00fe353208 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -16,6 +16,7 @@ from openpype.client import ( get_asset_by_name, version_is_latest, ) +from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings @@ -502,3 +503,46 @@ def compute_session_changes( changes["AVALON_WORKDIR"] = workdir return changes + + +def change_current_context(asset_doc, task_name, template_key=None): + """Update active Session to a new task work area. + + This updates the live Session to a different task under asset. + + Args: + asset_doc (Dict[str, Any]): The asset document to set. + task_name (str): The task to set under asset. + template_key (Union[str, None]): Prepared template key to be used for + workfile template in Anatomy. + + Returns: + Dict[str, str]: The changed key, values in the current Session. + """ + + changes = compute_session_changes( + legacy_io.Session, + asset_doc, + task_name, + template_key=template_key + ) + + # Update the Session and environments. Pop from environments all keys with + # value set to None. + for key, value in changes.items(): + legacy_io.Session[key] = value + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + data = changes.copy() + # Convert env keys to human readable keys + data["project_name"] = legacy_io.Session["AVALON_PROJECT"] + data["asset_name"] = legacy_io.Session["AVALON_ASSET"] + data["task_name"] = legacy_io.Session["AVALON_TASK"] + + # Emit session change + emit_event("taskChanged", data) + + return changes From 6257fcb7774857e23abcabea6261193c8caf35af Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:34:43 +0200 Subject: [PATCH 0887/1030] marked 'compute_session_changes' and 'update_current_task' as deprecated in openpype.lib --- openpype/lib/avalon_context.py | 98 ++++++++++------------------------ 1 file changed, 27 insertions(+), 71 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index eed17fce9d..31fdf4c596 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -7,6 +7,8 @@ import logging import functools import warnings +import six + from openpype.client import ( get_project, get_assets, @@ -526,7 +528,7 @@ def template_data_from_session(session=None): return get_template_data_from_session(session) -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.compute_session_changes") def compute_session_changes( session, task=None, asset=None, app=None, template_key=None ): @@ -547,54 +549,24 @@ def compute_session_changes( Returns: dict: The required changes in the Session dictionary. + + Deprecated: + Function will be removed after release version 3.16.* """ - from openpype.pipeline.context_tools import get_workdir_from_session + from openpype.pipeline import legacy_io + from openpype.pipeline.context_tools import compute_session_changes - changes = dict() + if isinstance(asset, six.string_types): + project_name = legacy_io.active_project() + asset = get_asset_by_name(project_name, asset) - # If no changes, return directly - if not any([task, asset, app]): - return changes - - # Get asset document and asset - asset_document = None - asset_tasks = None - if isinstance(asset, dict): - # Assume asset database document - asset_document = asset - asset_tasks = asset_document.get("data", {}).get("tasks") - asset = asset["name"] - - if not asset_document or not asset_tasks: - # Assume asset name - project_name = session["AVALON_PROJECT"] - asset_document = get_asset_by_name( - project_name, asset, fields=["data.tasks"] - ) - assert asset_document, "Asset must exist" - - # Detect any changes compared session - mapping = { - "AVALON_ASSET": asset, - "AVALON_TASK": task, - "AVALON_APP": app, - } - changes = { - key: value - for key, value in mapping.items() - if value and value != session.get(key) - } - if not changes: - return changes - - # Compute work directory (with the temporary changed session so far) - _session = session.copy() - _session.update(changes) - - changes["AVALON_WORKDIR"] = get_workdir_from_session(_session) - - return changes + return compute_session_changes( + session, + asset, + task, + template_key + ) @deprecated("openpype.pipeline.context_tools.get_workdir_from_session") @@ -604,7 +576,7 @@ def get_workdir_from_session(session=None, template_key=None): return get_workdir_from_session(session, template_key) -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.change_current_context") def update_current_task(task=None, asset=None, app=None, template_key=None): """Update active Session to a new task work area. @@ -617,35 +589,19 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): Returns: dict: The changed key, values in the current Session. + + Deprecated: + Function will be removed after release version 3.16.* """ - changes = compute_session_changes( - legacy_io.Session, - task=task, - asset=asset, - app=app, - template_key=template_key - ) + from openpype.pipeline import legacy_io + from openpype.pipeline.context_tools import change_current_context - # Update the Session and environments. Pop from environments all keys with - # value set to None. - for key, value in changes.items(): - legacy_io.Session[key] = value - if value is None: - os.environ.pop(key, None) - else: - os.environ[key] = value + project_name = legacy_io.acitve_project() + if isinstance(asset, six.string_types): + asset = get_asset_by_name(project_name, asset) - data = changes.copy() - # Convert env keys to human readable keys - data["project_name"] = legacy_io.Session["AVALON_PROJECT"] - data["asset_name"] = legacy_io.Session["AVALON_ASSET"] - data["task_name"] = legacy_io.Session["AVALON_TASK"] - - # Emit session change - emit_event("taskChanged", data) - - return changes + return change_current_context(asset, task, template_key) @deprecated("openpype.client.get_workfile_info") From 38d2233b3f03ba8f151ecd74449f352ef41b8fea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:37:14 +0200 Subject: [PATCH 0888/1030] added or modified removement version in Deprecated category of deprecated functions --- openpype/lib/avalon_context.py | 69 ++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 31fdf4c596..ca8a04b9d0 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -180,7 +180,7 @@ def is_latest(representation): bool: Whether the representation is of latest version. Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.context_tools import is_representation_from_latest @@ -193,7 +193,7 @@ def any_outdated(): """Return whether the current scene has any outdated content. Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.load import any_outdated_containers @@ -214,7 +214,7 @@ def get_asset(asset_name=None): (MongoDB document) Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.context_tools import get_current_project_asset @@ -226,7 +226,7 @@ def get_asset(asset_name=None): def get_system_general_anatomy_data(system_settings=None): """ Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.template_data import get_general_template_data @@ -298,7 +298,7 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): dict: Last version document for entered. Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ if not project_name: @@ -346,6 +346,9 @@ def get_workfile_template_key_from_context( Raises: ValueError: When both 'dbcon' and 'project_name' were not passed. + + Deprecated: + Function will be removed after release version 3.16.* """ from openpype.pipeline.workfile import ( @@ -389,6 +392,9 @@ def get_workfile_template_key( Raises: ValueError: When both 'project_name' and 'project_settings' were not passed. + + Deprecated: + Function will be removed after release version 3.16.* """ from openpype.pipeline.workfile import get_workfile_template_key @@ -413,7 +419,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): dict: Data prepared for filling workdir template. Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.template_data import get_template_data @@ -449,6 +455,9 @@ def get_workdir_with_workdir_data( Raises: ValueError: When both `anatomy` and `project_name` are set to None. + + Deprecated: + Function will be removed after release version 3.15.* """ if not anatomy and not project_name: @@ -494,6 +503,9 @@ def get_workdir( Returns: TemplateResult: Workdir path. + + Deprecated: + Function will be removed after release version 3.15.* """ from openpype.pipeline.workfile import get_workdir @@ -520,7 +532,7 @@ def template_data_from_session(session=None): dict: All available data from session. Deprecated: - Function will be removed after release version 3.14.* + Function will be removed after release version 3.15.* """ from openpype.pipeline.context_tools import get_template_data_from_session @@ -571,6 +583,21 @@ def compute_session_changes( @deprecated("openpype.pipeline.context_tools.get_workdir_from_session") def get_workdir_from_session(session=None, template_key=None): + """Calculate workdir path based on session data. + + Args: + session (Union[None, Dict[str, str]]): Session to use. If not passed + current context session is used (from legacy_io). + template_key (Union[str, None]): Precalculate template key to define + workfile template name in Anatomy. + + Returns: + str: Workdir path. + + Deprecated: + Function will be removed after release version 3.16.* + """ + from openpype.pipeline.context_tools import get_workdir_from_session return get_workdir_from_session(session, template_key) @@ -620,6 +647,9 @@ def get_workfile_doc(asset_id, task_name, filename, dbcon=None): Returns: dict: Workfile document or None. + + Deprecated: + Function will be removed after release version 3.15.* """ # Use legacy_io if dbcon is not entered @@ -730,6 +760,11 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): @deprecated("openpype.pipeline.workfile.BuildWorkfile") def BuildWorkfile(): + """Build workfile class was moved to workfile pipeline. + + Deprecated: + Function will be removed after release version 3.16.* + """ from openpype.pipeline.workfile import BuildWorkfile return BuildWorkfile() @@ -772,10 +807,7 @@ def change_timer_to_current_context(): 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 + version 3.15.* """ from openpype.pipeline import legacy_io @@ -890,6 +922,9 @@ def get_custom_workfile_template_by_context( Returns: str: Path to template or None if none of profiles match current context. (Existence of formatted path is not validated.) + + Deprecated: + Function will be removed after release version 3.16.* """ if anatomy is None: @@ -948,6 +983,9 @@ def get_custom_workfile_template_by_string_context( Returns: str: Path to template or None if none of profiles match current context. (Existence of formatted path is not validated.) + + Deprecated: + Function will be removed after release version 3.16.* """ project_name = None @@ -982,6 +1020,9 @@ def get_custom_workfile_template(template_profiles): Returns: str: Path to template or None if none of profiles match current context. (Existence of formatted path is not validated.) + + Deprecated: + Function will be removed after release version 3.16.* """ from openpype.pipeline import legacy_io @@ -1010,6 +1051,9 @@ def get_last_workfile_with_version( Returns: tuple: Last workfile with version if there is any otherwise returns (None, None). + + Deprecated: + Function will be removed after release version 3.16.* """ from openpype.pipeline.workfile import get_last_workfile_with_version @@ -1036,6 +1080,9 @@ def get_last_workfile( Returns: str: Last or first workfile as filename of full path to filename. + + Deprecated: + Function will be removed after release version 3.16.* """ from openpype.pipeline.workfile import get_last_workfile From d74eb5961ee67a8b304e4fc616195b24021f9b96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:42:45 +0200 Subject: [PATCH 0889/1030] fix typo --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index ca8a04b9d0..0f4f04e4d3 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -624,7 +624,7 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): from openpype.pipeline import legacy_io from openpype.pipeline.context_tools import change_current_context - project_name = legacy_io.acitve_project() + project_name = legacy_io.active_project() if isinstance(asset, six.string_types): asset = get_asset_by_name(project_name, asset) From 867f91d9f4fff0e57ec185e6dfafeb311de9fe08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:42:52 +0200 Subject: [PATCH 0890/1030] removed unused import --- openpype/lib/avalon_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 0f4f04e4d3..f08adb5470 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -17,7 +17,6 @@ from openpype.client import ( get_workfile_info, ) from .profiles_filtering import filter_profiles -from .events import emit_event from .path_templates import StringTemplate legacy_io = None From 2fea675167c6f1a2441d014171918fd00c56288f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 14:58:52 +0200 Subject: [PATCH 0891/1030] use new functions in workfiles tool --- openpype/tools/workfiles/files_widget.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index a4109c511e..a5d5b14bb6 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -14,15 +14,15 @@ from openpype.lib import ( emit_event, create_workdir_extra_folders, ) -from openpype.lib.avalon_context import ( - update_current_task, - compute_session_changes -) from openpype.pipeline import ( registered_host, legacy_io, Anatomy, ) +from openpype.pipeline.context_tools import ( + compute_session_changes, + change_current_context +) from openpype.pipeline.workfile import get_workfile_template_key from .model import ( @@ -408,8 +408,8 @@ class FilesWidget(QtWidgets.QWidget): ) changes = compute_session_changes( session, - asset=self._get_asset_doc(), - task=self._task_name, + self._get_asset_doc(), + self._task_name, template_key=self.template_key ) session.update(changes) @@ -422,8 +422,8 @@ class FilesWidget(QtWidgets.QWidget): session = legacy_io.Session.copy() changes = compute_session_changes( session, - asset=self._get_asset_doc(), - task=self._task_name, + self._get_asset_doc(), + self._task_name, template_key=self.template_key ) if not changes: @@ -431,9 +431,9 @@ class FilesWidget(QtWidgets.QWidget): # to avoid any unwanted Task Changed callbacks to be triggered. return - update_current_task( - asset=self._get_asset_doc(), - task=self._task_name, + change_current_context( + self._get_asset_doc(), + self._task_name, template_key=self.template_key ) From 9b60b9faa89c8551c88977c42ad9f404869c7680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 15:19:26 +0200 Subject: [PATCH 0892/1030] change title if in view mode --- openpype/tools/settings/settings/window.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a907a034d1..77a2f64dac 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -105,6 +105,7 @@ class MainWidget(QtWidgets.QWidget): widget_width = 1000 widget_height = 600 + window_title = "OpenPype Settings" def __init__(self, user_role, parent=None, reset_on_show=True): super(MainWidget, self).__init__(parent) @@ -122,7 +123,7 @@ class MainWidget(QtWidgets.QWidget): self._password_dialog = None self.setObjectName("SettingsMainWidget") - self.setWindowTitle("OpenPype Settings") + self.setWindowTitle(self.window_title) self.resize(self.widget_width, self.widget_height) @@ -155,6 +156,11 @@ class MainWidget(QtWidgets.QWidget): self._shadow_widget = ShadowWidget("Working...", self) self._shadow_widget.setVisible(False) + controller.event_system.add_callback( + "edit.mode.changed", + self._edit_mode_changed + ) + header_tab_widget.currentChanged.connect(self._on_tab_changed) search_dialog.path_clicked.connect(self._on_search_path_clicked) @@ -301,6 +307,12 @@ class MainWidget(QtWidgets.QWidget): entity = widget.entity self._search_dialog.set_root_entity(entity) + def _edit_mode_changed(self, event): + title = self.window_title + if not event["edit_mode"]: + title += " [View only]" + self.setWindowTitle(title) + def _on_tab_changed(self): self._update_search_dialog() From 1b64160644a1af0a8fd4349814a1c5e3fe496b85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 15:21:48 +0200 Subject: [PATCH 0893/1030] use pretty time instead of timestamp --- openpype/tools/settings/settings/dialogs.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py index a3eed68ae3..f25374a48c 100644 --- a/openpype/tools/settings/settings/dialogs.py +++ b/openpype/tools/settings/settings/dialogs.py @@ -1,5 +1,7 @@ from Qt import QtWidgets, QtCore +from openpype.tools.utils.delegates import pretty_date + class BaseInfoDialog(QtWidgets.QDialog): width = 600 @@ -34,13 +36,17 @@ class BaseInfoDialog(QtWidgets.QDialog): ("Host IP", info_obj.hostip), ("System name", info_obj.system_name), ("Local ID", info_obj.local_id), - ("Time Stamp", info_obj.timestamp), ): other_information_layout.addRow( label, QtWidgets.QLabel(value, other_information) ) + timestamp_label = QtWidgets.QLabel( + pretty_date(info_obj.timestamp_obj), other_information + ) + other_information_layout.addRow("Time", timestamp_label) + footer_widget = QtWidgets.QWidget(self) buttons_widget = QtWidgets.QWidget(footer_widget) @@ -64,10 +70,27 @@ class BaseInfoDialog(QtWidgets.QDialog): layout.addWidget(separator_widget_2, 0) layout.addWidget(footer_widget, 0) + timestamp_timer = QtCore.QTimer() + timestamp_timer.setInterval(1000) + timestamp_timer.timeout.connect(self._on_timestamp_timer) + + self._timestamp_label = timestamp_label + self._timestamp_timer = timestamp_timer + def showEvent(self, event): super(BaseInfoDialog, self).showEvent(event) + self._timestamp_timer.start() self.resize(self.width, self.height) + def closeEvent(self, event): + self._timestamp_timer.stop() + super(BaseInfoDialog, self).closeEvent(event) + + def _on_timestamp_timer(self): + self._timestamp_label.setText( + pretty_date(self._info_obj.timestamp_obj) + ) + def result(self): return self._result From 5df9c1b41f99fd2d587355c54b5c15eef53ce588 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 16:48:32 +0200 Subject: [PATCH 0894/1030] Added default variant to workfile collectors for PS|AE Will only propagate in workfile subset (and final published name of workfile) if {variant} is used in subset name template. (By default it isn't.) --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 4 +++- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 9cb6900b0a..fef5448a4c 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -11,6 +11,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): label = "Collect After Effects Workfile Instance" order = pyblish.api.CollectorOrder + 0.1 + default_variant = "Main" + def process(self, context): existing_instance = None for instance in context: @@ -71,7 +73,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): family = "workfile" subset = get_subset_name_with_asset_doc( family, - "", + self.default_variant, context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index e4f0a07b34..6599f5c96e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -11,6 +11,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): label = "Collect Workfile" hosts = ["photoshop"] + default_variant = "Main" + def process(self, context): existing_instance = None for instance in context: @@ -22,7 +24,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): family = "workfile" subset = get_subset_name_with_asset_doc( family, - "", + self.default_variant, context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], From 9434cb43c1d480ea523093907ad0fbe2a4a24744 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 17:07:46 +0200 Subject: [PATCH 0895/1030] fix published workfile filtering --- openpype/tools/workfiles/model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/model.py b/openpype/tools/workfiles/model.py index d5b7cef339..9a7fd659a9 100644 --- a/openpype/tools/workfiles/model.py +++ b/openpype/tools/workfiles/model.py @@ -299,7 +299,6 @@ class PublishFilesModel(QtGui.QStandardItemModel): self.project_name, asset_ids=[self._asset_id], fields=["_id", "name"] - ) subset_ids = [subset_doc["_id"] for subset_doc in subset_docs] @@ -329,7 +328,9 @@ class PublishFilesModel(QtGui.QStandardItemModel): # extension extensions = [ext.replace(".", "") for ext in self._file_extensions] repre_docs = get_representations( - self.project_name, version_ids, extensions + self.project_name, + version_ids=version_ids, + context_filters={"ext": extensions} ) # Filter queried representations by task name if task is set From 45da6cf5d0fbdb31a91eef2115e04299860f5f48 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 17:50:06 +0200 Subject: [PATCH 0896/1030] Added possibility to propagate collected variant context.data["variant"] might be filled only by collect_batch_data, which should take precedence --- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 6599f5c96e..9cf6d5227e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -22,9 +22,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): break family = "workfile" + # context.data["variant"] might come only from collect_batch_data + variant = context.data.get("variant") or self.default_variant subset = get_subset_name_with_asset_doc( family, - self.default_variant, + variant, context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], From 20d08049672cd328f4a4ac093b01864ce61d270f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Aug 2022 17:50:59 +0200 Subject: [PATCH 0897/1030] Fix typo Co-authored-by: Simone Barbieri --- openpype/hosts/unreal/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index ee4282e357..d396b64072 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools import openpype.hosts.unreal -from openpypr.host import HostBase, ILoadHost +from openpype.host import HostBase, ILoadHost import unreal # noqa From 4a4712b02047ff61ea23a90b5348768458b4aa34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 17:55:57 +0200 Subject: [PATCH 0898/1030] Added default variant to new creator --- .../photoshop/plugins/create/workfile_creator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index 43302329f1..ce0245d5c6 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -11,6 +11,8 @@ class PSWorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" + default_variant = "Main" + def get_instance_attr_defs(self): return [] @@ -35,7 +37,6 @@ class PSWorkfileCreator(AutoCreator): existing_instance = instance break - variant = '' project_name = legacy_io.Session["AVALON_PROJECT"] asset_name = legacy_io.Session["AVALON_ASSET"] task_name = legacy_io.Session["AVALON_TASK"] @@ -43,15 +44,17 @@ class PSWorkfileCreator(AutoCreator): if existing_instance is None: 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 + self.default_variant, task_name, asset_doc, + project_name, host_name ) data = { "asset": asset_name, "task": task_name, - "variant": variant + "variant": self.default_variant } data.update(self.get_dynamic_data( - variant, task_name, asset_doc, project_name, host_name + self.default_variant, task_name, asset_doc, + project_name, host_name )) new_instance = CreatedInstance( @@ -67,7 +70,8 @@ class PSWorkfileCreator(AutoCreator): ): 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 + self.default_variant, task_name, asset_doc, + project_name, host_name ) existing_instance["asset"] = asset_name existing_instance["task"] = task_name From c0457a88ea7e67f55818fd4b19db9eec7f887ec4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 17:56:25 +0200 Subject: [PATCH 0899/1030] Added overwrite old subset name for different context --- openpype/hosts/photoshop/plugins/create/workfile_creator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index ce0245d5c6..e79d16d154 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -75,3 +75,4 @@ class PSWorkfileCreator(AutoCreator): ) existing_instance["asset"] = asset_name existing_instance["task"] = task_name + existing_instance["subset"] = subset_name From be568a0e4140312712ddfe3c8b2b4df573ffd279 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 18:03:17 +0200 Subject: [PATCH 0900/1030] Added default variant for workfile creator for AE --- .../plugins/create/workfile_creator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py index badb3675fd..3b6dee3b83 100644 --- a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -11,6 +11,8 @@ class AEWorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" + default_variant = "Main" + def get_instance_attr_defs(self): return [] @@ -35,7 +37,6 @@ class AEWorkfileCreator(AutoCreator): existing_instance = instance break - variant = '' project_name = legacy_io.Session["AVALON_PROJECT"] asset_name = legacy_io.Session["AVALON_ASSET"] task_name = legacy_io.Session["AVALON_TASK"] @@ -44,15 +45,17 @@ class AEWorkfileCreator(AutoCreator): if existing_instance is None: 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 + self.default_variant, task_name, asset_doc, + project_name, host_name ) data = { "asset": asset_name, "task": task_name, - "variant": variant + "variant": self.default_variant } data.update(self.get_dynamic_data( - variant, task_name, asset_doc, project_name, host_name + self.default_variant, task_name, asset_doc, + project_name, host_name )) new_instance = CreatedInstance( @@ -69,7 +72,8 @@ class AEWorkfileCreator(AutoCreator): ): 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 + self.default_variant, task_name, asset_doc, + project_name, host_name ) existing_instance["asset"] = asset_name existing_instance["task"] = task_name From 0e7c183c1d74380c5cc3b0ac843b5e5d82bf60d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Aug 2022 18:03:57 +0200 Subject: [PATCH 0901/1030] Added overwrite subset for different context in AE --- openpype/hosts/aftereffects/plugins/create/workfile_creator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py index 3b6dee3b83..f82d15b3c9 100644 --- a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -77,3 +77,4 @@ class AEWorkfileCreator(AutoCreator): ) existing_instance["asset"] = asset_name existing_instance["task"] = task_name + existing_instance["subset"] = subset_name From d91274bb98e470d501837bcd8f3feacc823a25ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:07:38 +0200 Subject: [PATCH 0902/1030] moved traypublish action into traypublisher host --- openpype/hosts/traypublisher/__init__.py | 6 +++++ .../traypublisher/module.py} | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 openpype/hosts/traypublisher/__init__.py rename openpype/{modules/traypublish_action.py => hosts/traypublisher/module.py} (70%) diff --git a/openpype/hosts/traypublisher/__init__.py b/openpype/hosts/traypublisher/__init__.py new file mode 100644 index 0000000000..4eb7bf3eef --- /dev/null +++ b/openpype/hosts/traypublisher/__init__.py @@ -0,0 +1,6 @@ +from .module import TrayPublishModule + + +__all__ = ( + "TrayPublishModule", +) diff --git a/openpype/modules/traypublish_action.py b/openpype/hosts/traypublisher/module.py similarity index 70% rename from openpype/modules/traypublish_action.py rename to openpype/hosts/traypublisher/module.py index 39163b8eb8..25012900bc 100644 --- a/openpype/modules/traypublish_action.py +++ b/openpype/hosts/traypublisher/module.py @@ -1,25 +1,24 @@ import os + +import click + from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules.interfaces import ITrayAction, IHostModule + +TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class TrayPublishAction(OpenPypeModule, ITrayAction): +class TrayPublishModule(OpenPypeModule, IHostModule, ITrayAction): label = "New Publish (beta)" name = "traypublish_tool" + host_name = "traypublish" def initialize(self, modules_settings): - import openpype self.enabled = True self.publish_paths = [ - os.path.join( - openpype.PACKAGE_DIR, - "hosts", - "traypublisher", - "plugins", - "publish" - ) + os.path.join(TRAYPUBLISH_ROOT_DIR, "plugins", "publish") ] self._experimental_tools = None @@ -29,7 +28,7 @@ class TrayPublishAction(OpenPypeModule, ITrayAction): self._experimental_tools = ExperimentalTools() def tray_menu(self, *args, **kwargs): - super(TrayPublishAction, self).tray_menu(*args, **kwargs) + super(TrayPublishModule, self).tray_menu(*args, **kwargs) traypublisher = self._experimental_tools.get("traypublisher") visible = False if traypublisher and traypublisher.enabled: @@ -45,5 +44,7 @@ class TrayPublishAction(OpenPypeModule, ITrayAction): self.publish_paths.extend(publish_paths) def run_traypublisher(self): - args = get_openpype_execute_args("traypublisher") + args = get_openpype_execute_args( + "module", "traypublish_tool", "launch" + ) run_detached_process(args) From 2a54de9b538ba4a29faaf3a82da043e276535877 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:07:52 +0200 Subject: [PATCH 0903/1030] added cli commands to traypublisher module --- openpype/hosts/traypublisher/module.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/hosts/traypublisher/module.py b/openpype/hosts/traypublisher/module.py index 25012900bc..6a088af635 100644 --- a/openpype/hosts/traypublisher/module.py +++ b/openpype/hosts/traypublisher/module.py @@ -48,3 +48,20 @@ class TrayPublishModule(OpenPypeModule, IHostModule, ITrayAction): "module", "traypublish_tool", "launch" ) run_detached_process(args) + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group(TrayPublishModule.name, help="TrayPublisher related commands.") +def cli_main(): + pass + + +@cli_main.command() +def launch(): + """Launch TrayPublish tool UI.""" + + from openpype.tools import traypublisher + + traypublisher.main() From e7000f0108d3a49934b1e7e3e5c20d2d63ffa7f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:08:03 +0200 Subject: [PATCH 0904/1030] removed global traypublisher cli command --- openpype/cli.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index ffe288040e..4b653ac43c 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -46,12 +46,6 @@ def standalonepublisher(): PypeCommands().launch_standalone_publisher() -@main.command() -def traypublisher(): - """Show new OpenPype Standalone publisher UI.""" - PypeCommands().launch_traypublisher() - - @main.command() def tray(): """Launch pype tray. From f9c49fcaefd04dbb5661f22193224456cf97da88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:12:06 +0200 Subject: [PATCH 0905/1030] better fill of module name --- openpype/hosts/traypublisher/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/module.py b/openpype/hosts/traypublisher/module.py index 6a088af635..92a2312fec 100644 --- a/openpype/hosts/traypublisher/module.py +++ b/openpype/hosts/traypublisher/module.py @@ -45,7 +45,7 @@ class TrayPublishModule(OpenPypeModule, IHostModule, ITrayAction): def run_traypublisher(self): args = get_openpype_execute_args( - "module", "traypublish_tool", "launch" + "module", self.name, "launch" ) run_detached_process(args) From ffaa0b7adf6816e12500506f5d77ff4ab1ade3f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:22:24 +0200 Subject: [PATCH 0906/1030] moved standalonepublish action into standalone publish host --- openpype/hosts/standalonepublisher/__init__.py | 6 ++++++ .../standalonepublisher/standalonepublish_module.py} | 0 2 files changed, 6 insertions(+) rename openpype/{modules/standalonepublish_action.py => hosts/standalonepublisher/standalonepublish_module.py} (100%) diff --git a/openpype/hosts/standalonepublisher/__init__.py b/openpype/hosts/standalonepublisher/__init__.py index e69de29bb2..64c6d995f7 100644 --- a/openpype/hosts/standalonepublisher/__init__.py +++ b/openpype/hosts/standalonepublisher/__init__.py @@ -0,0 +1,6 @@ +from standalonepublish_module import StandAlonePublishModule + + +__all__ = ( + "StandAlonePublishModule", +) diff --git a/openpype/modules/standalonepublish_action.py b/openpype/hosts/standalonepublisher/standalonepublish_module.py similarity index 100% rename from openpype/modules/standalonepublish_action.py rename to openpype/hosts/standalonepublisher/standalonepublish_module.py From 8c849670872d1d0be4ad5611e7c21c4d77fff8e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:23:20 +0200 Subject: [PATCH 0907/1030] modified standalone publish action to work also as host module --- .../standalonepublish_module.py | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/standalonepublisher/standalonepublish_module.py b/openpype/hosts/standalonepublisher/standalonepublish_module.py index ba53ce9b9e..2cd46ce342 100644 --- a/openpype/hosts/standalonepublisher/standalonepublish_module.py +++ b/openpype/hosts/standalonepublisher/standalonepublish_module.py @@ -1,26 +1,26 @@ import os import platform import subprocess + +import click + from openpype.lib import get_openpype_execute_args +from openpype.lib.execute import run_detached_process from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules.interfaces import ITrayAction, IHostModule + +STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class StandAlonePublishAction(OpenPypeModule, ITrayAction): +class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostModule): label = "Publish" name = "standalonepublish_tool" + host_name = "standalonepublisher" def initialize(self, modules_settings): - import openpype self.enabled = modules_settings[self.name]["enabled"] self.publish_paths = [ - os.path.join( - openpype.PACKAGE_DIR, - "hosts", - "standalonepublisher", - "plugins", - "publish" - ) + os.path.join(STANDALONEPUBLISH_ROOT_DIR, "plugins", "publish") ] def tray_init(self): @@ -31,19 +31,10 @@ class StandAlonePublishAction(OpenPypeModule, ITrayAction): def connect_with_modules(self, enabled_modules): """Collect publish paths from other modules.""" + publish_paths = self.manager.collect_plugin_paths()["publish"] self.publish_paths.extend(publish_paths) def run_standalone_publisher(self): - args = get_openpype_execute_args("standalonepublisher") - kwargs = {} - if platform.system().lower() == "darwin": - new_args = ["open", "-na", args.pop(0), "--args"] - new_args.extend(args) - args = new_args - - detached_process = getattr(subprocess, "DETACHED_PROCESS", None) - if detached_process is not None: - kwargs["creationflags"] = detached_process - - subprocess.Popen(args, **kwargs) + args = get_openpype_execute_args("module", self.name, "launch") + run_detached_process(args) From 9db2eb3cc0d85d706e5637ae19bfc4ae6f49f028 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:23:32 +0200 Subject: [PATCH 0908/1030] added cli commands for standalone publisher --- .../standalonepublish_module.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/hosts/standalonepublisher/standalonepublish_module.py b/openpype/hosts/standalonepublisher/standalonepublish_module.py index 2cd46ce342..2d0114dee1 100644 --- a/openpype/hosts/standalonepublisher/standalonepublish_module.py +++ b/openpype/hosts/standalonepublisher/standalonepublish_module.py @@ -38,3 +38,22 @@ class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostModule): def run_standalone_publisher(self): args = get_openpype_execute_args("module", self.name, "launch") run_detached_process(args) + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group( + StandAlonePublishModule.name, + help="StandalonePublisher related commands.") +def cli_main(): + pass + + +@cli_main.command() +def launch(): + """Launch StandalonePublisher tool UI.""" + + from openpype.tools import standalonepublish + + standalonepublish.main() From 6a271aae101d86e33eaffe6571b736d8b0ab8c88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:25:10 +0200 Subject: [PATCH 0909/1030] removed standalonepublisher from global cli commands --- openpype/cli.py | 6 ------ openpype/pype_commands.py | 5 ----- website/docs/admin_openpype_commands.md | 7 ------- 3 files changed, 18 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 4b653ac43c..398d1a94c0 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -40,12 +40,6 @@ def settings(dev): PypeCommands().launch_settings_gui(dev) -@main.command() -def standalonepublisher(): - """Show Pype Standalone publisher UI.""" - PypeCommands().launch_standalone_publisher() - - @main.command() def tray(): """Launch pype tray. diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index a447aa916b..66bf5e9bb4 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -76,11 +76,6 @@ class PypeCommands: import (run_webserver) return run_webserver(*args, **kwargs) - @staticmethod - def launch_standalone_publisher(): - from openpype.tools import standalonepublish - standalonepublish.main() - @staticmethod def launch_traypublisher(): from openpype.tools import traypublisher diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 53fc12410f..8345398e1d 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -48,7 +48,6 @@ For more information [see here](admin_use.md#run-openpype). | interactive | Start python like interactive console session. | | | projectmanager | Launch Project Manager UI | [📑](#projectmanager-arguments) | | settings | Open Settings UI | [📑](#settings-arguments) | -| standalonepublisher | Open Standalone Publisher UI | [📑](#standalonepublisher-arguments) | --- ### `tray` arguments {#tray-arguments} @@ -159,12 +158,6 @@ openpypeconsole settings ``` --- -### `standalonepublisher` arguments {#standalonepublisher-arguments} -`standalonepublisher` has no command-line arguments. -```shell -openpype_console standalonepublisher -``` - ### `repack-version` arguments {#repack-version-arguments} Takes path to unzipped and possibly modified OpenPype version. Files will be zipped, checksums recalculated and version will be determined by folder name From 986e4325c2379001bf33ca3729e8e9608ce9fe87 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:26:01 +0200 Subject: [PATCH 0910/1030] fix import --- openpype/hosts/standalonepublisher/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/__init__.py b/openpype/hosts/standalonepublisher/__init__.py index 64c6d995f7..394d5be397 100644 --- a/openpype/hosts/standalonepublisher/__init__.py +++ b/openpype/hosts/standalonepublisher/__init__.py @@ -1,4 +1,4 @@ -from standalonepublish_module import StandAlonePublishModule +from .standalonepublish_module import StandAlonePublishModule __all__ = ( From ab1e6c4e3dddacb0726c4eddea078af8fca42ebd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:30:27 +0200 Subject: [PATCH 0911/1030] removed unused imports --- openpype/hosts/standalonepublisher/standalonepublish_module.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/standalonepublish_module.py b/openpype/hosts/standalonepublisher/standalonepublish_module.py index 2d0114dee1..bf8e1d2c23 100644 --- a/openpype/hosts/standalonepublisher/standalonepublish_module.py +++ b/openpype/hosts/standalonepublisher/standalonepublish_module.py @@ -1,6 +1,4 @@ import os -import platform -import subprocess import click From 227a21c057412ee764facce5ba0ebec6bf19630c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:33:52 +0200 Subject: [PATCH 0912/1030] removed uninstall function --- openpype/hosts/tvpaint/api/__init__.py | 2 -- openpype/hosts/tvpaint/api/pipeline.py | 13 ------------- 2 files changed, 15 deletions(-) diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index c461b33f4b..43d411d8f9 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -6,7 +6,6 @@ from . import pipeline from . import plugin from .pipeline import ( install, - uninstall, maintained_selection, remove_instance, list_instances, @@ -33,7 +32,6 @@ __all__ = ( "plugin", "install", - "uninstall", "maintained_selection", "remove_instance", "list_instances", diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 0118c0104b..73e2c2335c 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -91,19 +91,6 @@ def install(): register_event_callback("application.exit", application_exit) -def uninstall(): - """Uninstall TVPaint-specific functionality. - - This function is called automatically on calling `uninstall_host()`. - """ - - log.info("OpenPype - Uninstalling TVPaint integration") - pyblish.api.deregister_host("tvpaint") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) - - def containerise( name, namespace, members, context, loader, current_containers=None ): From b9c175d9691bc07f3bc5db9f31604363edf7f969 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:39:01 +0200 Subject: [PATCH 0913/1030] converted tvpaint into module --- openpype/hosts/tvpaint/__init__.py | 26 +++++---------- openpype/hosts/tvpaint/tvpaint_module.py | 42 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 openpype/hosts/tvpaint/tvpaint_module.py diff --git a/openpype/hosts/tvpaint/__init__.py b/openpype/hosts/tvpaint/__init__.py index 09b7c52cd1..068631a010 100644 --- a/openpype/hosts/tvpaint/__init__.py +++ b/openpype/hosts/tvpaint/__init__.py @@ -1,20 +1,10 @@ -import os +from .tvpaint_module import ( + get_launch_script_path, + TVPaintModule, +) -def add_implementation_envs(env, _app): - """Modify environments to contain all required for implementation.""" - defaults = { - "OPENPYPE_LOG_NO_COLORS": "True" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - -def get_launch_script_path(): - current_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.join( - current_dir, - "api", - "launch_script.py" - ) +__all__ = ( + "get_launch_script_path", + "TVPaintModule", +) diff --git a/openpype/hosts/tvpaint/tvpaint_module.py b/openpype/hosts/tvpaint/tvpaint_module.py new file mode 100644 index 0000000000..a2471553a6 --- /dev/null +++ b/openpype/hosts/tvpaint/tvpaint_module.py @@ -0,0 +1,42 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +TVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def get_launch_script_path(): + return os.path.join( + TVPAINT_ROOT_DIR, + "api", + "launch_script.py" + ) + + + +class TVPaintModule(OpenPypeModule, IHostModule): + name = "tvpaint" + host_name = "tvpaint" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(env, _app): + """Modify environments to contain all required for implementation.""" + + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(TVPAINT_ROOT_DIR, "hooks") + ] + + def get_workfile_extensions(self): + return [".tvpp"] From c6f8b4559d249655f2b003982b8cf07f71fc70cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Aug 2022 18:45:05 +0200 Subject: [PATCH 0914/1030] import TVPAINT_ROOT_DIR in init --- openpype/hosts/tvpaint/__init__.py | 2 ++ openpype/hosts/tvpaint/tvpaint_module.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/__init__.py b/openpype/hosts/tvpaint/__init__.py index 068631a010..0a84b575dc 100644 --- a/openpype/hosts/tvpaint/__init__.py +++ b/openpype/hosts/tvpaint/__init__.py @@ -1,10 +1,12 @@ from .tvpaint_module import ( get_launch_script_path, TVPaintModule, + TVPAINT_ROOT_DIR, ) __all__ = ( "get_launch_script_path", "TVPaintModule", + "TVPAINT_ROOT_DIR", ) diff --git a/openpype/hosts/tvpaint/tvpaint_module.py b/openpype/hosts/tvpaint/tvpaint_module.py index a2471553a6..c29602babc 100644 --- a/openpype/hosts/tvpaint/tvpaint_module.py +++ b/openpype/hosts/tvpaint/tvpaint_module.py @@ -13,7 +13,6 @@ def get_launch_script_path(): ) - class TVPaintModule(OpenPypeModule, IHostModule): name = "tvpaint" host_name = "tvpaint" From d65607eedbc2a80121820cc8b6efc66cbc759006 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:27:26 +0200 Subject: [PATCH 0915/1030] removed unused imports --- openpype/hosts/tvpaint/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 73e2c2335c..427c927264 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -16,8 +16,6 @@ from openpype.pipeline import ( legacy_io, register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) From 5bf34ebed7c4c848681c484c50ec70bd4ebb728d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:29:27 +0200 Subject: [PATCH 0916/1030] fix project overrides save to mongo --- openpype/settings/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 09f36aa16e..79ec6248ac 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -957,7 +957,7 @@ class MongoSettingsHandler(SettingsHandler): if project_settings_doc: self.collection.update_one( {"_id": project_settings_doc["_id"]}, - new_project_settings_doc + {"$set": new_project_settings_doc} ) else: self.collection.insert_one(new_project_settings_doc) From 3176a0130d6b3ab110c0fd3c3616c6c56172df5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:40:27 +0200 Subject: [PATCH 0917/1030] replaced changelog and upgrade notes with releases information --- website/docs/admin_releases.md | 9 + website/docs/changelog.md | 1138 -------------------------------- website/docs/upgrade_notes.md | 165 ----- website/sidebars.js | 6 +- 4 files changed, 10 insertions(+), 1308 deletions(-) create mode 100644 website/docs/admin_releases.md delete mode 100644 website/docs/changelog.md delete mode 100644 website/docs/upgrade_notes.md diff --git a/website/docs/admin_releases.md b/website/docs/admin_releases.md new file mode 100644 index 0000000000..bba5a22110 --- /dev/null +++ b/website/docs/admin_releases.md @@ -0,0 +1,9 @@ +--- +id: admin_releases +title: Releases +sidebar_label: Releases +--- + +Information about releases can be found on GitHub [Releases page](https://github.com/pypeclub/OpenPype/releases). + +You can find features and bugfixes in the codebase or full changelog for advanced users. diff --git a/website/docs/changelog.md b/website/docs/changelog.md deleted file mode 100644 index 448592b930..0000000000 --- a/website/docs/changelog.md +++ /dev/null @@ -1,1138 +0,0 @@ ---- -id: changelog -title: Changelog -sidebar_label: Changelog ---- - -## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) -_**release date:** (2021-05-18)_ - -[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0) - -**Enhancements:** - -- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484)) -- TVPaint: Increment workfile version on successful publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) -- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) - -**Fixed bugs:** - -- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) -- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) -- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) -- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) - - -### [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) -_**release date:** (2021-05-06)_ - -[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3) - -**Fixed bugs:** - -- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) - -### [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) -_**release date:** (2021-05-04)_ - -[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) - -**Enhancements:** - -- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) - -### [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) -_**release date:** (2021-04-30)_ - -[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1) - -**Enhancements:** - -- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) -- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) -- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) -- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) -- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) -- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) -- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) - -**Fixed bugs:** - -- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) -- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) -- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) - -**Merged pull requests:** - -- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) -- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) - -## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) -_**release date:** (2021-04-20)_ - -[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0) - -**Enhancements:** - -- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) -- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) -- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) -- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) -- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298) -- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297) -- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234) -- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206) - -**Fixed bugs:** - -- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362) -- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308) -- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282) -- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194) -- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312) -- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303) -- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275) -- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242) -- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) -- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) -- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) -- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) -- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) - -## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) - - _**release date:** 2021-03-22_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0) - -**Enhancements:** - -- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167) -- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156) -- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152) -- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146) -- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142) -- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138) -- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127) -- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124) -- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117) -- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106) -- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101) -- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093) -- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088) -- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080) -- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078) -- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072) -- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008) -- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073) - - -**Fixed bugs:** - -- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174) -- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166) -- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163) -- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125) -- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090) -- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082) -- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067) -- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064) - -### [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) - - _**release date:** 2021-02-26_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3) - -**Enhancements:** - -- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053) -- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047) - -**Fixed bugs:** - -- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085) -- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059) -- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057) -- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046) - -### [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) - - _**release date:** 2021-02-19_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2) - -**Enhancements:** - -- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013) - -**Fixed bugs:** - -- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040) -- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036) -- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031) - -### [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) - - _**release date:** 2021-02-12_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1) - -**Enhancements:** - -- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011) -- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445) - -**Fixed bugs:** - -- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006) -- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922) - -## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) - - _**release date:** 2021-02-09_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0) - -**Enhancements:** - -- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932) -- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926) -- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894) -- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993) -- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986) -- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981) -- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965) -- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944) -- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941) -- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938) -- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903) -- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893) -- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891) -- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884) -- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881) -- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840) -- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824) -- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795) -- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695) - -**Fixed bugs:** - -- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995) -- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982) -- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960) -- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839) -- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990) -- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988) -- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984) -- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979) -- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972) -- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971) -- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967) -- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964) -- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959) -- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956) -- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954) -- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953) -- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933) -- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920) -- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909) -- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896) -- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889) -- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831) -- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822) - -**Merged pull requests:** - -- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934) - -### [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) - - _**release date:** 2021-01-15_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6) - -**Fixed bugs:** - -- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885) - -**Merged pull requests:** - -- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892) -- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869) - -### [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) - - _**release date:** 2021-01-06_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5) - -**Merged pull requests:** - -- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866) - -### [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) - - _**release date:** 2020-12-18_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4) - -**Merged pull requests:** - -- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837) - -### [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) - - _**release date:** 2020-12-16_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3) - -**Fixed bugs:** - -- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809) -- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807) -- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806) - -**Merged pull requests:** - -- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823) - -### [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) - - _**release date:** 2020-12-04_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2) - -**Enhancements:** - -- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767) - -**Fixed bugs:** - -- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768) -- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766) -- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763) - -**Merged pull requests:** - -- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781) -- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769) - -### [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) - - _**release date:** 2020-11-27_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1) - -**Enhancements:** - -- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770) -- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761) - -**Fixed bugs:** - -- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760) -- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754) -- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752) -- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748) -- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744) -- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742) - - - -## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) - - _**release date:** 2020-11-24_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0) - -**Enhancements:** - -- Ftrack: Event for syncing shot or asset status with tasks.[\#736](https://github.com/pypeclub/pype/pull/736) -- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721) -- Maya: Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678) -- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674) -- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719) -- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716) -- Standalone Publisher: Publish editorial from separate image sequences [\#699](https://github.com/pypeclub/pype/pull/699) -- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687) -- TV Paint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675) -- **TV Paint (Beta):** initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693) -- **After Effects (Beta):** base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667) -- Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666) - -**Fixed bugs:** - -- TVPaint: extract review fix [\#740](https://github.com/pypeclub/pype/pull/740) -- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738) -- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722) -- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682) - -**Deprecated:** - -- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717) -- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715) - - - - -### [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) - - _**release date:** 2020-11-19_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7) - -**Merged pull requests:** - -- fix\(SP\): getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729) - - - - -### [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) - - _**release date:** 2020-11-15_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.5...2.13.6) - -**Fixed bugs:** - -- Maya workfile version wasn't syncing with renders properly [\#711](https://github.com/pypeclub/pype/pull/711) -- Maya: Fix for publishing multiple cameras with review from the same scene [\#710](https://github.com/pypeclub/pype/pull/710) - - - - -### [2.13.5](https://github.com/pypeclub/pype/tree/2.13.5) - - _**release date:** 2020-11-12_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.4...2.13.5) - - -**Fixed bugs:** - -- Wrong thumbnail file was picked when publishing sequence in standalone publisher [\#703](https://github.com/pypeclub/pype/pull/703) -- Fix: Burnin data pass and FFmpeg tool check [\#701](https://github.com/pypeclub/pype/pull/701) - - - - -### [2.13.4](https://github.com/pypeclub/pype/tree/2.13.4) - - _**release date:** 2020-11-09_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.3...2.13.4) - - -**Fixed bugs:** - -- Photoshop unhiding hidden layers [\#688](https://github.com/pypeclub/pype/issues/688) -- Nuke: Favorite directories "shot dir" "project dir" - not working \#684 [\#685](https://github.com/pypeclub/pype/pull/685) - - - - - -### [2.13.3](https://github.com/pypeclub/pype/tree/2.13.3) - - _**release date:** _2020-11-03_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.2...2.13.3) - -**Fixed bugs:** - -- Fix ffmpeg executable path with spaces [\#680](https://github.com/pypeclub/pype/pull/680) -- Hotfix: Added default version number [\#679](https://github.com/pypeclub/pype/pull/679) - - - - -### [2.13.2](https://github.com/pypeclub/pype/tree/2.13.2) - - _**release date:** 2020-10-28_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.1...2.13.2) - -**Fixed bugs:** - -- Nuke: wrong conditions when fixing legacy write nodes [\#665](https://github.com/pypeclub/pype/pull/665) - - - - -### [2.13.1](https://github.com/pypeclub/pype/tree/2.13.1) - - _**release date:** 2020-10-23_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.0...2.13.1) - -**Fixed bugs:** - -- Photoshop: Layer name is not propagating to metadata [\#654](https://github.com/pypeclub/pype/issues/654) -- Photoshop: Loader in fails with "can't set attribute" [\#650](https://github.com/pypeclub/pype/issues/650) -- Hiero: Review video file adding one frame to the end [\#659](https://github.com/pypeclub/pype/issues/659) - - - -## [2.13.0](https://github.com/pypeclub/pype/tree/2.13.0) - - _**release date:** 2020-10-16_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.5...2.13.0) - -**Enhancements:** - -- Deadline Output Folder [\#636](https://github.com/pypeclub/pype/issues/636) -- Nuke Camera Loader [\#565](https://github.com/pypeclub/pype/issues/565) -- Deadline publish job shows publishing output folder [\#649](https://github.com/pypeclub/pype/pull/649) -- Get latest version in lib [\#642](https://github.com/pypeclub/pype/pull/642) -- Improved publishing of multiple representation from SP [\#638](https://github.com/pypeclub/pype/pull/638) -- TvPaint: launch shot work file from within Ftrack [\#631](https://github.com/pypeclub/pype/pull/631) -- Add mp4 support for RV action. [\#628](https://github.com/pypeclub/pype/pull/628) -- Maya: allow renders to have version synced with workfile [\#618](https://github.com/pypeclub/pype/pull/618) -- Renaming nukestudio host folder to hiero [\#617](https://github.com/pypeclub/pype/pull/617) -- Harmony: More efficient publishing [\#615](https://github.com/pypeclub/pype/pull/615) -- Ftrack server action improvement [\#608](https://github.com/pypeclub/pype/pull/608) -- Deadline user defaults to pype username if present [\#607](https://github.com/pypeclub/pype/pull/607) -- Standalone publisher now has icon [\#606](https://github.com/pypeclub/pype/pull/606) -- Nuke render write targeting knob improvement [\#603](https://github.com/pypeclub/pype/pull/603) -- Animated pyblish gui [\#602](https://github.com/pypeclub/pype/pull/602) -- Maya: Deadline - make use of asset dependencies optional [\#591](https://github.com/pypeclub/pype/pull/591) -- Nuke: Publishing, loading and updating alembic cameras [\#575](https://github.com/pypeclub/pype/pull/575) -- Maya: add look assigner to pype menu even if scriptsmenu is not available [\#573](https://github.com/pypeclub/pype/pull/573) -- Store task types in the database [\#572](https://github.com/pypeclub/pype/pull/572) -- Maya: Tiled EXRs to scanline EXRs render option [\#512](https://github.com/pypeclub/pype/pull/512) -- Fusion: basic integration refresh [\#452](https://github.com/pypeclub/pype/pull/452) - -**Fixed bugs:** - -- Burnin script did not propagate ffmpeg output [\#640](https://github.com/pypeclub/pype/issues/640) -- Pyblish-pype spacer in terminal wasn't transparent [\#646](https://github.com/pypeclub/pype/pull/646) -- Lib subprocess without logger [\#645](https://github.com/pypeclub/pype/pull/645) -- Nuke: prevent crash if we only have single frame in sequence [\#644](https://github.com/pypeclub/pype/pull/644) -- Burnin script logs better output [\#641](https://github.com/pypeclub/pype/pull/641) -- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639) -- review from imagesequence error [\#633](https://github.com/pypeclub/pype/pull/633) -- Hiero: wrong order of fps clip instance data collecting [\#627](https://github.com/pypeclub/pype/pull/627) -- Add source for review instances. [\#625](https://github.com/pypeclub/pype/pull/625) -- Task processing in event sync [\#623](https://github.com/pypeclub/pype/pull/623) -- sync to avalon doesn t remove renamed task [\#619](https://github.com/pypeclub/pype/pull/619) -- Intent publish setting wasn't working with default value [\#562](https://github.com/pypeclub/pype/pull/562) -- Maya: Updating a look where the shader name changed, leaves the geo without a shader [\#514](https://github.com/pypeclub/pype/pull/514) - - -### [2.12.5](https://github.com/pypeclub/pype/tree/2.12.5) - -_**release date:** 2020-10-14_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.4...2.12.5) - -**Fixed Bugs:** - -- Harmony: Disable application launch logic [\#637](https://github.com/pypeclub/pype/pull/637) - -### [2.12.4](https://github.com/pypeclub/pype/tree/2.12.4) - -_**release date:** 2020-10-08_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.3...2.12.4) - -**Fixed bugs:** - -- Sync to avalon doesn't remove renamed task [\#605](https://github.com/pypeclub/pype/issues/605) - - -**Merged pull requests:** - -- NukeStudio: small fixes [\#622](https://github.com/pypeclub/pype/pull/622) -- NukeStudio: broken order of plugins [\#620](https://github.com/pypeclub/pype/pull/620) - -### [2.12.3](https://github.com/pypeclub/pype/tree/2.12.3) - -_**release date:** 2020-10-06_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.2...2.12.3) - -**Fixed bugs:** - -- Harmony: empty scene contamination [\#583](https://github.com/pypeclub/pype/issues/583) -- Edit publishing in SP doesn't respect shot selection for publishing [\#542](https://github.com/pypeclub/pype/issues/542) -- Pathlib breaks compatibility with python2 hosts [\#281](https://github.com/pypeclub/pype/issues/281) -- Maya: fix maya scene type preset exception [\#569](https://github.com/pypeclub/pype/pull/569) -- Standalone publisher editorial plugins interfering [\#580](https://github.com/pypeclub/pype/pull/580) - -### [2.12.2](https://github.com/pypeclub/pype/tree/2.12.2) - -_**release date:** 2020-09-25_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.1...2.12.2) - -**Fixed bugs:** - -- Harmony: Saving heavy scenes will crash [\#507](https://github.com/pypeclub/pype/issues/507) -- Extract review a representation name with `\*\_burnin` [\#388](https://github.com/pypeclub/pype/issues/388) -- Hierarchy data was not considering active instances [\#551](https://github.com/pypeclub/pype/pull/551) - -### [2.12.1](https://github.com/pypeclub/pype/tree/2.12.1) - -_**release date:** 2020-09-15_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.12.0...2.12.1) - -**Fixed bugs:** - -- dependency security alert ! [\#484](https://github.com/pypeclub/pype/issues/484) -- Maya: RenderSetup is missing update [\#106](https://github.com/pypeclub/pype/issues/106) -- \ extract effects creates new instance [\#78](https://github.com/pypeclub/pype/issues/78) - - - - -## [2.12.0](https://github.com/pypeclub/pype/tree/2.12.0) ## - -_**release date:** 09 Sept 2020_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.8...2.12.0) - -**Enhancements:** - -- Pype now uses less mongo connections [\#509](https://github.com/pypeclub/pype/pull/509) -- Nuke: adding image loader [\#499](https://github.com/pypeclub/pype/pull/499) -- Completely new application launcher [\#443](https://github.com/pypeclub/pype/pull/443) -- Maya: Optional skip review on renders. [\#441](https://github.com/pypeclub/pype/pull/441) -- Ftrack: Option to push status from task to latest version [\#440](https://github.com/pypeclub/pype/pull/440) -- Maya: Properly containerize image plane loads. [\#434](https://github.com/pypeclub/pype/pull/434) -- Option to keep the review files. [\#426](https://github.com/pypeclub/pype/pull/426) -- Maya: Isolate models during preview publishing [\#425](https://github.com/pypeclub/pype/pull/425) -- Ftrack attribute group is backwards compatible [\#418](https://github.com/pypeclub/pype/pull/418) -- Maya: Publishing of tile renderings on Deadline [\#398](https://github.com/pypeclub/pype/pull/398) -- Slightly better logging gui [\#383](https://github.com/pypeclub/pype/pull/383) -- Standalonepublisher: editorial family features expansion [\#411](https://github.com/pypeclub/pype/pull/411) - -**Fixed bugs:** - -- Maya: Fix tile order for Draft Tile Assembler [\#511](https://github.com/pypeclub/pype/pull/511) -- Remove extra dash [\#501](https://github.com/pypeclub/pype/pull/501) -- Fix: strip dot from repre names in single frame renders [\#498](https://github.com/pypeclub/pype/pull/498) -- Better handling of destination during integrating [\#485](https://github.com/pypeclub/pype/pull/485) -- Fix: allow thumbnail creation for single frame renders [\#460](https://github.com/pypeclub/pype/pull/460) -- added missing argument to launch\_application in ftrack app handler [\#453](https://github.com/pypeclub/pype/pull/453) -- Burnins: Copy bit rate of input video to match quality. [\#448](https://github.com/pypeclub/pype/pull/448) -- Standalone publisher is now independent from tray [\#442](https://github.com/pypeclub/pype/pull/442) -- Bugfix/empty enumerator attributes [\#436](https://github.com/pypeclub/pype/pull/436) -- Fixed wrong order of "other" category collapssing in publisher [\#435](https://github.com/pypeclub/pype/pull/435) -- Multiple reviews where being overwritten to one. [\#424](https://github.com/pypeclub/pype/pull/424) -- Cleanup plugin fail on instances without staging dir [\#420](https://github.com/pypeclub/pype/pull/420) -- deprecated -intra parameter in ffmpeg to new `-g` [\#417](https://github.com/pypeclub/pype/pull/417) -- Delivery action can now work with entered path [\#397](https://github.com/pypeclub/pype/pull/397) - - - - - -### [2.11.8](https://github.com/pypeclub/pype/tree/2.11.8) ## - -_**release date:** 27 Aug 2020_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.7...2.11.8) - -**Fixed bugs:** - -- pyblish pype - other group is collapsed before plugins are done [\#431](https://github.com/pypeclub/pype/issues/431) -- Alpha white edges in harmony on PNGs [\#412](https://github.com/pypeclub/pype/issues/412) -- harmony image loader picks wrong representations [\#404](https://github.com/pypeclub/pype/issues/404) -- Clockify crash when response contain symbol not allowed by UTF-8 [\#81](https://github.com/pypeclub/pype/issues/81) - - - - -### [2.11.7](https://github.com/pypeclub/pype/tree/2.11.7) ## - -_**release date:** 21 Aug 2020_ - - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.6...2.11.7) - -**Fixed bugs:** - -- Clean Up Baked Movie [\#369](https://github.com/pypeclub/pype/issues/369) -- celaction last workfile wasn't picked up correctly [\#459](https://github.com/pypeclub/pype/pull/459) - - - -### [2.11.5](https://github.com/pypeclub/pype/tree/2.11.5) ## - -_**release date:** 13 Aug 2020_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.4...2.11.5) - -**Enhancements:** - -- Standalone publisher now only groups sequence if the extension is known [\#439](https://github.com/pypeclub/pype/pull/439) - -**Fixed bugs:** - -- Logs have been disable for editorial by default to speed up publishing [\#433](https://github.com/pypeclub/pype/pull/433) -- Various fixes for celaction [\#430](https://github.com/pypeclub/pype/pull/430) -- Harmony: invalid variable scope in validate scene settings [\#428](https://github.com/pypeclub/pype/pull/428) -- Harmomny: new representation name for audio was not accepted [\#427](https://github.com/pypeclub/pype/pull/427) - - - - -### [2.11.3](https://github.com/pypeclub/pype/tree/2.11.3) ## - -_**release date:** 4 Aug 2020_ - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.2...2.11.3) - -**Fixed bugs:** - -- Harmony: publishing performance issues [\#408](https://github.com/pypeclub/pype/pull/408) - - - - -## 2.11.0 ## - -_**release date:** 27 July 2020_ - -**new:** -- _(blender)_ namespace support [\#341](https://github.com/pypeclub/pype/pull/341) -- _(blender)_ start end frames [\#330](https://github.com/pypeclub/pype/pull/330) -- _(blender)_ camera asset [\#322](https://github.com/pypeclub/pype/pull/322) -- _(pype)_ toggle instances per family in pyblish GUI [\#320](https://github.com/pypeclub/pype/pull/320) -- _(pype)_ current release version is now shown in the tray menu [#379](https://github.com/pypeclub/pype/pull/379) - - -**improved:** -- _(resolve)_ tagging for publish [\#239](https://github.com/pypeclub/pype/issues/239) -- _(pype)_ Support publishing a subset of shots with standalone editorial [\#336](https://github.com/pypeclub/pype/pull/336) -- _(harmony)_ Basic support for palettes [\#324](https://github.com/pypeclub/pype/pull/324) -- _(photoshop)_ Flag outdated containers on startup and publish. [\#309](https://github.com/pypeclub/pype/pull/309) -- _(harmony)_ Flag Outdated containers [\#302](https://github.com/pypeclub/pype/pull/302) -- _(photoshop)_ Publish review [\#298](https://github.com/pypeclub/pype/pull/298) -- _(pype)_ Optional Last workfile launch [\#365](https://github.com/pypeclub/pype/pull/365) - - -**fixed:** -- _(premiere)_ workflow fixes [\#346](https://github.com/pypeclub/pype/pull/346) -- _(pype)_ pype-setup does not work with space in path [\#327](https://github.com/pypeclub/pype/issues/327) -- _(ftrack)_ Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/pype/issues/206) -- _(nuke)_ Priority was forced to 50 [\#345](https://github.com/pypeclub/pype/pull/345) -- _(nuke)_ Fix ValidateNukeWriteKnobs [\#340](https://github.com/pypeclub/pype/pull/340) -- _(maya)_ If camera attributes are connected, we can ignore them. [\#339](https://github.com/pypeclub/pype/pull/339) -- _(pype)_ stop appending of tools environment to existing env [\#337](https://github.com/pypeclub/pype/pull/337) -- _(ftrack)_ Ftrack timeout needs to look at AVALON\_TIMEOUT [\#325](https://github.com/pypeclub/pype/pull/325) -- _(harmony)_ Only zip files are supported. [\#310](https://github.com/pypeclub/pype/pull/310) -- _(pype)_ hotfix/Fix event server mongo uri [\#305](https://github.com/pypeclub/pype/pull/305) -- _(photoshop)_ Subset was not named or validated correctly. [\#304](https://github.com/pypeclub/pype/pull/304) - - - - - -## 2.10.0 ## - -_**release date:** 17 June 2020_ - -**new:** -- _(harmony)_ **Toon Boom Harmony** has been greatly extended to support rigging, scene build, animation and rendering workflows. [#270](https://github.com/pypeclub/pype/issues/270) [#271](https://github.com/pypeclub/pype/issues/271) [#190](https://github.com/pypeclub/pype/issues/190) [#191](https://github.com/pypeclub/pype/issues/191) [#172](https://github.com/pypeclub/pype/issues/172) [#168](https://github.com/pypeclub/pype/issues/168) -- _(pype)_ Added support for rudimentary **edl publishing** into individual shots. [#265](https://github.com/pypeclub/pype/issues/265) -- _(celaction)_ Simple **Celaction** integration has been added with support for workfiles and rendering. [#255](https://github.com/pypeclub/pype/issues/255) -- _(maya)_ Support for multiple job types when submitting to the farm. We can now render Maya or Standalone render jobs for Vray and Arnold (limited support for arnold) [#204](https://github.com/pypeclub/pype/issues/204) -- _(photoshop)_ Added initial support for Photoshop [#232](https://github.com/pypeclub/pype/issues/232) - -**improved:** -- _(blender)_ Updated support for rigs and added support Layout family [#233](https://github.com/pypeclub/pype/issues/233) [#226](https://github.com/pypeclub/pype/issues/226) -- _(premiere)_ It is now possible to choose different storage root for workfiles of different task types. [#255](https://github.com/pypeclub/pype/issues/255) -- _(maya)_ Support for unmerged AOVs in Redshift multipart EXRs [#197](https://github.com/pypeclub/pype/issues/197) -- _(pype)_ Pype repository has been refactored in preparation for 3.0 release [#169](https://github.com/pypeclub/pype/issues/169) -- _(deadline)_ All file dependencies are now passed to deadline from maya to prevent premature start of rendering if caches or textures haven't been coppied over yet. [#195](https://github.com/pypeclub/pype/issues/195) -- _(nuke)_ Script validation can now be made optional. [#194](https://github.com/pypeclub/pype/issues/194) -- _(pype)_ Publishing can now be stopped at any time. [#194](https://github.com/pypeclub/pype/issues/194) - -**fix:** -- _(pype)_ Pyblish-lite has been integrated into pype repository, plus various publishing GUI fixes. [#274](https://github.com/pypeclub/pype/issues/274) [#275](https://github.com/pypeclub/pype/issues/275) [#268](https://github.com/pypeclub/pype/issues/268) [#227](https://github.com/pypeclub/pype/issues/227) [#238](https://github.com/pypeclub/pype/issues/238) -- _(maya)_ Alembic extractor was getting wrong frame range type in certain scenarios [#254](https://github.com/pypeclub/pype/issues/254) -- _(maya)_ Attaching a render to subset in maya was not passing validation in certain scenarios [#256](https://github.com/pypeclub/pype/issues/256) -- _(ftrack)_ Various small fixes to ftrack sync [#263](https://github.com/pypeclub/pype/issues/263) [#259](https://github.com/pypeclub/pype/issues/259) -- _(maya)_ Look extraction is now able to skp invalid connections in shaders [#207](https://github.com/pypeclub/pype/issues/207) - - - - - -## 2.9.0 ## - -_**release date:** 25 May 2020_ - -**new:** -- _(pype)_ Support for **Multiroot projects**. You can now store project data on multiple physical or virtual storages and target individual publishes to these locations. For instance render can be stored on a faster storage than the rest of the project. [#145](https://github.com/pypeclub/pype/issues/145), [#38](https://github.com/pypeclub/pype/issues/38) -- _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) -- _(pype)_ OSX support is in public beta now. There are issues to be expected, but the main implementation should be functional. [#141](https://github.com/pypeclub/pype/issues/141) - - -**improved:** - -- _(pype)_ **Review extractor** has been completely rebuilt. It now supports granular filtering so you can create **multiple outputs** for different tasks, families or hosts. [#103](https://github.com/pypeclub/pype/issues/103), [#166](https://github.com/pypeclub/pype/issues/166), [#165](https://github.com/pypeclub/pype/issues/165) -- _(pype)_ **Burnin** generation had been extended to **support same multi-output filtering** as review extractor [#103](https://github.com/pypeclub/pype/issues/103) -- _(pype)_ Publishing file templates can now be specified in config for each individual family [#114](https://github.com/pypeclub/pype/issues/114) -- _(pype)_ Studio specific plugins can now be appended to pype standard publishing plugins. [#112](https://github.com/pypeclub/pype/issues/112) -- _(nukestudio)_ Reviewable clips no longer need to be previously cut, exported and re-imported to timeline. **Pype can now dynamically cut reviewable quicktimes** from continuous offline footage during publishing. [#23](https://github.com/pypeclub/pype/issues/23) -- _(deadline)_ Deadline can now correctly differentiate between staging and production pype. [#154](https://github.com/pypeclub/pype/issues/154) -- _(deadline)_ `PYPE_PYTHON_EXE` env variable can now be used to direct publishing to explicit python installation. [#120](https://github.com/pypeclub/pype/issues/120) -- _(nuke)_ Nuke now check for new version of loaded data on file open. [#140](https://github.com/pypeclub/pype/issues/140) -- _(nuke)_ frame range and limit checkboxes are now exposed on write node. [#119](https://github.com/pypeclub/pype/issues/119) - - - -**fix:** - -- _(nukestudio)_ Project Location was using backslashes which was breaking nukestudio native exporting in certains configurations [#82](https://github.com/pypeclub/pype/issues/82) -- _(nukestudio)_ Duplicity in hierarchy tags was prone to throwing publishing error [#130](https://github.com/pypeclub/pype/issues/130), [#144](https://github.com/pypeclub/pype/issues/144) -- _(ftrack)_ multiple stability improvements [#157](https://github.com/pypeclub/pype/issues/157), [#159](https://github.com/pypeclub/pype/issues/159), [#128](https://github.com/pypeclub/pype/issues/128), [#118](https://github.com/pypeclub/pype/issues/118), [#127](https://github.com/pypeclub/pype/issues/127) -- _(deadline)_ multipart EXRs were stopping review publishing on the farm. They are still not supported for automatic review generation, but the publish will go through correctly without the quicktime. [#155](https://github.com/pypeclub/pype/issues/155) -- _(deadline)_ If deadline is non-responsive it will no longer freeze host when publishing [#149](https://github.com/pypeclub/pype/issues/149) -- _(deadline)_ Sometimes deadline was trying to launch render before all the source data was coppied over. [#137](https://github.com/pypeclub/pype/issues/137) _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) -- _(nuke)_ Filepath knob wasn't updated properly. [#131](https://github.com/pypeclub/pype/issues/131) -- _(maya)_ When extracting animation, the "Write Color Set" options on the instance were not respected. [#108](https://github.com/pypeclub/pype/issues/108) -- _(maya)_ Attribute overrides for AOV only worked for the legacy render layers. Now it works for new render setup as well [#132](https://github.com/pypeclub/pype/issues/132) -- _(maya)_ Stability and usability improvements in yeti workflow [#104](https://github.com/pypeclub/pype/issues/104) - - - - - -## 2.8.0 ## - -_**release date:** 20 April 2020_ - -**new:** - -- _(pype)_ Option to generate slates from json templates. [PYPE-628] [#26](https://github.com/pypeclub/pype/issues/26) -- _(pype)_ It is now possible to automate loading of published subsets into any scene. Documentation will follow :). [PYPE-611] [#24](https://github.com/pypeclub/pype/issues/24) - -**fix:** - -- _(maya)_ Some Redshift render tokens could break publishing. [PYPE-778] [#33](https://github.com/pypeclub/pype/issues/33) -- _(maya)_ Publish was not preserving maya file extension. [#39](https://github.com/pypeclub/pype/issues/39) -- _(maya)_ Rig output validator was failing on nodes without shapes. [#40](https://github.com/pypeclub/pype/issues/40) -- _(maya)_ Yeti caches can now be properly versioned up in the scene inventory. [#40](https://github.com/pypeclub/pype/issues/40) -- _(nuke)_ Build first workfiles was not accepting jpeg sequences. [#34](https://github.com/pypeclub/pype/issues/34) -- _(deadline)_ Trying to generate ffmpeg review from multipart EXRs no longer crashes publishing. [PYPE-781] -- _(deadline)_ Render publishing is more stable in multiplatform environments. [PYPE-775] - - - - - -## 2.7.0 ## - -_**release date:** 30 March 2020_ - -**new:** - -- _(maya)_ Artist can now choose to load multiple references of the same subset at once [PYPE-646, PYPS-81] -- _(nuke)_ Option to use named OCIO colorspaces for review colour baking. [PYPS-82] -- _(pype)_ Pype can now work with `master` versions for publishing and loading. These are non-versioned publishes that are overwritten with the latest version during publish. These are now supported in all the GUIs, but their publishing is deactivated by default. [PYPE-653] -- _(blender)_ Added support for basic blender workflow. We currently support `rig`, `model` and `animation` families. [PYPE-768] -- _(pype)_ Source timecode can now be used in burn-ins. [PYPE-777] -- _(pype)_ Review outputs profiles can now specify delivery resolution different than project setting [PYPE-759] -- _(nuke)_ Bookmark to current context is now added automatically to all nuke browser windows. [PYPE-712] - -**change:** - -- _(maya)_ It is now possible to publish camera without. baking. Keep in mind that unbaked cameras can't be guaranteed to work in other hosts. [PYPE-595] -- _(maya)_ All the renders from maya are now grouped in the loader by their Layer name. [PYPE-482] -- _(nuke/hiero)_ Any publishes from nuke and hiero can now be versioned independently of the workfile. [PYPE-728] - - -**fix:** - -- _(nuke)_ Mixed slashes caused issues in ocio config path. -- _(pype)_ Intent field in pyblish GUI was passing label instead of value to ftrack. [PYPE-733] -- _(nuke)_ Publishing of pre-renders was inconsistent. [PYPE-766] -- _(maya)_ Handles and frame ranges were inconsistent in various places during publishing. -- _(nuke)_ Nuke was crashing if it ran into certain missing knobs. For example DPX output missing `autocrop` [PYPE-774] -- _(deadline)_ Project overrides were not working properly with farm render publishing. -- _(hiero)_ Problems with single frame plates publishing. -- _(maya)_ Redshift RenderPass token were breaking render publishing. [PYPE-778] -- _(nuke)_ Build first workfile was not accepting jpeg sequences. -- _(maya)_ Multipart (Multilayer) EXRs were breaking review publishing due to FFMPEG incompatiblity [PYPE-781] - - - - -## 2.6.0 ## - -_**release date:** 9 March 2020_ - -**change:** -- _(maya)_ render publishing has been simplified and made more robust. Render setup layers are now automatically added to publishing subsets and `render globals` family has been replaced with simple `render` [PYPE-570] -- _(avalon)_ change context and workfiles apps, have been merged into one, that allows both actions to be performed at the same time. [PYPE-747] -- _(pype)_ thumbnails are now automatically propagate to asset from the last published subset in the loader -- _(ftrack)_ publishing comment and intent are now being published to ftrack note as well as describtion. [PYPE-727] -- _(pype)_ when overriding existing version new old representations are now overriden, instead of the new ones just being appended. (to allow this behaviour, the version validator need to be disabled. [PYPE-690]) -- _(pype)_ burnin preset has been significantly simplified. It now doesn't require passing function to each field, but only need the actual text template. to use this, all the current burnin PRESETS MUST BE UPDATED for all the projects. -- _(ftrack)_ credentials are now stored on a per server basis, so it's possible to switch between ftrack servers without having to log in and out. [PYPE-723] - - -**new:** -- _(pype)_ production and development deployments now have different colour of the tray icon. Orange for Dev and Green for production [PYPE-718] -- _(maya)_ renders can now be attached to a publishable subset rather than creating their own subset. For example it is possible to create a reviewable `look` or `model` render and have it correctly attached as a representation of the subsets [PYPE-451] -- _(maya)_ after saving current scene into a new context (as a new shot for instance), all the scene publishing subsets data gets re-generated automatically to match the new context [PYPE-532] -- _(pype)_ we now support project specific publish, load and create plugins [PYPE-740] -- _(ftrack)_ new action that allow archiving/deleting old published versions. User can keep how many of the latest version to keep when the action is ran. [PYPE-748, PYPE-715] -- _(ftrack)_ it is now possible to monitor and restart ftrack event server using ftrack action. [PYPE-658] -- _(pype)_ validator that prevent accidental overwrites of previously published versions. [PYPE-680] -- _(avalon)_ avalon core updated to version 5.6.0 -- _(maya)_ added validator to make sure that relative paths are used when publishing arnold standins. -- _(nukestudio)_ it is now possible to extract and publish audio family from clip in nuke studio [PYPE-682] - -**fix**: -- _(maya)_ maya set framerange button was ignoring handles [PYPE-719] -- _(ftrack)_ sync to avalon was sometime crashing when ran on empty project -- _(nukestudio)_ publishing same shots after they've been previously archived/deleted would result in a crash. [PYPE-737] -- _(nuke)_ slate workflow was breaking in certain scenarios. [PYPE-730] -- _(pype)_ rendering publish workflow has been significantly improved to prevent error resulting from implicit render collection. [PYPE-665, PYPE-746] -- _(pype)_ launching application on a non-synced project resulted in obscure [PYPE-528] -- _(pype)_ missing keys in burnins no longer result in an error. [PYPE-706] -- _(ftrack)_ create folder structure action was sometimes failing for project managers due to wrong permissions. -- _(Nukestudio)_ using `source` in the start frame tag could result in wrong frame range calculation -- _(ftrack)_ sync to avalon action and event have been improved by catching more edge cases and provessing them properly. - - - - -## 2.5.0 ## - -_**release date:** 11 Feb 2020_ - -**change:** -- _(pype)_ added many logs for easier debugging -- _(pype)_ review presets can now be separated between 2d and 3d renders [PYPE-693] -- _(pype)_ anatomy module has been greatly improved to allow for more dynamic pulblishing and faster debugging [PYPE-685] -- _(pype)_ avalon schemas have been moved from `pype-config` to `pype` repository, for simplification. [PYPE-670] -- _(ftrack)_ updated to latest ftrack API -- _(ftrack)_ publishing comments now appear in ftrack also as a note on version with customisable category [PYPE-645] -- _(ftrack)_ delete asset/subset action had been improved. It is now able to remove multiple entities and descendants of the selected entities [PYPE-361, PYPS-72] -- _(workfiles)_ added date field to workfiles app [PYPE-603] -- _(maya)_ old deprecated loader have been removed in favour of a single unified reference loader (old scenes will upgrade automatically to the new loader upon opening) [PYPE-633, PYPE-697] -- _(avalon)_ core updated to 5.5.15 [PYPE-671] -- _(nuke)_ library loader is now available in nuke [PYPE-698] - - -**new:** -- _(pype)_ added pype render wrapper to allow rendering on mixed platform farms. [PYPE-634] -- _(pype)_ added `pype launch` command. It let's admin run applications with dynamically built environment based on the given context. [PYPE-634] -- _(pype)_ added support for extracting review sequences with burnins [PYPE-657] -- _(publish)_ users can now set intent next to a comment when publishing. This will then be reflected on an attribute in ftrack. [PYPE-632] -- _(burnin)_ timecode can now be added to burnin -- _(burnin)_ datetime keys can now be added to burnin and anatomy [PYPE-651] -- _(burnin)_ anatomy templates can now be used in burnins. [PYPE=626] -- _(nuke)_ new validator for render resolution -- _(nuke)_ support for attach slate to nuke renders [PYPE-630] -- _(nuke)_ png sequences were added to loaders -- _(maya)_ added maya 2020 compatibility [PYPE-677] -- _(maya)_ ability to publish and load .ASS standin sequences [PYPS-54] -- _(pype)_ thumbnails can now be published and are visible in the loader. `AVALON_THUMBNAIL_ROOT` environment variable needs to be set for this to work [PYPE-573, PYPE-132] -- _(blender)_ base implementation of blender was added with publishing and loading of .blend files [PYPE-612] -- _(ftrack)_ new action for preparing deliveries [PYPE-639] - - -**fix**: -- _(burnin)_ more robust way of finding ffmpeg for burnins. -- _(pype)_ improved UNC paths remapping when sending to farm. -- _(pype)_ float frames sometimes made their way to representation context in database, breaking loaders [PYPE-668] -- _(pype)_ `pype install --force` was failing sometimes [PYPE-600] -- _(pype)_ padding in published files got calculated wrongly sometimes. It is now instead being always read from project anatomy. [PYPE-667] -- _(publish)_ comment publishing was failing in certain situations -- _(ftrack)_ multiple edge case scenario fixes in auto sync and sync-to-avalon action -- _(ftrack)_ sync to avalon now works on empty projects -- _(ftrack)_ thumbnail update event was failing when deleting entities [PYPE-561] -- _(nuke)_ loader applies proper colorspaces from Presets -- _(nuke)_ publishing handles didn't always work correctly [PYPE-686] -- _(maya)_ assembly publishing and loading wasn't working correctly - - - - - - -## 2.4.0 ## - -_**release date:** 9 Dec 2019_ - -**change:** -- _(ftrack)_ version to status ftrack event can now be configured from Presets - - based on preset `presets/ftracc/ftrack_config.json["status_version_to_task"]` -- _(ftrack)_ sync to avalon event has been completely re-written. It now supports most of the project management situations on ftrack including moving, renaming and deleting entities, updating attributes and working with tasks. -- _(ftrack)_ sync to avalon action has been also re-writen. It is now much faster (up to 100 times depending on a project structure), has much better logging and reporting on encountered problems, and is able to handle much more complex situations. -- _(ftrack)_ sync to avalon trigger by checking `auto-sync` toggle on ftrack [PYPE-504] -- _(pype)_ various new features in the REST api -- _(pype)_ new visual identity used across pype -- _(pype)_ started moving all requirements to pip installation rather than vendorising them in pype repository. Due to a few yet unreleased packages, this means that pype can temporarily be only installed in the offline mode. - -**new:** -- _(nuke)_ support for publishing gizmos and loading them as viewer processes -- _(nuke)_ support for publishing nuke nodes from backdrops and loading them back -- _(pype)_ burnins can now work with start and end frames as keys - - use keys `{frame_start}`, `{frame_end}` and `{current_frame}` in burnin preset to use them. [PYPS-44,PYPS-73, PYPE-602] -- _(pype)_ option to filter logs by user and level in loggin GUI -- _(pype)_ image family added to standalone publisher [PYPE-574] -- _(pype)_ matchmove family added to standalone publisher [PYPE-574] -- _(nuke)_ validator for comparing arbitrary knobs with values from presets -- _(maya)_ option to force maya to copy textures in the new look publish rather than hardlinking them -- _(pype)_ comments from pyblish GUI are now being added to ftrack version -- _(maya)_ validator for checking outdated containers in the scene -- _(maya)_ option to publish and load arnold standin sequence [PYPE-579, PYPS-54] - -**fix**: -- _(pype)_ burnins were not respecting codec of the input video -- _(nuke)_ lot's of various nuke and nuke studio fixes across the board [PYPS-45] -- _(pype)_ workfiles app is not launching with the start of the app by default [PYPE-569] -- _(ftrack)_ ftrack integration during publishing was failing under certain situations [PYPS-66] -- _(pype)_ minor fixes in REST api -- _(ftrack)_ status change event was crashing when the target status was missing [PYPS-68] -- _(ftrack)_ actions will try to reconnect if they fail for some reason -- _(maya)_ problems with fps mapping when using float FPS values -- _(deadline)_ overall improvements to deadline publishing -- _(setup)_ environment variables are now remapped on the fly based on the platform pype is running on. This fixes many issues in mixed platform environments. - - - - -## 2.3.6 # - -_**release date:** 27 Nov 2019_ - -**hotfix**: -- _(ftrack)_ was hiding important debug logo -- _(nuke)_ crashes during workfile publishing -- _(ftrack)_ event server crashes because of signal problems -- _(muster)_ problems with muster render submissions -- _(ftrack)_ thumbnail update event syntax errors - - - - -## 2.3.0 ## - -_release date: 6 Oct 2019_ - -**new**: -- _(maya)_ support for yeti rigs and yeti caches -- _(maya)_ validator for comparing arbitrary attributes against ftrack -- _(pype)_ burnins can now show current date and time -- _(muster)_ pools can now be set in render globals in maya -- _(pype)_ Rest API has been implemented in beta stage -- _(nuke)_ LUT loader has been added -- _(pype)_ rudimentary user module has been added as preparation for user management -- _(pype)_ a simple logging GUI has been added to pype tray -- _(nuke)_ nuke can now bake input process into mov -- _(maya)_ imported models now have selection handle displayed by defaulting -- _(avalon)_ it's is now possible to load multiple assets at once using loader -- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading - -**changed**: -- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process. -- _(nuke)_ task name is now added to all rendered subsets -- _(pype)_ adding more families to standalone publisher -- _(pype)_ standalone publisher now uses pyblish-lite -- _(pype)_ standalone publisher can now create review quicktimes -- _(ftrack)_ queries to ftrack were sped up -- _(ftrack)_ multiple ftrack action have been deprecated -- _(avalon)_ avalon upstream has been updated to 5.5.0 -- _(nukestudio)_ published transforms can now be animated -- - -**fix**: -- _(maya)_ fps popup button didn't work in some cases -- _(maya)_ geometry instances and references in maya were losing shader assignments -- _(muster)_ muster rendering templates were not working correctly -- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist -- _(pype)_ problems with avalon db sync -- _(maya)_ ftrack was rounding FPS making it inconsistent -- _(pype)_ wrong icon names in Creator -- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene -- _(nukestudio)_ multiple bugs squashed -- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya - -## 2.2.0 ## -_**release date:** 8 Sept 2019_ - -**new**: -- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts -- _(nuke)_ option to choose deadline chunk size on write nodes -- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio -- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets. -- _(nuke)_ nuke writes now have deadline tab. -- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project. -- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them. -- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system - -**changed**: -- nukestudio now uses workio API for workfiles -- _(maya)_ "FIX FPS" prompt in maya now appears in the middle of the screen -- _(muster)_ can now be configured with custom templates -- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones - - -**fix**: -- wrong version retrieval from path in certain scenarios -- nuke reset resolution wasn't working in certain scenarios - -## 2.1.0 ## -_release date: 6 Aug 2019_ - -A large cleanup release. Most of the change are under the hood. - -**new**: -- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts -- _(pype)_ Added configurable option to add burnins to any generated quicktimes -- _(ftrack)_ Action that identifies what machines pype is running on. -- _(system)_ unify subprocess calls -- _(maya)_ add audio to review quicktimes -- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg -- **Nuke Studio** publishing and workfiles support -- **Muster** render manager support -- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup -- _(maya)_ Ability to load published sequences as image planes -- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack. -- _(maya)_ Pyblish plugin that allow validation of maya attributes -- _(system)_ added better startup logging to tray debug, including basic connection information -- _(avalon)_ option to group published subsets to groups in the loader -- _(avalon)_ loader family filters are working now - -**changed**: -- change multiple key attributes to unify their behaviour across the pipeline - - `frameRate` to `fps` - - `startFrame` to `frameStart` - - `endFrame` to `frameEnd` - - `fstart` to `frameStart` - - `fend` to `frameEnd` - - `handle_start` to `handleStart` - - `handle_end` to `handleEnd` - - `resolution_width` to `resolutionWidth` - - `resolution_height` to `resolutionHeight` - - `pixel_aspect` to `pixelAspect` - -- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist -- rendered frames are now deleted from temporary location after their publishing is finished. -- _(ftrack)_ RV action can now be launched from any entity -- after publishing only refresh button is now available in pyblish UI -- added context instance pyblish-lite so that artist knows if context plugin fails -- _(avalon)_ allow opening selected files using enter key -- _(avalon)_ core updated to v5.2.9 with our forked changes on top - -**fix**: -- faster hierarchy retrieval from db -- _(nuke)_ A lot of stability enhancements -- _(nuke studio)_ A lot of stability enhancements -- _(nuke)_ now only renders a single write node on farm -- _(ftrack)_ pype would crash when launcher project level task -- work directory was sometimes not being created correctly -- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning. -- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner diff --git a/website/docs/upgrade_notes.md b/website/docs/upgrade_notes.md deleted file mode 100644 index 8231cf997d..0000000000 --- a/website/docs/upgrade_notes.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -id: update_notes -title: Update Notes -sidebar_label: Update Notes ---- - - - -## **Updating to 2.13.0** ## - -### MongoDB - -**Must** - -Due to changes in how tasks are stored in the database (we added task types and possibility of more arbitrary data.), we must take a few precautions when updating. -1. Make sure that ftrack event server with sync to avalon is NOT running during the update. -2. Any project that is to be worked on with 2.13 must be synced from ftrack to avalon with the updated sync to avalon action, or using and updated event server sync to avalon event. - -If 2.12 event servers runs when trying to update the project sync with 2.13, it will override any changes. - -### Nuke Studio / hiero - -Make sure to re-generate pype tags and replace any `task` tags on your shots with the new ones. This will allow you to make multiple tasks of the same type, but with different task name at the same time. - -### Nuke - -Due to a minor update to nuke write node, artists will be prompted to update their write nodes before being able to publish any old shots. There is a "repair" action for this in the publisher, so it doesn't have to be done manually. - - - - -## **Updating to 2.12.0** ## - -### Apps and tools - -**Must** - -run Create/Update Custom attributes action (to update custom attributes group) -check if studio has set custom intent values and move values to ~/config/presets/global/intent.json - -**Optional** - -Set true/false on application and tools by studio usage (eliminate app list in Ftrack and time for registering Ftrack ations) - - - - -## **Updating to 2.11.0** ## - -### Maya in deadline - -We added or own maya deadline plugin to make render management easier. It operates the same as standard mayaBatch in deadline, but allow us to separate Pype sumitted jobs from standard submitter. You'll need to follow this guide to update this [install pype deadline](https://pype.club/docs/admin_hosts#pype-dealine-supplement-code) - - - - -## **Updating to 2.9.0** ## - -### Review and Burnin PRESETS - -This release introduces a major update to working with review and burnin presets. They can now be much more granular and can target extremely specific usecases. The change is backwards compatible with previous format of review and burnin presets, however we highly recommend updating all the presets to the new format. Documentation on what this looks like can be found on pype main [documentation page](https://pype.club/docs/admin_presets_plugins#publishjson). - -### Multiroot and storages - -With the support of multiroot projects, we removed the old `storage.json` from configuration and replaced it with simpler `config/anatomy/roots.json`. This is a required change, but only needs to be done once per studio during the update to 2.9.0. [Read More](https://pype.club/docs/next/admin_config#roots) - - - - -## **Updating to 2.7.0** ## - -### Master Versions -To activate `master` version workflow you need to activate `integrateMasterVersion` plugin in the `config/presets/plugins/global/publish.json` - -``` -"IntegrateMasterVersion": {"enabled": true}, -``` - -### Ftrack - -Make sure that `intent` attributes in ftrack is set correctly. It should follow this setup unless you have your custom values -``` -{ - "label": "Intent", - "key": "intent", - "type": "enumerator", - "entity_type": "assetversion", - "group": "avalon", - "config": { - "multiselect": false, - "data": [ - {"test": "Test"}, - {"wip": "WIP"}, - {"final": "Final"} - ] - } -``` - - - - -## **Updating to 2.6.0** ## - -### Dev vs Prod - -If you want to differentiate between dev and prod deployments of pype, you need to add `config.ini` file to `pype-setup/pypeapp` folder with content. - -``` -[Default] -dev=true -``` - -### Ftrack - -You will have to log in to ftrack in pype after the update. You should be automatically prompted with the ftrack login window when you launch 2.6 release for the first time. - -Event server has to be restarted after the update to enable the ability to control it via action. - -### Presets - -There is a major change in the way how burnin presets are being stored. We simplified the preset format, however that means the currently running production configs need to be tweaked to match the new format. - -:::note Example of converting burnin preset from 2.5 to 2.6 - -2.5 burnin preset - -``` -"burnins":{ - "TOP_LEFT": { - "function": "text", - "text": "{dd}/{mm}/{yyyy}" - }, - "TOP_CENTERED": { - "function": "text", - "text": "" - }, - "TOP_RIGHT": { - "function": "text", - "text": "v{version:0>3}" - }, - "BOTTOM_LEFT": { - "function": "text", - "text": "{frame_start}-{current_frame}-{frame_end}" - }, - "BOTTOM_CENTERED": { - "function": "text", - "text": "{asset}" - }, - "BOTTOM_RIGHT": { - "function": "frame_numbers", - "text": "{username}" - } -``` - -2.6 burnin preset -``` -"burnins":{ - "TOP_LEFT": "{dd}/{mm}/{yyyy}", - "TOP_CENTER": "", - "TOP_RIGHT": "v{version:0>3}" - "BOTTOM_LEFT": "{frame_start}-{current_frame}-{frame_end}", - "BOTTOM_CENTERED": "{asset}", - "BOTTOM_RIGHT": "{username}" -} -``` diff --git a/website/sidebars.js b/website/sidebars.js index 9d60a5811c..c4d07e728f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -109,11 +109,7 @@ module.exports = { "admin_hosts_tvpaint" ], }, - { - type: "category", - label: "Releases", - items: ["changelog", "update_notes"], - }, + "admin_releases", { type: "category", collapsed: false, From 7466063001d7efce8ef63302e22ac50607be20bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:41:02 +0200 Subject: [PATCH 0918/1030] Fix typo Co-authored-by: Milan Kolar --- website/docs/system_introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/system_introduction.md b/website/docs/system_introduction.md index b8a2cea487..05627b5359 100644 --- a/website/docs/system_introduction.md +++ b/website/docs/system_introduction.md @@ -17,7 +17,7 @@ various usage scenarios. You can find detailed breakdown of technical requirements [here](dev_requirements), but in general OpenPype should be able to operate in most studios fairly quickly. The main obstacles are usually related to workflows and habits, that -might now be fully compatible with what OpenPype is expecting or enforcing. It is recommended to go through artists [key concepts](artist_concepts) to get idea about basics. +might not be fully compatible with what OpenPype is expecting or enforcing. It is recommended to go through artists [key concepts](artist_concepts) to get idea about basics. Keep in mind that if you run into any workflows that are not supported, it's usually just because we haven't hit that particular case and it can most likely be added upon request. From bf5584d77f9766dc5da23890c76dba1841c6bfdb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:46:47 +0200 Subject: [PATCH 0919/1030] Change avalon to openpype Co-authored-by: Roy Nieterau --- website/docs/module_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index ad9cf75e8f..6d5529b512 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -72,7 +72,7 @@ We do not recommend setting your Ftrack user and api key environments in a persi ### Where to run event server -We recommend you to run event server on stable server machine with ability to connect to Avalon database and Ftrack web server. Best practice we recommend is to run event server as service. It can be Windows or Linux. +We recommend you to run event server on stable server machine with ability to connect to OpenPype database and Ftrack web server. Best practice we recommend is to run event server as service. It can be Windows or Linux. :::important Event server should **not** run more than once! It may cause major issues. From 4943b7889eecb207304683b01dca778d376dc9ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:47:07 +0200 Subject: [PATCH 0920/1030] grammar fix Co-authored-by: Roy Nieterau --- website/docs/artist_concepts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_concepts.md b/website/docs/artist_concepts.md index f67ab89b9c..7582540811 100644 --- a/website/docs/artist_concepts.md +++ b/website/docs/artist_concepts.md @@ -10,7 +10,7 @@ sidebar_label: Key Concepts In our pipeline all the main entities the project is made from are internally considered *'Assets'*. Episode, sequence, shot, character, prop, etc. All of these behave identically in the pipeline. Asset names need to be absolutely unique within the project because they are their key identifier. -OpenPype has limitation regarging duplicated names. Name of assets must be unique across whole project. +OpenPype has a limitation regarding duplicated names. Name of assets must be unique across whole project. ### Subset From 90910cc3eeb929c70542f13b1b9fd3c4b5179025 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 15:02:01 +0200 Subject: [PATCH 0921/1030] use project name in prepare root value instead of project code --- openpype/hosts/maya/plugins/load/_load_animation.py | 2 +- openpype/hosts/maya/plugins/load/load_ass.py | 7 ++++--- openpype/hosts/maya/plugins/load/load_look.py | 2 +- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- openpype/hosts/maya/plugins/load/load_yeti_rig.py | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 0010efb829..b419a730b5 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -36,7 +36,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # hero_001 (abc) # asset_counter{optional} file_url = self.prepare_root_value(self.fname, - context["project"]["code"]) + context["project"]["name"]) nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 1f0eb88995..d1b12ceaba 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -65,8 +65,9 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): proxyPath = proxyPath_base + ".ma" + project_name = context["project"]["name"] file_url = self.prepare_root_value(proxyPath, - context["project"]["code"]) + project_name) nodes = cmds.file(file_url, namespace=namespace, @@ -85,7 +86,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): proxyShape.dso.set(path) proxyShape.aiOverrideShaders.set(0) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) @@ -128,7 +129,7 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): file_url = self.prepare_root_value(proxyPath, representation["context"] ["project"] - ["code"]) + ["name"]) content = cmds.file(file_url, loadReference=reference_node, type="mayaAscii", diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py index 7392adc4dd..3ef19ad96f 100644 --- a/openpype/hosts/maya/plugins/load/load_look.py +++ b/openpype/hosts/maya/plugins/load/load_look.py @@ -33,7 +33,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with lib.maintained_selection(): file_url = self.prepare_root_value(self.fname, - context["project"]["code"]) + context["project"]["name"]) nodes = cmds.file(file_url, namespace=namespace, reference=True, diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index e4355ed3d4..fa8cbfbe64 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -52,7 +52,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) file_url = self.prepare_root_value(self.fname, - context["project"]["code"]) + context["project"]["name"]) nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index 241c28467a..4b730ad2c1 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -54,7 +54,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # load rig with lib.maintained_selection(): file_url = self.prepare_root_value(self.fname, - context["project"]["code"]) + context["project"]["name"]) nodes = cmds.file(file_url, namespace=namespace, reference=True, From 6a42f07d8e5977193236f9b3665a5a655188ae1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 15:05:55 +0200 Subject: [PATCH 0922/1030] fix missing argument --- openpype/hosts/tvpaint/tvpaint_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/tvpaint_module.py b/openpype/hosts/tvpaint/tvpaint_module.py index c29602babc..a004359231 100644 --- a/openpype/hosts/tvpaint/tvpaint_module.py +++ b/openpype/hosts/tvpaint/tvpaint_module.py @@ -20,7 +20,7 @@ class TVPaintModule(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True - def add_implementation_envs(env, _app): + def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" defaults = { From 9c2c1118ac40d6f8ec3b691a947c2a985439da83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 15:07:47 +0200 Subject: [PATCH 0923/1030] added notes into client directory --- openpype/client/notes.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 openpype/client/notes.md diff --git a/openpype/client/notes.md b/openpype/client/notes.md new file mode 100644 index 0000000000..a261b86eca --- /dev/null +++ b/openpype/client/notes.md @@ -0,0 +1,39 @@ +# Client functionality +## Reason +Preparation for OpenPype v4 server. Goal is to remove direct mongo calls in code to prepare a little bit for different source of data for code before. To start think about database calls less as mongo calls but more universally. To do so was implemented simple wrapper around database calls to not use pymongo specific code. + +Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tighly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state. + +## Queries +Query functions don't use full potential of mongo queries like very specific queries based on subdictionaries or unknown structures. We try to avoid these calls as much as possible because they'll probably won't be available in future. If it's really necessary a new function can be added but only if it's reasonable for overall logic. All query functions were moved to `~/client/entities.py`. Each function has arguments with available filters and possible reduce of returned keys for each entity. + +## Changes +Changes are a little bit complicated. Mongo has many options how update can happen which had to be reduced also it would be at this stage complicated to validate values which are created or updated thus automation is at this point almost none. Changes can be made using operations available in `~/client/operations.py`. Each operation require project name and entity type, but may require operation specific data. + +### Create +Create operations expect already prepared document data, for that are prepared functions creating skeletal structures of documents (do not fill all required data), except `_id` all data should be right. Existence of entity is not validated so if the same creation operation is send n times it will create the entity n times which can cause issues. + +### Update +Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare__update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementaion. + +### Delete +Delete operation need entity id. Entity will be deleted from mongo. + + +## What (probably) won't be replaced +Some parts of code are still using direct mongo calls. In most of cases it is for very specific calls that are module specific or their usage will completely change in future. +- Mongo calls that are not project specific (out of `avalon` collection) will be removed or will have to use different mechanism how the data are stored. At this moment it is related to OpenPype settings and logs, ftrack server events, some other data. +- Sync server queries. They're complex and very specific for sync server module. Their replacement will require specific calls to OpenPype server in v4 thus their abstraction with wrapper is irrelevant and would complicate production in v3. +- Project managers (ftrack, kitsu, shotgrid, embedded Project Manager, etc.). Project managers are creating, updating or removing assets in v3, but in v4 will create folders with different structure. Wrapping creation of assets would not help to prepare for v4 because of new data structures. The same can be said about editorial Extract Hierarchy Avalon plugin which create project structure. +- Code parts that is marked as deprecated in v3 or will be deprecated in v4. + - integrate asset legacy publish plugin - already is legacy kept for safety + - integrate thumbnail - thumbnails will be stored in different way in v4 + - input links - link will be stored in different way and will have different mechanism of linking. In v3 are links limited to same entity type "asset <-> asset" or "representation <-> representation". + +## Known missing replacements +- change subset group in loader tool +- integrate subset group +- query input links in openpype lib +- create project in openpype lib +- save/create workfile doc in openpype lib +- integrate hero version From ffa3b0829f800f3ee43d7b8e0cf80801dffda591 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Aug 2022 15:12:31 +0200 Subject: [PATCH 0924/1030] flame: fixing frame ranges after client tests --- .../publish/extract_subset_resources.py | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 8a03ba119c..3e1e8db986 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -69,6 +69,9 @@ class ExtractSubsetResources(openpype.api.Extractor): # get media source first frame source_first_frame = instance.data["sourceFirstFrame"] + self.log.debug("_ frame_start: {}".format(frame_start)) + self.log.debug("_ source_first_frame: {}".format(source_first_frame)) + # get timeline in/out of segment clip_in = instance.data["clipIn"] clip_out = instance.data["clipOut"] @@ -102,6 +105,25 @@ class ExtractSubsetResources(openpype.api.Extractor): + r_handle_end ) + # get frame range with handles for representation range + frame_start_handle = frame_start - handle_start + repre_frame_start = frame_start_handle + if include_handles: + if r_speed == 1.0: + frame_start_handle = frame_start + else: + frame_start_handle = ( + frame_start - handle_start) + r_handle_start + + self.log.debug("_ frame_start_handle: {}".format( + frame_start_handle)) + self.log.debug("_ repre_frame_start: {}".format( + repre_frame_start)) + + # calculate duration with handles + source_duration_handles = ( + source_end_handles - source_start_handles) + 1 + # create staging dir path staging_dir = self.staging_dir(instance) @@ -120,15 +142,22 @@ class ExtractSubsetResources(openpype.api.Extractor): # set versiondata if any retime version_data = retimed_data.get("version_data") + self.log.debug("_ version_data: {}".format(version_data)) if version_data: instance.data["versionData"].update(version_data) if r_speed != 1.0: instance.data["versionData"].update({ - "frameStart": source_start_handles + r_handle_start, - "frameEnd": source_end_handles - r_handle_end, + "frameStart": frame_start_handle, + "frameEnd": ( + (frame_start_handle + source_duration_handles - 1) + - (r_handle_start + r_handle_end) + ) }) + self.log.debug("_ i_version_data: {}".format( + instance.data["versionData"] + )) # loop all preset names and for unique_name, preset_config in export_presets.items(): @@ -152,22 +181,6 @@ class ExtractSubsetResources(openpype.api.Extractor): ) ) - # get frame range with handles for representation range - frame_start_handle = frame_start - handle_start - if include_handles: - if r_speed == 1.0: - frame_start_handle = frame_start - else: - frame_start_handle = ( - frame_start - handle_start) + r_handle_start - - self.log.debug("_ frame_start_handle: {}".format( - frame_start_handle)) - - # calculate duration with handles - source_duration_handles = ( - source_end_handles - source_start_handles) + 1 - exporting_clip = None name_patern_xml = "_{}.".format( unique_name) @@ -203,7 +216,7 @@ class ExtractSubsetResources(openpype.api.Extractor): modify_xml_data.update({ # enum position low start from 0 "frameIndex": 0, - "startFrame": frame_start_handle, + "startFrame": repre_frame_start, "namePattern": name_patern_xml }) @@ -248,7 +261,7 @@ class ExtractSubsetResources(openpype.api.Extractor): "namePattern": "__thumbnail" }) thumb_frame_number = int(in_mark + ( - source_duration_handles / 2)) + (out_mark - in_mark + 1) / 2)) self.log.debug("__ thumb_frame_number: {}".format( thumb_frame_number @@ -329,9 +342,9 @@ class ExtractSubsetResources(openpype.api.Extractor): # add frame range if preset_config["representation_add_range"]: representation_data.update({ - "frameStart": frame_start_handle, + "frameStart": repre_frame_start, "frameEnd": ( - frame_start_handle + source_duration_handles) - 1, + repre_frame_start + source_duration_handles) - 1, "fps": instance.data["fps"] }) From b96cff6ea9f85f80d5ee801fc8bd17020e484580 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Aug 2022 15:24:58 +0200 Subject: [PATCH 0925/1030] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/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 0d8ab1c17ea388b3f639d1bedab67359463af44c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 15:35:35 +0200 Subject: [PATCH 0926/1030] fix unsetting of value --- openpype/client/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index c4b95bf696..618cdf9d1e 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -444,7 +444,7 @@ class UpdateOperation(AbstractOperation): set_data = {} for key, value in self._update_data.items(): if value is REMOVED_VALUE: - unset_data[key] = value + unset_data[key] = None else: set_data[key] = value From 0f114331ec45dacc571ab2dc3b04dc869d833d12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 15:35:57 +0200 Subject: [PATCH 0927/1030] use client options to change subset group --- openpype/tools/loader/widgets.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 597c35e89b..cbf5720803 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -17,6 +17,7 @@ from openpype.client import ( get_thumbnail_id_from_source, get_thumbnail, ) +from openpype.client.operations import OperationsSession, REMOVED_VALUE from openpype.pipeline import HeroVersionType, Anatomy from openpype.pipeline.thumbnail import get_thumbnail_binary from openpype.pipeline.load import ( @@ -614,26 +615,30 @@ class SubsetWidget(QtWidgets.QWidget): box.show() def group_subsets(self, name, asset_ids, items): - field = "data.subsetGroup" + subset_ids = { + item["_id"] + for item in items + if item.get("_id") + } + if not subset_ids: + return if name: - update = {"$set": {field: name}} self.echo("Group subsets to '%s'.." % name) else: - update = {"$unset": {field: ""}} self.echo("Ungroup subsets..") - subsets = list() - for item in items: - subsets.append(item["subset"]) + project_name = self.dbcon.active_project() + op_session = OperationsSession() + for subset_id in subset_ids: + op_session.update_entity( + project_name, + "subset", + subset_id, + {"data.subsetGroup": name or REMOVED_VALUE} + ) - for asset_id in asset_ids: - filtr = { - "type": "subset", - "parent": asset_id, - "name": {"$in": subsets}, - } - self.dbcon.update_many(filtr, update) + op_session.commit() def echo(self, message): print(message) From 7f234e1d814a92cbe1e446aeef7c71a2a2165163 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 16:20:54 +0200 Subject: [PATCH 0928/1030] fix iterator index acces --- openpype/tools/loader/model.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 3ce44ea6c8..4f1f37b217 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -272,11 +272,13 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): # update availability on active site when version changes if self.sync_server.enabled and version_doc: - repre_info = self.sync_server.get_repre_info_for_versions( - project_name, - [version_doc["_id"]], - self.active_site, - self.remote_site + repre_info = list( + self.sync_server.get_repre_info_for_versions( + project_name, + [version_doc["_id"]], + self.active_site, + self.remote_site + ) ) if repre_info: version_doc["data"].update( From 4062bf56f32a6d5ec3e6bf6ec62c61910b3365bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 16:36:24 +0200 Subject: [PATCH 0929/1030] print traceback on crashed dynamic thread --- openpype/tools/utils/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 99d8c75ab4..fb2348518a 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -2,6 +2,7 @@ import os import sys import contextlib import collections +import traceback from Qt import QtWidgets, QtCore, QtGui import qtawesome @@ -643,7 +644,11 @@ class DynamicQThread(QtCore.QThread): def create_qthread(func, *args, **kwargs): class Thread(QtCore.QThread): def run(self): - func(*args, **kwargs) + try: + func(*args, **kwargs) + except: + traceback.print_exception(*sys.exc_info()) + raise return Thread() From fce4e6e3d8f5c3d7f2b929a0328324ce1d951e9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 16:38:45 +0200 Subject: [PATCH 0930/1030] fix version specific repre info in loader --- openpype/tools/loader/model.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 4f1f37b217..929e497890 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -272,7 +272,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): # update availability on active site when version changes if self.sync_server.enabled and version_doc: - repre_info = list( + repres_info = list( self.sync_server.get_repre_info_for_versions( project_name, [version_doc["_id"]], @@ -280,9 +280,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self.remote_site ) ) - if repre_info: + if repres_info: version_doc["data"].update( - self._get_repre_dict(repre_info[0])) + self._get_repre_dict(repres_info[0])) self.set_version(index, version_doc) @@ -474,29 +474,34 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): last_versions_by_subset_id[subset_id] = hero_version - repre_info = {} + repre_info_by_version_id = {} if self.sync_server.enabled: - version_ids = set() + versions_by_id = {} for _subset_id, doc in last_versions_by_subset_id.items(): - version_ids.add(doc["_id"]) + versions_by_id[doc["_id"]] = doc - repres = self.sync_server.get_repre_info_for_versions( + repres_info = self.sync_server.get_repre_info_for_versions( project_name, - list(version_ids), self.active_site, self.remote_site + list(versions_by_id.keys()), + self.active_site, + self.remote_site ) - for repre in repres: + for repre_info in repres_info: if self._doc_fetching_stop: return + + version_id = repre_info["_id"] + doc = versions_by_id[version_id] doc["active_provider"] = self.active_provider doc["remote_provider"] = self.remote_provider - repre_info[repre["_id"]] = repre + repre_info_by_version_id[version_id] = repre_info self._doc_payload = { "asset_docs_by_id": asset_docs_by_id, "subset_docs_by_id": subset_docs_by_id, "subset_families": subset_families, "last_versions_by_subset_id": last_versions_by_subset_id, - "repre_info_by_version_id": repre_info + "repre_info_by_version_id": repre_info_by_version_id } self.doc_fetched.emit() From c393105e2502ed4b9dba95aa2a49cefbd849c2a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 16:39:31 +0200 Subject: [PATCH 0931/1030] use BaseException --- openpype/tools/utils/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index fb2348518a..97b680b77e 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -646,7 +646,7 @@ def create_qthread(func, *args, **kwargs): def run(self): try: func(*args, **kwargs) - except: + except BaseException: traceback.print_exception(*sys.exc_info()) raise return Thread() From 265d67f1fc2aa42072b0c32c688c9ef6e3a467f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 17:19:48 +0200 Subject: [PATCH 0932/1030] added helper getters to modules manager --- openpype/modules/base.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e26075283d..1316d7f734 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -562,6 +562,40 @@ class ModulesManager: self.initialize_modules() self.connect_modules() + def __getitem__(self, module_name): + return self.modules_by_name[module_name] + + def get(self, module_name, default=None): + """Access module by name. + + Args: + module_name (str): Name of module which should be returned. + default (Any): Default output if module is not available. + + Returns: + Union[OpenPypeModule, None]: Module found by name or None. + """ + return self.modules_by_name.get(module_name, default) + + def get_enabled_module(self, module_name, default=None): + """Fast access to enabled module. + + If module is available but is not enabled default value is returned. + + Args: + module_name (str): Name of module which should be returned. + default (Any): Default output if module is not available or is + not enabled. + + Returns: + Union[OpenPypeModule, None]: Enabled module found by name or None. + """ + + module = self.get(module_name) + if module is not None and module.enabled: + return module + return default + def initialize_modules(self): """Import and initialize modules.""" # Make sure modules are loaded From 309a272a1833ad73badeab5235ece8707c904c33 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Aug 2022 17:27:47 +0200 Subject: [PATCH 0933/1030] nuke: fixing validate knobs --- openpype/hosts/nuke/plugins/publish/validate_knobs.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_knobs.py b/openpype/hosts/nuke/plugins/publish/validate_knobs.py index 573c25f3fe..e2b11892e5 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_knobs.py +++ b/openpype/hosts/nuke/plugins/publish/validate_knobs.py @@ -65,13 +65,22 @@ class ValidateKnobs(pyblish.api.ContextPlugin): # Filter families. families = [instance.data["family"]] families += instance.data.get("families", []) - families = list(set(families) & set(cls.knobs.keys())) + if not families: continue # Get all knobs to validate. knobs = {} for family in families: + # check if dot in family + if "." in family: + family = family.split(".")[0] + + # avoid families not in settings + if family not in cls.knobs: + continue + + # get presets of knobs for preset in cls.knobs[family]: knobs[preset] = cls.knobs[family][preset] From eb897ac579e0993103cc2d12c82d574181e55754 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Aug 2022 17:28:03 +0200 Subject: [PATCH 0934/1030] remove unused import --- openpype/hosts/nuke/plugins/publish/validate_write_nodes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 9c9b8babaa..362ff31174 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,4 +1,3 @@ -import six import pyblish.api from openpype.api import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( From bc7aa718add1ffd053f942d7a8913cce05c24ddd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 17:45:49 +0200 Subject: [PATCH 0935/1030] use current schemas from client --- openpype/lib/avalon_context.py | 12 +++++++++--- .../event_sync_to_avalon.py | 4 ++-- openpype/modules/ftrack/lib/avalon_sync.py | 19 ++++++++----------- .../project_manager/project_manager/model.py | 10 ++++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index eed17fce9d..b9d66291be 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -14,6 +14,11 @@ from openpype.client import ( get_last_version_by_subset_name, get_workfile_info, ) +from openpype.client.operations import ( + CURRENT_ASSET_DOC_SCHEMA, + CURRENT_PROJECT_SCHEMA, + CURRENT_PROJECT_CONFIG_SCHEMA, +) from .profiles_filtering import filter_profiles from .events import emit_event from .path_templates import StringTemplate @@ -23,10 +28,11 @@ legacy_io = None log = logging.getLogger("AvalonContext") +# Backwards compatibility - should not be used anymore CURRENT_DOC_SCHEMAS = { - "project": "openpype:project-3.0", - "asset": "openpype:asset-3.0", - "config": "openpype:config-2.0" + "project": CURRENT_PROJECT_SCHEMA, + "asset": CURRENT_ASSET_DOC_SCHEMA, + "config": CURRENT_PROJECT_CONFIG_SCHEMA } PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" PROJECT_NAME_REGEX = re.compile( diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 738181dc9a..e549de7ed0 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -18,6 +18,7 @@ from openpype.client import ( get_archived_assets, get_asset_ids_with_subsets ) +from openpype.client.operations import CURRENT_ASSET_DOC_SCHEMA from openpype.pipeline import AvalonMongoDB, schema from openpype_modules.ftrack.lib import ( @@ -35,7 +36,6 @@ from openpype_modules.ftrack.lib.avalon_sync import ( convert_to_fps, InvalidFpsValue ) -from openpype.lib import CURRENT_DOC_SCHEMAS class SyncToAvalonEvent(BaseEvent): @@ -1236,7 +1236,7 @@ class SyncToAvalonEvent(BaseEvent): "_id": mongo_id, "name": name, "type": "asset", - "schema": CURRENT_DOC_SCHEMAS["asset"], + "schema": CURRENT_ASSET_DOC_SCHEMA, "parent": proj["_id"], "data": { "ftrackId": ftrack_ent["id"], diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index f8883cefbd..72be6a8e9a 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -14,6 +14,11 @@ from openpype.client import ( get_versions, get_representations ) +from openpype.client.operations import ( + CURRENT_ASSET_DOC_SCHEMA, + CURRENT_PROJECT_SCHEMA, + CURRENT_PROJECT_CONFIG_SCHEMA, +) from openpype.api import ( Logger, get_anatomy_settings @@ -32,14 +37,6 @@ import ftrack_api log = Logger.get_logger(__name__) -# Current schemas for avalon types -CURRENT_DOC_SCHEMAS = { - "project": "openpype:project-3.0", - "asset": "openpype:asset-3.0", - "config": "openpype:config-2.0" -} - - class InvalidFpsValue(Exception): pass @@ -2063,7 +2060,7 @@ class SyncEntitiesFactory: item["_id"] = new_id item["parent"] = self.avalon_project_id - item["schema"] = CURRENT_DOC_SCHEMAS["asset"] + item["schema"] = CURRENT_ASSET_DOC_SCHEMA item["data"]["visualParent"] = avalon_parent new_id_str = str(new_id) @@ -2198,8 +2195,8 @@ class SyncEntitiesFactory: project_item["_id"] = new_id project_item["parent"] = None - project_item["schema"] = CURRENT_DOC_SCHEMAS["project"] - project_item["config"]["schema"] = CURRENT_DOC_SCHEMAS["config"] + project_item["schema"] = CURRENT_PROJECT_SCHEMA + project_item["config"]["schema"] = CURRENT_PROJECT_CONFIG_SCHEMA self.ftrack_avalon_mapper[self.ft_project_id] = new_id self.avalon_ftrack_mapper[new_id] = self.ft_project_id diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 3aaee75698..6f40140e5e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -13,10 +13,8 @@ from openpype.client import ( get_assets, get_asset_ids_with_subsets, ) -from openpype.lib import ( - CURRENT_DOC_SCHEMAS, - PypeLogger, -) +from openpype.client.operations import CURRENT_ASSET_DOC_SCHEMA +from openpype.lib import Logger from .constants import ( IDENTIFIER_ROLE, @@ -203,7 +201,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): @property def log(self): if self._log is None: - self._log = PypeLogger.get_logger("ProjectManagerModel") + self._log = Logger.get_logger("ProjectManagerModel") return self._log @property @@ -1961,7 +1959,7 @@ class AssetItem(BaseItem): } schema_name = ( self._origin_asset_doc.get("schema") - or CURRENT_DOC_SCHEMAS["asset"] + or CURRENT_ASSET_DOC_SCHEMA ) doc = { From 2ded3136c7903ce1dcf651c932fec17c05e22422 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 17:47:04 +0200 Subject: [PATCH 0936/1030] moved project name regex to client operations and use it from there --- openpype/client/operations.py | 6 ++++++ openpype/lib/avalon_context.py | 6 ++---- openpype/tools/project_manager/project_manager/widgets.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index 618cdf9d1e..c0716ee109 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -1,3 +1,4 @@ +import re import uuid import copy import collections @@ -11,6 +12,11 @@ from .mongo import get_project_connection REMOVED_VALUE = object() +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) +) + CURRENT_PROJECT_SCHEMA = "openpype:project-3.0" CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0" CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b9d66291be..2abd634832 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -18,6 +18,8 @@ from openpype.client.operations import ( CURRENT_ASSET_DOC_SCHEMA, CURRENT_PROJECT_SCHEMA, CURRENT_PROJECT_CONFIG_SCHEMA, + PROJECT_NAME_ALLOWED_SYMBOLS, + PROJECT_NAME_REGEX, ) from .profiles_filtering import filter_profiles from .events import emit_event @@ -34,10 +36,6 @@ CURRENT_DOC_SCHEMAS = { "asset": CURRENT_ASSET_DOC_SCHEMA, "config": CURRENT_PROJECT_CONFIG_SCHEMA } -PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" -PROJECT_NAME_REGEX = re.compile( - "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) -) class AvalonContextDeprecatedWarning(DeprecationWarning): diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 371d1ba2ef..d0715f204d 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -5,8 +5,8 @@ from .constants import ( NAME_ALLOWED_SYMBOLS, NAME_REGEX ) -from openpype.lib import ( - create_project, +from openpype.lib import create_project +from openpype.client.operations import ( PROJECT_NAME_ALLOWED_SYMBOLS, PROJECT_NAME_REGEX ) From 38e907d4ea5bfe663cdf929fa0befb0cfa18c283 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 17:48:43 +0200 Subject: [PATCH 0937/1030] removed unused import and added deprecation comment --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 2abd634832..780a830f21 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1,6 +1,5 @@ """Should be used only inside of hosts.""" import os -import re import copy import platform import logging @@ -31,6 +30,7 @@ log = logging.getLogger("AvalonContext") # Backwards compatibility - should not be used anymore +# - Will be removed in OP 3.16.* CURRENT_DOC_SCHEMAS = { "project": CURRENT_PROJECT_SCHEMA, "asset": CURRENT_ASSET_DOC_SCHEMA, From da80b2506ec6b5ba0ccddd1d32b4177401d8c0b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 18:21:47 +0200 Subject: [PATCH 0938/1030] moved get creator by name to pipeline.create --- openpype/lib/avalon_context.py | 22 +++++----------- openpype/pipeline/create/__init__.py | 8 ++++-- openpype/pipeline/create/creator_plugins.py | 28 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index f08adb5470..b7d0774cf8 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -769,7 +769,7 @@ def BuildWorkfile(): return BuildWorkfile() -@with_pipeline_io +@deprecated("openpype.pipeline.create.get_legacy_creator_by_name") def get_creator_by_name(creator_name, case_sensitive=False): """Find creator plugin by name. @@ -780,23 +780,13 @@ def get_creator_by_name(creator_name, case_sensitive=False): Returns: Creator: Return first matching plugin or `None`. + + Deprecated: + Function will be removed after release version 3.16.* """ - from openpype.pipeline import discover_legacy_creator_plugins + from openpype.pipeline.create import get_legacy_creator_by_name - # Lower input creator name if is not case sensitive - if not case_sensitive: - creator_name = creator_name.lower() - - for creator_plugin in discover_legacy_creator_plugins(): - _creator_name = creator_plugin.__name__ - - # Lower creator plugin name if is not case sensitive - if not case_sensitive: - _creator_name = _creator_name.lower() - - if _creator_name == creator_name: - return creator_plugin - return None + return get_legacy_creator_by_name(creator_name, case_sensitive) @deprecated diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index bd196ccfd1..733e7766b2 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -9,8 +9,10 @@ from .creator_plugins import ( AutoCreator, HiddenCreator, - discover_creator_plugins, discover_legacy_creator_plugins, + get_legacy_creator_by_name, + + discover_creator_plugins, register_creator_plugin, deregister_creator_plugin, register_creator_plugin_path, @@ -38,8 +40,10 @@ __all__ = ( "AutoCreator", "HiddenCreator", - "discover_creator_plugins", "discover_legacy_creator_plugins", + "get_legacy_creator_by_name", + + "discover_creator_plugins", "register_creator_plugin", "deregister_creator_plugin", "register_creator_plugin_path", diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 9a5d559774..9e1530a6a7 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -458,6 +458,34 @@ def discover_legacy_creator_plugins(): return plugins +def get_legacy_creator_by_name(creator_name, case_sensitive=False): + """Find creator plugin by name. + + Args: + creator_name (str): Name of creator class that should be returned. + case_sensitive (bool): Match of creator plugin name is case sensitive. + Set to `False` by default. + + Returns: + Creator: Return first matching plugin or `None`. + """ + + # Lower input creator name if is not case sensitive + if not case_sensitive: + creator_name = creator_name.lower() + + for creator_plugin in discover_legacy_creator_plugins(): + _creator_name = creator_plugin.__name__ + + # Lower creator plugin name if is not case sensitive + if not case_sensitive: + _creator_name = _creator_name.lower() + + if _creator_name == creator_name: + return creator_plugin + return None + + def register_creator_plugin(plugin): if issubclass(plugin, BaseCreator): register_plugin(BaseCreator, plugin) From fe75b25c9b83762553957e1d4c763c6b27785ddb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 18:23:11 +0200 Subject: [PATCH 0939/1030] use 'get_legacy_creator_by_name' instead of 'get_creator_by_name' --- .../hosts/blender/plugins/load/load_layout_blend.py | 4 ++-- .../hosts/blender/plugins/load/load_layout_json.py | 2 +- openpype/hosts/blender/plugins/load/load_rig.py | 4 ++-- openpype/hosts/maya/plugins/load/load_reference.py | 10 ++++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index cf8e89ed1f..e0124053bf 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -6,12 +6,12 @@ from typing import Dict, List, Optional import bpy -from openpype import lib from openpype.pipeline import ( legacy_create, get_representation_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.create import get_legacy_creator_by_name from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, @@ -157,7 +157,7 @@ class BlendLayoutLoader(plugin.AssetLoader): t.id = local_obj elif local_obj.type == 'EMPTY': - creator_plugin = lib.get_creator_by_name("CreateAnimation") + creator_plugin = get_legacy_creator_by_name("CreateAnimation") if not creator_plugin: raise ValueError("Creator plugin \"CreateAnimation\" was " "not found.") diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index a0580af4a0..eca098627e 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -118,7 +118,7 @@ class JsonLayoutLoader(plugin.AssetLoader): # Camera creation when loading a layout is not necessary for now, # but the code is worth keeping in case we need it in the future. # # Create the camera asset and the camera instance - # creator_plugin = lib.get_creator_by_name("CreateCamera") + # creator_plugin = get_legacy_creator_by_name("CreateCamera") # if not creator_plugin: # raise ValueError("Creator plugin \"CreateCamera\" was " # "not found.") diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 4dfa96167f..1d23a70061 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -6,12 +6,12 @@ from typing import Dict, List, Optional import bpy -from openpype import lib from openpype.pipeline import ( legacy_create, get_representation_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.create import get_legacy_creator_by_name from openpype.hosts.blender.api import ( plugin, get_selection, @@ -244,7 +244,7 @@ class BlendRigLoader(plugin.AssetLoader): objects = self._process(libpath, asset_group, group_name, action) if create_animation: - creator_plugin = lib.get_creator_by_name("CreateAnimation") + creator_plugin = get_legacy_creator_by_name("CreateAnimation") if not creator_plugin: raise ValueError("Creator plugin \"CreateAnimation\" was " "not found.") diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index e4355ed3d4..15fd3575d5 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -2,10 +2,10 @@ import os from maya import cmds from openpype.api import get_project_settings -from openpype.lib import get_creator_by_name -from openpype.pipeline import ( - legacy_io, +from openpype.pipeline import legacy_io +from openpype.pipeline.create import ( legacy_create, + get_legacy_creator_by_name, ) import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import maintained_selection @@ -153,7 +153,9 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.log.info("Creating subset: {}".format(namespace)) # Create the animation instance - creator_plugin = get_creator_by_name(self.animation_creator_name) + creator_plugin = get_legacy_creator_by_name( + self.animation_creator_name + ) with maintained_selection(): cmds.select([output, controls] + roots, noExpand=True) legacy_create( From 88a11e86f4a710444acb5d025f672834b9aa2404 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 18:56:01 +0200 Subject: [PATCH 0940/1030] copied code to openpype/pipeline/create content --- openpype/pipeline/create/constants.py | 2 + openpype/pipeline/create/subset_name.py | 143 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 openpype/pipeline/create/subset_name.py diff --git a/openpype/pipeline/create/constants.py b/openpype/pipeline/create/constants.py index bfbbccfd12..3af9651947 100644 --- a/openpype/pipeline/create/constants.py +++ b/openpype/pipeline/create/constants.py @@ -1,6 +1,8 @@ SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_." +DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", + "DEFAULT_SUBSET_TEMPLATE", ) diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py new file mode 100644 index 0000000000..d5dcf44c04 --- /dev/null +++ b/openpype/pipeline/create/subset_name.py @@ -0,0 +1,143 @@ +import os + +from openpype.client import get_asset_by_id +from openpype.settings import get_project_settings +from openpype.lib import filter_profiles, prepare_template_data +from openpype.pipeline import legacy_io + +from .constants import DEFAULT_SUBSET_TEMPLATE + + +class TaskNotSetError(KeyError): + def __init__(self, msg=None): + if not msg: + msg = "Creator's subset name template requires task name." + super(TaskNotSetError, self).__init__(msg) + + +def get_subset_name_with_asset_doc( + family, + variant, + task_name, + asset_doc, + project_name=None, + host_name=None, + default_template=None, + dynamic_data=None +): + """Calculate subset name based on passed context and OpenPype settings. + + Subst name templates are defined in `project_settings/global/tools/creator + /subset_name_profiles` where are profiles with host name, family, task name + and task type filters. If context does not match any profile then + `DEFAULT_SUBSET_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate subset + name. + + Args: + family (str): Instance family. + variant (str): In most of cases it is user input during creation. + task_name (str): Task name on which context is instance created. + asset_doc (dict): Queried asset document with it's tasks in data. + Used to get task type. + project_name (str): Name of project on which is instance created. + Important for project settings that are loaded. + host_name (str): One of filtering criteria for template profile + filters. + default_template (str): Default template if any profile does not match + passed context. Constant 'DEFAULT_SUBSET_TEMPLATE' is used if + is not passed. + dynamic_data (dict): Dynamic data specific for a creator which creates + instance. + dbcon (AvalonMongoDB): Mongo connection to be able query asset document + if 'asset_doc' is not passed. + """ + + if not family: + return "" + + if not host_name: + host_name = os.environ["AVALON_APP"] + + # Use only last part of class family value split by dot (`.`) + family = family.rsplit(".", 1)[-1] + + if project_name is None: + project_name = legacy_io.Session["AVALON_PROJECT"] + + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + # Get settings + tools_settings = get_project_settings(project_name)["global"]["tools"] + profiles = tools_settings["creator"]["subset_name_profiles"] + filtering_criteria = { + "families": family, + "hosts": host_name, + "tasks": task_name, + "task_types": task_type + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + template = None + if matching_profile: + template = matching_profile["template"] + + # Make sure template is set (matching may have empty string) + if not template: + template = default_template or DEFAULT_SUBSET_TEMPLATE + + # Simple check of task name existence for template with {task} in + # - missing task should be possible only in Standalone publisher + if not task_name and "{task" in template.lower(): + raise TaskNotSetError() + + fill_pairs = { + "variant": variant, + "family": family, + "task": task_name + } + if dynamic_data: + # Dynamic data may override default values + for key, value in dynamic_data.items(): + fill_pairs[key] = value + + return template.format(**prepare_template_data(fill_pairs)) + + +def get_subset_name( + family, + variant, + task_name, + asset_id, + project_name=None, + host_name=None, + default_template=None, + dynamic_data=None, + dbcon=None +): + """Calculate subset name using OpenPype settings. + + This variant of function expects asset id as argument. + + This is legacy function should be replaced with + `get_subset_name_with_asset_doc` where asset document is expected. + """ + + if project_name is None: + project_name = dbcon.project_name + + asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) + + return get_subset_name_with_asset_doc( + family, + variant, + task_name, + asset_doc or {}, + project_name, + host_name, + default_template, + dynamic_data + ) From 65b3a9a5a399bcd5fc633b96250623cf0f287292 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 18:57:07 +0200 Subject: [PATCH 0941/1030] added ability to pass project settings --- openpype/pipeline/create/subset_name.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py index d5dcf44c04..b6028d6427 100644 --- a/openpype/pipeline/create/subset_name.py +++ b/openpype/pipeline/create/subset_name.py @@ -23,7 +23,8 @@ def get_subset_name_with_asset_doc( project_name=None, host_name=None, default_template=None, - dynamic_data=None + dynamic_data=None, + project_settings=None ): """Calculate subset name based on passed context and OpenPype settings. @@ -71,7 +72,9 @@ def get_subset_name_with_asset_doc( task_type = task_info.get("type") # Get settings - tools_settings = get_project_settings(project_name)["global"]["tools"] + if not project_settings: + project_settings = get_project_settings(project_name) + tools_settings = project_settings["global"]["tools"] profiles = tools_settings["creator"]["subset_name_profiles"] filtering_criteria = { "families": family, @@ -116,7 +119,7 @@ def get_subset_name( host_name=None, default_template=None, dynamic_data=None, - dbcon=None + project_settings=None ): """Calculate subset name using OpenPype settings. @@ -127,7 +130,7 @@ def get_subset_name( """ if project_name is None: - project_name = dbcon.project_name + project_name = legacy_io.Session["AVALON_PROJECT"] asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) @@ -139,5 +142,6 @@ def get_subset_name( project_name, host_name, default_template, - dynamic_data + dynamic_data, + project_settings ) From daea5fd45e52770dd59057c9d836bf8dd23643b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 18:58:59 +0200 Subject: [PATCH 0942/1030] import content to create level --- openpype/pipeline/create/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index bd196ccfd1..4f3d2c03e5 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -1,6 +1,14 @@ from .constants import ( - SUBSET_NAME_ALLOWED_SYMBOLS + SUBSET_NAME_ALLOWED_SYMBOLS, + DEFAULT_SUBSET_TEMPLATE, ) + +from .subset_name import ( + TaskNotSetError, + get_subset_name, + get_subset_name_with_asset_doc, +) + from .creator_plugins import ( CreatorError, @@ -30,6 +38,11 @@ from .legacy_create import ( __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", + "DEFAULT_SUBSET_TEMPLATE", + + "TaskNotSetError", + "get_subset_name", + "get_subset_name_with_asset_doc", "CreatorError", From 476153e81c31e5b755159618368eccbfb1d68b1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 19:01:56 +0200 Subject: [PATCH 0943/1030] changed imports of task not set error --- .../traypublisher/plugins/create/create_movie_batch.py | 6 ++++-- openpype/tools/publisher/widgets/create_dialog.py | 4 ++-- openpype/tools/publisher/widgets/widgets.py | 6 ++++-- openpype/tools/standalonepublish/widgets/widget_family.py | 8 +++++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index c5f0d6b75e..5d0fe4b177 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -6,13 +6,15 @@ from openpype.client import get_assets, get_asset_by_name from openpype.lib import ( FileDef, BoolDef, - get_subset_name_with_asset_doc, - TaskNotSetError, ) from openpype.pipeline import ( CreatedInstance, CreatorError ) +from openpype.pipeline.create import ( + get_subset_name_with_asset_doc, + TaskNotSetError, +) from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index d4740b2493..173df7d5c8 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -11,10 +11,10 @@ except Exception: from Qt import QtWidgets, QtCore, QtGui from openpype.client import get_asset_by_name, get_subsets -from openpype.lib import TaskNotSetError from openpype.pipeline.create import ( CreatorError, - SUBSET_NAME_ALLOWED_SYMBOLS + SUBSET_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, ) from openpype.tools.utils import ( ErrorMessageBox, diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 5a5f8c4c37..aa7e3be687 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -6,7 +6,6 @@ import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome -from openpype.lib import TaskNotSetError from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -17,7 +16,10 @@ from openpype.tools.utils import ( BaseClickableFrame, set_style_property, ) -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) from .assets_widget import AssetsDialog from .tasks_widget import TasksModel from .icons import ( diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index 1736be84ab..eab66d75b3 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -8,10 +8,12 @@ from openpype.client import ( get_subsets, get_last_version_by_subset_id, ) -from openpype.api import get_project_settings +from openpype.settings import get_project_settings from openpype.pipeline import LegacyCreator -from openpype.lib import TaskNotSetError -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget From 7e59a577a66f857ecd28920ed457915e14c1f0b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 19:12:28 +0200 Subject: [PATCH 0944/1030] use new import of 'get_subset_name_with_asset_doc' --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 2 +- .../hosts/flame/plugins/publish/collect_timeline_otio.py | 6 +++--- openpype/hosts/harmony/plugins/publish/collect_workfile.py | 4 ++-- openpype/hosts/photoshop/plugins/publish/collect_review.py | 2 +- .../hosts/photoshop/plugins/publish/collect_workfile.py | 2 +- .../plugins/publish/collect_bulk_mov_instances.py | 2 +- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 2 +- .../hosts/tvpaint/plugins/publish/collect_scene_render.py | 2 +- openpype/hosts/tvpaint/plugins/publish/collect_workfile.py | 2 +- .../webpublisher/plugins/publish/collect_published_files.py | 6 ++---- .../plugins/publish/collect_tvpaint_instances.py | 2 +- 11 files changed, 15 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index fef5448a4c..b1f40113a4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -1,8 +1,8 @@ import os import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc from openpype.pipeline import legacy_io +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index 0a9b0db334..c0c7eee7f2 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -1,9 +1,9 @@ import pyblish.api -import openpype.lib as oplib -from openpype.pipeline import legacy_io import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export +from openpype.pipeline import legacy_io +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollecTimelineOTIO(pyblish.api.ContextPlugin): @@ -24,7 +24,7 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): sequence = opfapi.get_current_sequence(opfapi.CTX.selection) # create subset name - subset_name = oplib.get_subset_name_with_asset_doc( + subset_name = get_subset_name_with_asset_doc( family, variant, task_name, diff --git a/openpype/hosts/harmony/plugins/publish/collect_workfile.py b/openpype/hosts/harmony/plugins/publish/collect_workfile.py index c0493315a4..924661d310 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_workfile.py +++ b/openpype/hosts/harmony/plugins/publish/collect_workfile.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """Collect current workfile from Harmony.""" -import pyblish.api import os +import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 2ea5503f3f..ce475524a7 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -10,7 +10,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectReview(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 9cf6d5227e..5e673bebb1 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py index 052a97af7d..7a66026e1c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py @@ -2,8 +2,8 @@ import copy import json import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc from openpype.client import get_asset_by_name +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectBulkMovInstances(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 9b6d5c4879..68bfa8ef6a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -3,8 +3,8 @@ import copy import pyblish.api from openpype.client import get_asset_by_name -from openpype.lib import get_subset_name_with_asset_doc from openpype.pipeline import legacy_io +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectInstances(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 20c5bb586a..a7bc2f3c76 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -3,7 +3,7 @@ import copy import pyblish.api from openpype.client import get_asset_by_name -from openpype.lib import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectRenderScene(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index 88c5f4dbc7..f88b32b980 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -3,8 +3,8 @@ import json import pyblish.api from openpype.client import get_asset_by_name -from openpype.lib import get_subset_name_with_asset_doc from openpype.pipeline import legacy_io +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 20e277d794..5b0a4a6910 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -23,10 +23,8 @@ from openpype.lib import ( get_ffprobe_streams, convert_ffprobe_fps_value, ) -from openpype.lib.plugin_tools import ( - parse_json, - get_subset_name_with_asset_doc -) +from openpype.lib.plugin_tools import parse_json +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectPublishedFiles(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py index 92f581be5f..3a9f8eb8f2 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py @@ -10,7 +10,7 @@ import re import copy import pyblish.api -from openpype.lib import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name_with_asset_doc class CollectTVPaintInstances(pyblish.api.ContextPlugin): From ce31b9a47706f0c71f56fc9625d560e9cc5185a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 19:13:23 +0200 Subject: [PATCH 0945/1030] provide more data as arguments during publishing --- .../aftereffects/plugins/publish/collect_workfile.py | 3 ++- .../flame/plugins/publish/collect_timeline_otio.py | 3 +++ .../harmony/plugins/publish/collect_workfile.py | 3 ++- .../photoshop/plugins/publish/collect_review.py | 3 ++- .../photoshop/plugins/publish/collect_workfile.py | 3 ++- .../plugins/publish/collect_bulk_mov_instances.py | 4 +++- .../tvpaint/plugins/publish/collect_instances.py | 3 ++- .../tvpaint/plugins/publish/collect_scene_render.py | 3 ++- .../tvpaint/plugins/publish/collect_workfile.py | 3 ++- .../plugins/publish/collect_published_files.py | 9 +++++++-- .../plugins/publish/collect_tvpaint_instances.py | 12 ++++++++---- 11 files changed, 35 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index b1f40113a4..bd52f569a3 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -77,7 +77,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"] + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] ) # Create instance instance = context.create_instance(subset) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index c0c7eee7f2..e57ef270b8 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -29,6 +29,9 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): variant, task_name, asset_doc, + context.data["projectName"], + context.data["hostName"], + project_settings=context.data["project_settings"] ) # adding otio timeline to context diff --git a/openpype/hosts/harmony/plugins/publish/collect_workfile.py b/openpype/hosts/harmony/plugins/publish/collect_workfile.py index 924661d310..3d1d2f03c2 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_workfile.py +++ b/openpype/hosts/harmony/plugins/publish/collect_workfile.py @@ -23,7 +23,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"] + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] ) # Create instance diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index ce475524a7..eb2ad644e5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -33,7 +33,8 @@ class CollectReview(pyblish.api.ContextPlugin): context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"] + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] ) instance = context.create_instance(subset) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 5e673bebb1..21ec914910 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -30,7 +30,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"] + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] ) file_path = context.data["currentFile"] diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py index 7a66026e1c..fa99a8c7a7 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py @@ -49,7 +49,9 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): self.subset_name_variant, task_name, asset_doc, - project_name + project_name, + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] ) instance_name = f"{asset_name}_{subset_name}" diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 68bfa8ef6a..cd7eccc067 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -113,7 +113,8 @@ class CollectInstances(pyblish.api.ContextPlugin): task_name, asset_doc, project_name, - host_name + host_name, + project_settings=context.data["project_settings"] ) instance_data["subset"] = new_subset_name diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index a7bc2f3c76..d909317274 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -82,7 +82,8 @@ class CollectRenderScene(pyblish.api.ContextPlugin): asset_doc, project_name, host_name, - dynamic_data=dynamic_data + dynamic_data=dynamic_data, + project_settings=context.data["project_settings"] ) instance_data = { diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index f88b32b980..ef67ae8003 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -45,7 +45,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): task_name, asset_doc, project_name, - host_name + host_name, + project_settings=context.data["project_settings"] ) # Create Workfile instance diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 5b0a4a6910..4a497a9514 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -79,8 +79,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): extension.replace(".", '')) subset_name = get_subset_name_with_asset_doc( - family, variant, task_name, asset_doc, - project_name=project_name, host_name="webpublisher" + family, + variant, + task_name, + asset_doc, + project_name=project_name, + host_name="webpublisher", + project_settings=context.data["project_settings"] ) version = self._get_next_version( project_name, asset_doc, subset_name diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py index 3a9f8eb8f2..bdacdbdc26 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py @@ -53,7 +53,8 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): task_name, asset_doc, project_name, - host_name + host_name, + project_settings=context.data["project_settings"] ) workfile_instance = self._create_workfile_instance( context, workfile_subset_name @@ -67,7 +68,8 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): task_name, asset_doc, project_name, - host_name + host_name, + project_settings=context.data["project_settings"] ) review_instance = self._create_review_instance( context, review_subset_name @@ -121,7 +123,8 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): asset_doc, project_name, host_name, - dynamic_data=dynamic_data + dynamic_data=dynamic_data, + project_settings=context.data["project_settings"] ) instance = self._create_render_pass_instance( @@ -144,7 +147,8 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): asset_doc, project_name, host_name, - dynamic_data=dynamic_data + dynamic_data=dynamic_data, + project_settings=context.data["project_settings"] ) instance = self._create_render_layer_instance( context, layers, subset_name From df0565222c0f0061ca34472a02f5aa1747faf32e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 19:15:54 +0200 Subject: [PATCH 0946/1030] marked functions in openpype.lib as deprecated --- openpype/lib/plugin_tools.py | 94 +++++++++++------------------------- 1 file changed, 28 insertions(+), 66 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 060db94ae0..6534e7355f 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -8,16 +8,10 @@ import json import warnings import functools -from openpype.client import get_asset_by_id from openpype.settings import get_project_settings -from .profiles_filtering import filter_profiles - log = logging.getLogger(__name__) -# Subset name template used when plugin does not have defined any -DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" - class PluginToolsDeprecatedWarning(DeprecationWarning): pass @@ -64,13 +58,14 @@ def deprecated(new_destination): return _decorator(func) -class TaskNotSetError(KeyError): - def __init__(self, msg=None): - if not msg: - msg = "Creator's subset name template requires task name." - super(TaskNotSetError, self).__init__(msg) +@deprecated("openpype.pipeline.create.TaskNotSetError") +def TaskNotSetError(*args, **kwargs): + from openpype.pipeline.create import TaskNotSetError + + return TaskNotSetError(*args, **kwargs) +@deprecated("openpype.pipeline.create.get_subset_name_with_asset_doc") def get_subset_name_with_asset_doc( family, variant, @@ -109,61 +104,22 @@ def get_subset_name_with_asset_doc( dbcon (AvalonMongoDB): Mongo connection to be able query asset document if 'asset_doc' is not passed. """ - if not family: - return "" - if not host_name: - host_name = os.environ["AVALON_APP"] + from openpype.pipeline.create import get_subset_name_with_asset_doc - # Use only last part of class family value split by dot (`.`) - family = family.rsplit(".", 1)[-1] - - if project_name is None: - from openpype.pipeline import legacy_io - - project_name = legacy_io.Session["AVALON_PROJECT"] - - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - # Get settings - tools_settings = get_project_settings(project_name)["global"]["tools"] - profiles = tools_settings["creator"]["subset_name_profiles"] - filtering_criteria = { - "families": family, - "hosts": host_name, - "tasks": task_name, - "task_types": task_type - } - - matching_profile = filter_profiles(profiles, filtering_criteria) - template = None - if matching_profile: - template = matching_profile["template"] - - # Make sure template is set (matching may have empty string) - if not template: - template = default_template or DEFAULT_SUBSET_TEMPLATE - - # Simple check of task name existence for template with {task} in - # - missing task should be possible only in Standalone publisher - if not task_name and "{task" in template.lower(): - raise TaskNotSetError() - - fill_pairs = { - "variant": variant, - "family": family, - "task": task_name - } - if dynamic_data: - # Dynamic data may override default values - for key, value in dynamic_data.items(): - fill_pairs[key] = value - - return template.format(**prepare_template_data(fill_pairs)) + return get_subset_name_with_asset_doc( + family, + variant, + task_name, + asset_doc, + project_name, + host_name, + default_template, + dynamic_data + ) +@deprecated("openpype.pipeline.create.get_subset_name") def get_subset_name( family, variant, @@ -183,16 +139,16 @@ def get_subset_name( `get_subset_name_with_asset_doc` where asset document is expected. """ + from openpype.pipeline.create import get_subset_name + if project_name is None: project_name = dbcon.project_name - asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) - - return get_subset_name_with_asset_doc( + return get_subset_name( family, variant, task_name, - asset_doc or {}, + asset_id, project_name, host_name, default_template, @@ -254,6 +210,9 @@ def filter_pyblish_plugins(plugins): Args: plugins (dict): Dictionary of plugins produced by :mod:`pyblish-base` `discover()` method. + + Deprecated: + Function will be removed after release version 3.15.* """ from openpype.pipeline.publish.lib import filter_pyblish_plugins @@ -277,6 +236,9 @@ def set_plugin_attributes_from_settings( Value from environment `AVALON_APP` is used if not entered. project_name (str): Name of project for which settings will be loaded. Value from environment `AVALON_PROJECT` is used if not entered. + + Deprecated: + Function will be removed after release version 3.15.* """ # Function is not used anymore From 7a4cd9c1faca8c4ca3d7f2fea871c241c38b1320 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 19:20:04 +0200 Subject: [PATCH 0947/1030] removed 'get_subset_name' and renamed 'get_subset_name_with_asset_doc' to 'get_subset_name' --- .../plugins/publish/collect_workfile.py | 4 +- .../plugins/publish/collect_timeline_otio.py | 4 +- .../plugins/publish/collect_workfile.py | 4 +- .../plugins/publish/collect_review.py | 4 +- .../plugins/publish/collect_workfile.py | 4 +- .../publish/collect_bulk_mov_instances.py | 4 +- .../plugins/create/create_movie_batch.py | 6 +-- .../plugins/publish/collect_instances.py | 4 +- .../plugins/publish/collect_scene_render.py | 4 +- .../plugins/publish/collect_workfile.py | 4 +- .../publish/collect_published_files.py | 4 +- .../publish/collect_tvpaint_instances.py | 10 ++--- openpype/lib/plugin_tools.py | 13 +++--- openpype/pipeline/create/__init__.py | 2 - openpype/pipeline/create/subset_name.py | 40 +------------------ 15 files changed, 37 insertions(+), 74 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index bd52f569a3..3c5013b3bd 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectWorkfile(pyblish.api.ContextPlugin): @@ -71,7 +71,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # workfile instance family = "workfile" - subset = get_subset_name_with_asset_doc( + subset = get_subset_name( family, self.default_variant, context.data["anatomyData"]["task"]["name"], diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index e57ef270b8..917041e053 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -3,7 +3,7 @@ import pyblish.api import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollecTimelineOTIO(pyblish.api.ContextPlugin): @@ -24,7 +24,7 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): sequence = opfapi.get_current_sequence(opfapi.CTX.selection) # create subset name - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( family, variant, task_name, diff --git a/openpype/hosts/harmony/plugins/publish/collect_workfile.py b/openpype/hosts/harmony/plugins/publish/collect_workfile.py index 3d1d2f03c2..3624147435 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_workfile.py +++ b/openpype/hosts/harmony/plugins/publish/collect_workfile.py @@ -3,7 +3,7 @@ import os import pyblish.api -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectWorkfile(pyblish.api.ContextPlugin): @@ -17,7 +17,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): """Plugin entry point.""" family = "workfile" basename = os.path.basename(context.data["currentFile"]) - subset = get_subset_name_with_asset_doc( + subset = get_subset_name( family, "", context.data["anatomyData"]["task"]["name"], diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index eb2ad644e5..7f395b46d7 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -10,7 +10,7 @@ import os import pyblish.api -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectReview(pyblish.api.ContextPlugin): @@ -27,7 +27,7 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" - subset = get_subset_name_with_asset_doc( + subset = get_subset_name( family, context.data.get("variant", ''), context.data["anatomyData"]["task"]["name"], diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 21ec914910..9a5aad5569 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectWorkfile(pyblish.api.ContextPlugin): @@ -24,7 +24,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): family = "workfile" # context.data["variant"] might come only from collect_batch_data variant = context.data.get("variant") or self.default_variant - subset = get_subset_name_with_asset_doc( + subset = get_subset_name( family, variant, context.data["anatomyData"]["task"]["name"], diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py index fa99a8c7a7..7925b0ecf3 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py @@ -3,7 +3,7 @@ import json import pyblish.api from openpype.client import get_asset_by_name -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectBulkMovInstances(pyblish.api.InstancePlugin): @@ -44,7 +44,7 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): task_name = available_task_names[_task_name_low] break - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( self.new_instance_family, self.subset_name_variant, task_name, diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index 5d0fe4b177..abe29d7473 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -12,7 +12,7 @@ from openpype.pipeline import ( CreatorError ) from openpype.pipeline.create import ( - get_subset_name_with_asset_doc, + get_subset_name, TaskNotSetError, ) @@ -132,7 +132,7 @@ class BatchMovieCreator(TrayPublishCreator): task_name = self._get_task_name(asset_doc) try: - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( self.family, variant, task_name, @@ -145,7 +145,7 @@ class BatchMovieCreator(TrayPublishCreator): # but user have ability to change it # NOTE: This expect that there is not task 'Undefined' on asset task_name = "Undefined" - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( self.family, variant, task_name, diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index cd7eccc067..ae1326a5bd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -4,7 +4,7 @@ import pyblish.api from openpype.client import get_asset_by_name from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectInstances(pyblish.api.ContextPlugin): @@ -107,7 +107,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Use empty variant value variant = "" task_name = legacy_io.Session["AVALON_TASK"] - new_subset_name = get_subset_name_with_asset_doc( + new_subset_name = get_subset_name( family, variant, task_name, diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index d909317274..92a2815ba0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -3,7 +3,7 @@ import copy import pyblish.api from openpype.client import get_asset_by_name -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectRenderScene(pyblish.api.ContextPlugin): @@ -75,7 +75,7 @@ class CollectRenderScene(pyblish.api.ContextPlugin): dynamic_data["render_pass"] = dynamic_data["renderpass"] task_name = workfile_context["task"] - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( "render", variant, task_name, diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index ef67ae8003..8c7c8c3899 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -4,7 +4,7 @@ import pyblish.api from openpype.client import get_asset_by_name from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectWorkfile(pyblish.api.ContextPlugin): @@ -39,7 +39,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # Use empty variant value variant = "" task_name = legacy_io.Session["AVALON_TASK"] - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( family, variant, task_name, diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 4a497a9514..f2d1d19609 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -24,7 +24,7 @@ from openpype.lib import ( convert_ffprobe_fps_value, ) from openpype.lib.plugin_tools import parse_json -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectPublishedFiles(pyblish.api.ContextPlugin): @@ -78,7 +78,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): is_sequence, extension.replace(".", '')) - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( family, variant, task_name, diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py index bdacdbdc26..948e86c23e 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py @@ -10,7 +10,7 @@ import re import copy import pyblish.api -from openpype.pipeline.create import get_subset_name_with_asset_doc +from openpype.pipeline.create import get_subset_name class CollectTVPaintInstances(pyblish.api.ContextPlugin): @@ -47,7 +47,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): new_instances = [] # Workfile instance - workfile_subset_name = get_subset_name_with_asset_doc( + workfile_subset_name = get_subset_name( self.workfile_family, self.workfile_variant, task_name, @@ -62,7 +62,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): new_instances.append(workfile_instance) # Review instance - review_subset_name = get_subset_name_with_asset_doc( + review_subset_name = get_subset_name( self.review_family, self.review_variant, task_name, @@ -116,7 +116,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): "family": "render" } - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( self.render_pass_family, render_pass, task_name, @@ -140,7 +140,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): # Override family for subset name "family": "render" } - subset_name = get_subset_name_with_asset_doc( + subset_name = get_subset_name( self.render_layer_family, variant, task_name, diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 6534e7355f..065188625e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -8,6 +8,7 @@ import json import warnings import functools +from openpype.client import get_asset_by_id from openpype.settings import get_project_settings log = logging.getLogger(__name__) @@ -65,7 +66,7 @@ def TaskNotSetError(*args, **kwargs): return TaskNotSetError(*args, **kwargs) -@deprecated("openpype.pipeline.create.get_subset_name_with_asset_doc") +@deprecated("openpype.pipeline.create.get_subset_name") def get_subset_name_with_asset_doc( family, variant, @@ -105,9 +106,9 @@ def get_subset_name_with_asset_doc( if 'asset_doc' is not passed. """ - from openpype.pipeline.create import get_subset_name_with_asset_doc + from openpype.pipeline.create import get_subset_name - return get_subset_name_with_asset_doc( + return get_subset_name( family, variant, task_name, @@ -119,7 +120,7 @@ def get_subset_name_with_asset_doc( ) -@deprecated("openpype.pipeline.create.get_subset_name") +@deprecated def get_subset_name( family, variant, @@ -144,11 +145,13 @@ def get_subset_name( if project_name is None: project_name = dbcon.project_name + asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) + return get_subset_name( family, variant, task_name, - asset_id, + asset_doc, project_name, host_name, default_template, diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 4f3d2c03e5..b698224924 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -6,7 +6,6 @@ from .constants import ( from .subset_name import ( TaskNotSetError, get_subset_name, - get_subset_name_with_asset_doc, ) from .creator_plugins import ( @@ -42,7 +41,6 @@ __all__ = ( "TaskNotSetError", "get_subset_name", - "get_subset_name_with_asset_doc", "CreatorError", diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py index b6028d6427..f508263708 100644 --- a/openpype/pipeline/create/subset_name.py +++ b/openpype/pipeline/create/subset_name.py @@ -1,6 +1,5 @@ import os -from openpype.client import get_asset_by_id from openpype.settings import get_project_settings from openpype.lib import filter_profiles, prepare_template_data from openpype.pipeline import legacy_io @@ -15,7 +14,7 @@ class TaskNotSetError(KeyError): super(TaskNotSetError, self).__init__(msg) -def get_subset_name_with_asset_doc( +def get_subset_name( family, variant, task_name, @@ -108,40 +107,3 @@ def get_subset_name_with_asset_doc( fill_pairs[key] = value return template.format(**prepare_template_data(fill_pairs)) - - -def get_subset_name( - family, - variant, - task_name, - asset_id, - project_name=None, - host_name=None, - default_template=None, - dynamic_data=None, - project_settings=None -): - """Calculate subset name using OpenPype settings. - - This variant of function expects asset id as argument. - - This is legacy function should be replaced with - `get_subset_name_with_asset_doc` where asset document is expected. - """ - - if project_name is None: - project_name = legacy_io.Session["AVALON_PROJECT"] - - asset_doc = get_asset_by_id(project_name, asset_id, fields=["data.tasks"]) - - return get_subset_name_with_asset_doc( - family, - variant, - task_name, - asset_doc or {}, - project_name, - host_name, - default_template, - dynamic_data, - project_settings - ) From cdab361dd933781965be776389fb0af48af4cf72 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 24 Aug 2022 04:13:30 +0000 Subject: [PATCH 0948/1030] [Automated] Bump version --- CHANGELOG.md | 50 ++++++++++++++++++++++----------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a3cb27e6..a45f65b6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,40 @@ # Changelog -## [3.14.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.0...HEAD) +### 📖 Documentation + +- Documentation: Few updates [\#3698](https://github.com/pypeclub/OpenPype/pull/3698) +- Documentation: Settings development [\#3660](https://github.com/pypeclub/OpenPype/pull/3660) + **🚀 Enhancements** +- Unreal: Define unreal as module and use host class [\#3701](https://github.com/pypeclub/OpenPype/pull/3701) +- Settings: Lock settings UI session [\#3700](https://github.com/pypeclub/OpenPype/pull/3700) - Ftrack: More logs related to auto sync value change [\#3671](https://github.com/pypeclub/OpenPype/pull/3671) **🐛 Bug fixes** +- Settings: Fix project overrides save [\#3708](https://github.com/pypeclub/OpenPype/pull/3708) +- Workfiles tool: Fix published workfile filtering [\#3704](https://github.com/pypeclub/OpenPype/pull/3704) +- PS, AE: Provide default variant value for workfile subset [\#3703](https://github.com/pypeclub/OpenPype/pull/3703) - RoyalRender: handle host name that is not set [\#3695](https://github.com/pypeclub/OpenPype/pull/3695) +- Flame: retime is working on clip publishing [\#3684](https://github.com/pypeclub/OpenPype/pull/3684) + +**🔀 Refactored code** + +- Loader: Subset groups using client operations [\#3710](https://github.com/pypeclub/OpenPype/pull/3710) +- TVPaint: Defined as module [\#3707](https://github.com/pypeclub/OpenPype/pull/3707) +- StandalonePublisher: Define StandalonePublisher as module [\#3706](https://github.com/pypeclub/OpenPype/pull/3706) +- TrayPublisher: Define TrayPublisher as module [\#3705](https://github.com/pypeclub/OpenPype/pull/3705) +- General: Move context specific functions to context tools [\#3702](https://github.com/pypeclub/OpenPype/pull/3702) + +**Merged pull requests:** + +- Deadline: better logging for DL webservice failures [\#3694](https://github.com/pypeclub/OpenPype/pull/3694) +- Photoshop: resize saved images in ExtractReview for ffmpeg [\#3676](https://github.com/pypeclub/OpenPype/pull/3676) ## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18) @@ -91,7 +115,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) -- Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** @@ -111,32 +134,9 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.2-nightly.4...3.12.2) -### 📖 Documentation - -- Update website with more studios [\#3554](https://github.com/pypeclub/OpenPype/pull/3554) -- Documentation: Update publishing dev docs [\#3549](https://github.com/pypeclub/OpenPype/pull/3549) - -**🚀 Enhancements** - -- General: Global thumbnail extractor is ready for more cases [\#3561](https://github.com/pypeclub/OpenPype/pull/3561) - **🐛 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) -- General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) -- 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) - -**🔀 Refactored code** - -- General: Use query functions in integrator [\#3563](https://github.com/pypeclub/OpenPype/pull/3563) - -**Merged pull requests:** - -- Maya: fix active pane loss [\#3566](https://github.com/pypeclub/OpenPype/pull/3566) ## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13) diff --git a/openpype/version.py b/openpype/version.py index 174aca1e6c..e738689c20 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.1-nightly.1" +__version__ = "3.14.1-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index e01cc71201..bfc570f597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.14.1-nightly.1" # OpenPype +version = "3.14.1-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 0053a7ad7709327e5cce0cb0a3ead5e100c2c08e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 10:38:55 +0200 Subject: [PATCH 0949/1030] fix last saved object access --- openpype/settings/handlers.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 79ec6248ac..def8c16ea7 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -831,7 +831,10 @@ class MongoSettingsHandler(SettingsHandler): data_cache.update_last_saved_info(last_saved_info) self._save_project_data( - project_name, self._project_settings_key, data_cache + project_name, + self._project_settings_key, + data_cache, + last_saved_info ) def save_project_anatomy(self, project_name, anatomy_data): @@ -849,8 +852,16 @@ class MongoSettingsHandler(SettingsHandler): self._save_project_anatomy_data(project_name, data_cache) else: + last_saved_info = SettingsStateInfo.create_new( + self._current_version, + PROJECT_ANATOMY_KEY, + project_name + ) self._save_project_data( - project_name, self._project_anatomy_key, data_cache + project_name, + self._project_anatomy_key, + data_cache, + last_saved_info ) @classmethod @@ -931,14 +942,16 @@ class MongoSettingsHandler(SettingsHandler): {"$set": update_dict} ) - def _save_project_data(self, project_name, doc_type, data_cache): + def _save_project_data( + self, project_name, doc_type, data_cache, last_saved_info + ): is_default = bool(project_name is None) query_filter = { "type": doc_type, "is_default": is_default, "version": self._current_version } - last_saved_info = data_cache.last_saved_info + new_project_settings_doc = { "type": doc_type, "data": data_cache.data, @@ -946,6 +959,7 @@ class MongoSettingsHandler(SettingsHandler): "version": self._current_version, "last_saved_info": last_saved_info.to_data() } + if not is_default: query_filter["project_name"] = project_name new_project_settings_doc["project_name"] = project_name From 5ca80dbdea9d35a0610e91677b1d145675e20e70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 11:17:43 +0200 Subject: [PATCH 0950/1030] hiero is converted to module --- openpype/hosts/hiero/__init__.py | 47 +++++---------------------- openpype/hosts/hiero/module.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 openpype/hosts/hiero/module.py diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index d2ac82391b..a307e265d5 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -1,41 +1,10 @@ -import os -import platform +from .module import ( + HIERO_ROOT_DIR, + HieroModule, +) -def add_implementation_envs(env, _app): - # Add requirements to HIERO_PLUGIN_PATH - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_hiero_paths = [ - os.path.join(pype_root, "openpype", "hosts", "hiero", "api", "startup") - ] - old_hiero_path = env.get("HIERO_PLUGIN_PATH") or "" - for path in old_hiero_path.split(os.pathsep): - if not path: - continue - - norm_path = os.path.normpath(path) - if norm_path not in new_hiero_paths: - new_hiero_paths.append(norm_path) - - env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths) - env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) - - # Try to add QuickTime to PATH - quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" - if platform.system() == "windows" and os.path.exists(quick_time_path): - path_value = env.get("PATH") or "" - path_paths = [ - path - for path in path_value.split(os.pathsep) - if path - ] - path_paths.append(quick_time_path) - env["PATH"] = os.pathsep.join(path_paths) - - # Set default values if are not already set via settings - defaults = { - "LOGLEVEL": "DEBUG" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value +__all__ = ( + "HIERO_ROOT_DIR", + "HieroModule", +) diff --git a/openpype/hosts/hiero/module.py b/openpype/hosts/hiero/module.py new file mode 100644 index 0000000000..373b89962d --- /dev/null +++ b/openpype/hosts/hiero/module.py @@ -0,0 +1,54 @@ +import os +import platform +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class HieroModule(OpenPypeModule, IHostModule): + name = "hiero" + host_name = "hiero" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to HIERO_PLUGIN_PATH + new_hiero_paths = [ + os.path.join(HIERO_ROOT_DIR, "api", "startup") + ] + old_hiero_path = env.get("HIERO_PLUGIN_PATH") or "" + for path in old_hiero_path.split(os.pathsep): + if not path: + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_hiero_paths: + new_hiero_paths.append(norm_path) + + env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths) + env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + + # Set default values if are not already set via settings + defaults = { + "LOGLEVEL": "DEBUG" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + # Try to add QuickTime to PATH + quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" + if platform.system() == "windows" and os.path.exists(quick_time_path): + path_value = env.get("PATH") or "" + path_paths = [ + path + for path in path_value.split(os.pathsep) + if path + ] + path_paths.append(quick_time_path) + env["PATH"] = os.pathsep.join(path_paths) + + def get_workfile_extensions(self): + return [".hrox"] From 8839adaf89477086f62539c0c40a6a8baa05120e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 11:18:15 +0200 Subject: [PATCH 0951/1030] added protobuf as vendorized module for hiero --- openpype/hosts/hiero/module.py | 9 + .../hiero/vendor/google/protobuf/__init__.py | 33 + .../hiero/vendor/google/protobuf/any_pb2.py | 26 + .../hiero/vendor/google/protobuf/api_pb2.py | 32 + .../google/protobuf/compiler/__init__.py | 0 .../google/protobuf/compiler/plugin_pb2.py | 35 + .../vendor/google/protobuf/descriptor.py | 1224 +++++++++++ .../google/protobuf/descriptor_database.py | 177 ++ .../vendor/google/protobuf/descriptor_pb2.py | 1925 +++++++++++++++++ .../vendor/google/protobuf/descriptor_pool.py | 1295 +++++++++++ .../vendor/google/protobuf/duration_pb2.py | 26 + .../hiero/vendor/google/protobuf/empty_pb2.py | 26 + .../vendor/google/protobuf/field_mask_pb2.py | 26 + .../google/protobuf/internal/__init__.py | 0 .../protobuf/internal/_parameterized.py | 443 ++++ .../protobuf/internal/api_implementation.py | 112 + .../google/protobuf/internal/builder.py | 130 ++ .../google/protobuf/internal/containers.py | 710 ++++++ .../google/protobuf/internal/decoder.py | 1029 +++++++++ .../google/protobuf/internal/encoder.py | 829 +++++++ .../protobuf/internal/enum_type_wrapper.py | 124 ++ .../protobuf/internal/extension_dict.py | 213 ++ .../protobuf/internal/message_listener.py | 78 + .../internal/message_set_extensions_pb2.py | 36 + .../internal/missing_enum_values_pb2.py | 37 + .../internal/more_extensions_dynamic_pb2.py | 29 + .../protobuf/internal/more_extensions_pb2.py | 41 + .../protobuf/internal/more_messages_pb2.py | 556 +++++ .../protobuf/internal/no_package_pb2.py | 27 + .../protobuf/internal/python_message.py | 1539 +++++++++++++ .../google/protobuf/internal/type_checkers.py | 435 ++++ .../protobuf/internal/well_known_types.py | 878 ++++++++ .../google/protobuf/internal/wire_format.py | 268 +++ .../vendor/google/protobuf/json_format.py | 912 ++++++++ .../hiero/vendor/google/protobuf/message.py | 424 ++++ .../vendor/google/protobuf/message_factory.py | 185 ++ .../vendor/google/protobuf/proto_builder.py | 134 ++ .../vendor/google/protobuf/pyext/__init__.py | 0 .../google/protobuf/pyext/cpp_message.py | 65 + .../google/protobuf/pyext/python_pb2.py | 34 + .../vendor/google/protobuf/reflection.py | 95 + .../hiero/vendor/google/protobuf/service.py | 228 ++ .../google/protobuf/service_reflection.py | 295 +++ .../google/protobuf/source_context_pb2.py | 26 + .../vendor/google/protobuf/struct_pb2.py | 36 + .../vendor/google/protobuf/symbol_database.py | 194 ++ .../vendor/google/protobuf/text_encoding.py | 110 + .../vendor/google/protobuf/text_format.py | 1795 +++++++++++++++ .../vendor/google/protobuf/timestamp_pb2.py | 26 + .../hiero/vendor/google/protobuf/type_pb2.py | 42 + .../vendor/google/protobuf/util/__init__.py | 0 .../google/protobuf/util/json_format_pb2.py | 72 + .../protobuf/util/json_format_proto3_pb2.py | 129 ++ .../vendor/google/protobuf/wrappers_pb2.py | 42 + 54 files changed, 17192 insertions(+) create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/__init__.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/any_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/api_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/compiler/__init__.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/compiler/plugin_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/descriptor.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/descriptor_database.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/descriptor_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/descriptor_pool.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/duration_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/empty_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/field_mask_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/__init__.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/_parameterized.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/api_implementation.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/builder.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/containers.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/decoder.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/encoder.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/enum_type_wrapper.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/json_format.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/message.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/message_factory.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/pyext/__init__.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/reflection.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/service.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/text_format.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/util/__init__.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py create mode 100644 openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py diff --git a/openpype/hosts/hiero/module.py b/openpype/hosts/hiero/module.py index 373b89962d..375486e034 100644 --- a/openpype/hosts/hiero/module.py +++ b/openpype/hosts/hiero/module.py @@ -30,6 +30,15 @@ class HieroModule(OpenPypeModule, IHostModule): env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths) env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + # Add vendor to PYTHONPATH + python_path = env["PYTHONPATH"] + python_path_parts = [] + if python_path: + python_path_parts = python_path.split(os.pathsep) + vendor_path = os.path.join(HIERO_ROOT_DIR, "vendor") + python_path_parts.insert(0, vendor_path) + env["PYTHONPATH"] = os.pathsep.join(python_path_parts) + # Set default values if are not already set via settings defaults = { "LOGLEVEL": "DEBUG" diff --git a/openpype/hosts/hiero/vendor/google/protobuf/__init__.py b/openpype/hosts/hiero/vendor/google/protobuf/__init__.py new file mode 100644 index 0000000000..03f3b29ee7 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/__init__.py @@ -0,0 +1,33 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Copyright 2007 Google Inc. All Rights Reserved. + +__version__ = '3.20.1' diff --git a/openpype/hosts/hiero/vendor/google/protobuf/any_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/any_pb2.py new file mode 100644 index 0000000000..9121193d11 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/any_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/any.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"&\n\x03\x41ny\x12\x10\n\x08type_url\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42v\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _ANY._serialized_start=46 + _ANY._serialized_end=84 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/api_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/api_pb2.py new file mode 100644 index 0000000000..1721b10a75 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/api_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/api.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 +from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\x81\x02\n\x03\x41pi\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Method\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12&\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.Mixin\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x01\n\x06Method\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10request_type_url\x18\x02 \x01(\t\x12\x19\n\x11request_streaming\x18\x03 \x01(\x08\x12\x19\n\x11response_type_url\x18\x04 \x01(\t\x12\x1a\n\x12response_streaming\x18\x05 \x01(\x08\x12(\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.Option\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"#\n\x05Mixin\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04root\x18\x02 \x01(\tBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _API._serialized_start=113 + _API._serialized_end=370 + _METHOD._serialized_start=373 + _METHOD._serialized_end=586 + _MIXIN._serialized_start=588 + _MIXIN._serialized_end=623 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/compiler/__init__.py b/openpype/hosts/hiero/vendor/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/hiero/vendor/google/protobuf/compiler/plugin_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/compiler/plugin_pb2.py new file mode 100644 index 0000000000..715a891370 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/compiler/plugin_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/compiler/plugin.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"F\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\x05\x12\r\n\x05minor\x18\x02 \x01(\x05\x12\r\n\x05patch\x18\x03 \x01(\x05\x12\x0e\n\x06suffix\x18\x04 \x01(\t\"\xba\x01\n\x14\x43odeGeneratorRequest\x12\x18\n\x10\x66ile_to_generate\x18\x01 \x03(\t\x12\x11\n\tparameter\x18\x02 \x01(\t\x12\x38\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\x12;\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.Version\"\xc1\x02\n\x15\x43odeGeneratorResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x1a\n\x12supported_features\x18\x02 \x01(\x04\x12\x42\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.File\x1a\x7f\n\x04\x46ile\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0finsertion_point\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x0f \x01(\t\x12?\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfo\"8\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x42W\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb' + _VERSION._serialized_start=101 + _VERSION._serialized_end=171 + _CODEGENERATORREQUEST._serialized_start=174 + _CODEGENERATORREQUEST._serialized_end=360 + _CODEGENERATORRESPONSE._serialized_start=363 + _CODEGENERATORRESPONSE._serialized_end=684 + _CODEGENERATORRESPONSE_FILE._serialized_start=499 + _CODEGENERATORRESPONSE_FILE._serialized_end=626 + _CODEGENERATORRESPONSE_FEATURE._serialized_start=628 + _CODEGENERATORRESPONSE_FEATURE._serialized_end=684 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/descriptor.py b/openpype/hosts/hiero/vendor/google/protobuf/descriptor.py new file mode 100644 index 0000000000..ad70be9a11 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/descriptor.py @@ -0,0 +1,1224 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Descriptors essentially contain exactly the information found in a .proto +file, in types that make this information accessible in Python. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import threading +import warnings + +from google.protobuf.internal import api_implementation + +_USE_C_DESCRIPTORS = False +if api_implementation.Type() == 'cpp': + # Used by MakeDescriptor in cpp mode + import binascii + import os + from google.protobuf.pyext import _message + _USE_C_DESCRIPTORS = True + + +class Error(Exception): + """Base error for this module.""" + + +class TypeTransformationError(Error): + """Error transforming between python proto type and corresponding C++ type.""" + + +if _USE_C_DESCRIPTORS: + # This metaclass allows to override the behavior of code like + # isinstance(my_descriptor, FieldDescriptor) + # and make it return True when the descriptor is an instance of the extension + # type written in C++. + class DescriptorMetaclass(type): + def __instancecheck__(cls, obj): + if super(DescriptorMetaclass, cls).__instancecheck__(obj): + return True + if isinstance(obj, cls._C_DESCRIPTOR_CLASS): + return True + return False +else: + # The standard metaclass; nothing changes. + DescriptorMetaclass = type + + +class _Lock(object): + """Wrapper class of threading.Lock(), which is allowed by 'with'.""" + + def __new__(cls): + self = object.__new__(cls) + self._lock = threading.Lock() # pylint: disable=protected-access + return self + + def __enter__(self): + self._lock.acquire() + + def __exit__(self, exc_type, exc_value, exc_tb): + self._lock.release() + + +_lock = threading.Lock() + + +def _Deprecated(name): + if _Deprecated.count > 0: + _Deprecated.count -= 1 + warnings.warn( + 'Call to deprecated create function %s(). Note: Create unlinked ' + 'descriptors is going to go away. Please use get/find descriptors from ' + 'generated code or query the descriptor_pool.' + % name, + category=DeprecationWarning, stacklevel=3) + + +# Deprecated warnings will print 100 times at most which should be enough for +# users to notice and do not cause timeout. +_Deprecated.count = 100 + + +_internal_create_key = object() + + +class DescriptorBase(metaclass=DescriptorMetaclass): + + """Descriptors base class. + + This class is the base of all descriptor classes. It provides common options + related functionality. + + Attributes: + has_options: True if the descriptor has non-default options. Usually it + is not necessary to read this -- just call GetOptions() which will + happily return the default instance. However, it's sometimes useful + for efficiency, and also useful inside the protobuf implementation to + avoid some bootstrapping issues. + """ + + if _USE_C_DESCRIPTORS: + # The class, or tuple of classes, that are considered as "virtual + # subclasses" of this descriptor class. + _C_DESCRIPTOR_CLASS = () + + def __init__(self, options, serialized_options, options_class_name): + """Initialize the descriptor given its options message and the name of the + class of the options message. The name of the class is required in case + the options message is None and has to be created. + """ + self._options = options + self._options_class_name = options_class_name + self._serialized_options = serialized_options + + # Does this descriptor have non-default options? + self.has_options = (options is not None) or (serialized_options is not None) + + def _SetOptions(self, options, options_class_name): + """Sets the descriptor's options + + This function is used in generated proto2 files to update descriptor + options. It must not be used outside proto2. + """ + self._options = options + self._options_class_name = options_class_name + + # Does this descriptor have non-default options? + self.has_options = options is not None + + def GetOptions(self): + """Retrieves descriptor options. + + This method returns the options set or creates the default options for the + descriptor. + """ + if self._options: + return self._options + + from google.protobuf import descriptor_pb2 + try: + options_class = getattr(descriptor_pb2, + self._options_class_name) + except AttributeError: + raise RuntimeError('Unknown options class name %s!' % + (self._options_class_name)) + + with _lock: + if self._serialized_options is None: + self._options = options_class() + else: + self._options = _ParseOptions(options_class(), + self._serialized_options) + + return self._options + + +class _NestedDescriptorBase(DescriptorBase): + """Common class for descriptors that can be nested.""" + + def __init__(self, options, options_class_name, name, full_name, + file, containing_type, serialized_start=None, + serialized_end=None, serialized_options=None): + """Constructor. + + Args: + options: Protocol message options or None + to use default message options. + options_class_name (str): The class name of the above options. + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + file (FileDescriptor): Reference to file info. + containing_type: if provided, this is a nested descriptor, with this + descriptor as parent, otherwise None. + serialized_start: The start index (inclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_end: The end index (exclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_options: Protocol message serialized options or None. + """ + super(_NestedDescriptorBase, self).__init__( + options, serialized_options, options_class_name) + + self.name = name + # TODO(falk): Add function to calculate full_name instead of having it in + # memory? + self.full_name = full_name + self.file = file + self.containing_type = containing_type + + self._serialized_start = serialized_start + self._serialized_end = serialized_end + + def CopyToProto(self, proto): + """Copies this to the matching proto in descriptor_pb2. + + Args: + proto: An empty proto instance from descriptor_pb2. + + Raises: + Error: If self couldn't be serialized, due to to few constructor + arguments. + """ + if (self.file is not None and + self._serialized_start is not None and + self._serialized_end is not None): + proto.ParseFromString(self.file.serialized_pb[ + self._serialized_start:self._serialized_end]) + else: + raise Error('Descriptor does not contain serialization.') + + +class Descriptor(_NestedDescriptorBase): + + """Descriptor for a protocol message type. + + Attributes: + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + containing_type (Descriptor): Reference to the descriptor of the type + containing us, or None if this is top-level. + fields (list[FieldDescriptor]): Field descriptors for all fields in + this type. + fields_by_number (dict(int, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed + by "number" attribute in each FieldDescriptor. + fields_by_name (dict(str, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by + "name" attribute in each :class:`FieldDescriptor`. + nested_types (list[Descriptor]): Descriptor references + for all protocol message types nested within this one. + nested_types_by_name (dict(str, Descriptor)): Same Descriptor + objects as in :attr:`nested_types`, but indexed by "name" attribute + in each Descriptor. + enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references + for all enums contained within this type. + enum_types_by_name (dict(str, EnumDescriptor)): Same + :class:`EnumDescriptor` objects as in :attr:`enum_types`, but + indexed by "name" attribute in each EnumDescriptor. + enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping + from enum value name to :class:`EnumValueDescriptor` for that value. + extensions (list[FieldDescriptor]): All extensions defined directly + within this message type (NOT within a nested type). + extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor + objects as :attr:`extensions`, but indexed by "name" attribute of each + FieldDescriptor. + is_extendable (bool): Does this type define any extension ranges? + oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields + in this message. + oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in + :attr:`oneofs`, but indexed by "name" attribute. + file (FileDescriptor): Reference to file descriptor. + + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.Descriptor + + def __new__( + cls, + name=None, + full_name=None, + filename=None, + containing_type=None, + fields=None, + nested_types=None, + enum_types=None, + extensions=None, + options=None, + serialized_options=None, + is_extendable=True, + extension_ranges=None, + oneofs=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + syntax=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindMessageTypeByName(full_name) + + # NOTE(tmarek): The file argument redefining a builtin is nothing we can + # fix right now since we don't know how many clients already rely on the + # name of the argument. + def __init__(self, name, full_name, filename, containing_type, fields, + nested_types, enum_types, extensions, options=None, + serialized_options=None, + is_extendable=True, extension_ranges=None, oneofs=None, + file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin + syntax=None, create_key=None): + """Arguments to __init__() are as described in the description + of Descriptor fields above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('Descriptor') + + super(Descriptor, self).__init__( + options, 'MessageOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + # We have fields in addition to fields_by_name and fields_by_number, + # so that: + # 1. Clients can index fields by "order in which they're listed." + # 2. Clients can easily iterate over all fields with the terse + # syntax: for f in descriptor.fields: ... + self.fields = fields + for field in self.fields: + field.containing_type = self + self.fields_by_number = dict((f.number, f) for f in fields) + self.fields_by_name = dict((f.name, f) for f in fields) + self._fields_by_camelcase_name = None + + self.nested_types = nested_types + for nested_type in nested_types: + nested_type.containing_type = self + self.nested_types_by_name = dict((t.name, t) for t in nested_types) + + self.enum_types = enum_types + for enum_type in self.enum_types: + enum_type.containing_type = self + self.enum_types_by_name = dict((t.name, t) for t in enum_types) + self.enum_values_by_name = dict( + (v.name, v) for t in enum_types for v in t.values) + + self.extensions = extensions + for extension in self.extensions: + extension.extension_scope = self + self.extensions_by_name = dict((f.name, f) for f in extensions) + self.is_extendable = is_extendable + self.extension_ranges = extension_ranges + self.oneofs = oneofs if oneofs is not None else [] + self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) + for oneof in self.oneofs: + oneof.containing_type = self + self.syntax = syntax or "proto2" + + @property + def fields_by_camelcase_name(self): + """Same FieldDescriptor objects as in :attr:`fields`, but indexed by + :attr:`FieldDescriptor.camelcase_name`. + """ + if self._fields_by_camelcase_name is None: + self._fields_by_camelcase_name = dict( + (f.camelcase_name, f) for f in self.fields) + return self._fields_by_camelcase_name + + def EnumValueName(self, enum, value): + """Returns the string name of an enum value. + + This is just a small helper method to simplify a common operation. + + Args: + enum: string name of the Enum. + value: int, value of the enum. + + Returns: + string name of the enum value. + + Raises: + KeyError if either the Enum doesn't exist or the value is not a valid + value for the enum. + """ + return self.enum_types_by_name[enum].values_by_number[value].name + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.DescriptorProto. + + Args: + proto: An empty descriptor_pb2.DescriptorProto. + """ + # This function is overridden to give a better doc comment. + super(Descriptor, self).CopyToProto(proto) + + +# TODO(robinson): We should have aggressive checking here, +# for example: +# * If you specify a repeated field, you should not be allowed +# to specify a default value. +# * [Other examples here as needed]. +# +# TODO(robinson): for this and other *Descriptor classes, we +# might also want to lock things down aggressively (e.g., +# prevent clients from setting the attributes). Having +# stronger invariants here in general will reduce the number +# of runtime checks we must do in reflection.py... +class FieldDescriptor(DescriptorBase): + + """Descriptor for a single field in a .proto file. + + Attributes: + name (str): Name of this field, exactly as it appears in .proto. + full_name (str): Name of this field, including containing scope. This is + particularly relevant for extensions. + index (int): Dense, 0-indexed index giving the order that this + field textually appears within its message in the .proto file. + number (int): Tag number declared for this field in the .proto file. + + type (int): (One of the TYPE_* constants below) Declared type. + cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to + represent this field. + + label (int): (One of the LABEL_* constants below) Tells whether this + field is optional, required, or repeated. + has_default_value (bool): True if this field has a default value defined, + otherwise false. + default_value (Varies): Default value of this field. Only + meaningful for non-repeated scalar fields. Repeated fields + should always set this to [], and non-repeated composite + fields should always set this to None. + + containing_type (Descriptor): Descriptor of the protocol message + type that contains this field. Set by the Descriptor constructor + if we're passed into one. + Somewhat confusingly, for extension fields, this is the + descriptor of the EXTENDED message, not the descriptor + of the message containing this field. (See is_extension and + extension_scope below). + message_type (Descriptor): If a composite field, a descriptor + of the message type contained in this field. Otherwise, this is None. + enum_type (EnumDescriptor): If this field contains an enum, a + descriptor of that enum. Otherwise, this is None. + + is_extension: True iff this describes an extension field. + extension_scope (Descriptor): Only meaningful if is_extension is True. + Gives the message that immediately contains this extension field. + Will be None iff we're a top-level (file-level) extension field. + + options (descriptor_pb2.FieldOptions): Protocol message field options or + None to use default field options. + + containing_oneof (OneofDescriptor): If the field is a member of a oneof + union, contains its descriptor. Otherwise, None. + + file (FileDescriptor): Reference to file descriptor. + """ + + # Must be consistent with C++ FieldDescriptor::Type enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + TYPE_DOUBLE = 1 + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + MAX_TYPE = 18 + + # Must be consistent with C++ FieldDescriptor::CppType enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + CPPTYPE_INT32 = 1 + CPPTYPE_INT64 = 2 + CPPTYPE_UINT32 = 3 + CPPTYPE_UINT64 = 4 + CPPTYPE_DOUBLE = 5 + CPPTYPE_FLOAT = 6 + CPPTYPE_BOOL = 7 + CPPTYPE_ENUM = 8 + CPPTYPE_STRING = 9 + CPPTYPE_MESSAGE = 10 + MAX_CPPTYPE = 10 + + _PYTHON_TO_CPP_PROTO_TYPE_MAP = { + TYPE_DOUBLE: CPPTYPE_DOUBLE, + TYPE_FLOAT: CPPTYPE_FLOAT, + TYPE_ENUM: CPPTYPE_ENUM, + TYPE_INT64: CPPTYPE_INT64, + TYPE_SINT64: CPPTYPE_INT64, + TYPE_SFIXED64: CPPTYPE_INT64, + TYPE_UINT64: CPPTYPE_UINT64, + TYPE_FIXED64: CPPTYPE_UINT64, + TYPE_INT32: CPPTYPE_INT32, + TYPE_SFIXED32: CPPTYPE_INT32, + TYPE_SINT32: CPPTYPE_INT32, + TYPE_UINT32: CPPTYPE_UINT32, + TYPE_FIXED32: CPPTYPE_UINT32, + TYPE_BYTES: CPPTYPE_STRING, + TYPE_STRING: CPPTYPE_STRING, + TYPE_BOOL: CPPTYPE_BOOL, + TYPE_MESSAGE: CPPTYPE_MESSAGE, + TYPE_GROUP: CPPTYPE_MESSAGE + } + + # Must be consistent with C++ FieldDescriptor::Label enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + LABEL_OPTIONAL = 1 + LABEL_REQUIRED = 2 + LABEL_REPEATED = 3 + MAX_LABEL = 3 + + # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, + # and kLastReservedNumber in descriptor.h + MAX_FIELD_NUMBER = (1 << 29) - 1 + FIRST_RESERVED_FIELD_NUMBER = 19000 + LAST_RESERVED_FIELD_NUMBER = 19999 + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FieldDescriptor + + def __new__(cls, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + _message.Message._CheckCalledFromGeneratedFile() + if is_extension: + return _message.default_pool.FindExtensionByName(full_name) + else: + return _message.default_pool.FindFieldByName(full_name) + + def __init__(self, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + """The arguments are as described in the description of FieldDescriptor + attributes above. + + Note that containing_type may be None, and may be set later if necessary + (to deal with circular references between message types, for example). + Likewise for extension_scope. + """ + if create_key is not _internal_create_key: + _Deprecated('FieldDescriptor') + + super(FieldDescriptor, self).__init__( + options, serialized_options, 'FieldOptions') + self.name = name + self.full_name = full_name + self.file = file + self._camelcase_name = None + if json_name is None: + self.json_name = _ToJsonName(name) + else: + self.json_name = json_name + self.index = index + self.number = number + self.type = type + self.cpp_type = cpp_type + self.label = label + self.has_default_value = has_default_value + self.default_value = default_value + self.containing_type = containing_type + self.message_type = message_type + self.enum_type = enum_type + self.is_extension = is_extension + self.extension_scope = extension_scope + self.containing_oneof = containing_oneof + if api_implementation.Type() == 'cpp': + if is_extension: + self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) + else: + self._cdescriptor = _message.default_pool.FindFieldByName(full_name) + else: + self._cdescriptor = None + + @property + def camelcase_name(self): + """Camelcase name of this field. + + Returns: + str: the name in CamelCase. + """ + if self._camelcase_name is None: + self._camelcase_name = _ToCamelCase(self.name) + return self._camelcase_name + + @property + def has_presence(self): + """Whether the field distinguishes between unpopulated and default values. + + Raises: + RuntimeError: singular field that is not linked with message nor file. + """ + if self.label == FieldDescriptor.LABEL_REPEATED: + return False + if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or + self.containing_oneof): + return True + if hasattr(self.file, 'syntax'): + return self.file.syntax == 'proto2' + if hasattr(self.message_type, 'syntax'): + return self.message_type.syntax == 'proto2' + raise RuntimeError( + 'has_presence is not ready to use because field %s is not' + ' linked with message type nor file' % self.full_name) + + @staticmethod + def ProtoTypeToCppProtoType(proto_type): + """Converts from a Python proto type to a C++ Proto Type. + + The Python ProtocolBuffer classes specify both the 'Python' datatype and the + 'C++' datatype - and they're not the same. This helper method should + translate from one to another. + + Args: + proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) + Returns: + int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. + Raises: + TypeTransformationError: when the Python proto type isn't known. + """ + try: + return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] + except KeyError: + raise TypeTransformationError('Unknown proto_type: %s' % proto_type) + + +class EnumDescriptor(_NestedDescriptorBase): + + """Descriptor for an enum defined in a .proto file. + + Attributes: + name (str): Name of the enum type. + full_name (str): Full name of the type, including package name + and any enclosing type(s). + + values (list[EnumValueDescriptor]): List of the values + in this enum. + values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "name" field of each EnumValueDescriptor. + values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "number" field of each EnumValueDescriptor. + containing_type (Descriptor): Descriptor of the immediate containing + type of this enum, or None if this is an enum defined at the + top level in a .proto file. Set by Descriptor's constructor + if we're passed into one. + file (FileDescriptor): Reference to file descriptor. + options (descriptor_pb2.EnumOptions): Enum options message or + None to use default enum options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumDescriptor + + def __new__(cls, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindEnumTypeByName(full_name) + + def __init__(self, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + """Arguments are as described in the attribute description above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('EnumDescriptor') + + super(EnumDescriptor, self).__init__( + options, 'EnumOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + self.values = values + for value in self.values: + value.type = self + self.values_by_name = dict((v.name, v) for v in values) + # Values are reversed to ensure that the first alias is retained. + self.values_by_number = dict((v.number, v) for v in reversed(values)) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.EnumDescriptorProto. + + Args: + proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(EnumDescriptor, self).CopyToProto(proto) + + +class EnumValueDescriptor(DescriptorBase): + + """Descriptor for a single value within an enum. + + Attributes: + name (str): Name of this value. + index (int): Dense, 0-indexed index giving the order that this + value appears textually within its enum in the .proto file. + number (int): Actual number assigned to this enum value. + type (EnumDescriptor): :class:`EnumDescriptor` to which this value + belongs. Set by :class:`EnumDescriptor`'s constructor if we're + passed into one. + options (descriptor_pb2.EnumValueOptions): Enum value options message or + None to use default enum value options options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor + + def __new__(cls, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + # There is no way we can build a complete EnumValueDescriptor with the + # given parameters (the name of the Enum is not known, for example). + # Fortunately generated files just pass it to the EnumDescriptor() + # constructor, which will ignore it, so returning None is good enough. + return None + + def __init__(self, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('EnumValueDescriptor') + + super(EnumValueDescriptor, self).__init__( + options, serialized_options, 'EnumValueOptions') + self.name = name + self.index = index + self.number = number + self.type = type + + +class OneofDescriptor(DescriptorBase): + """Descriptor for a oneof field. + + Attributes: + name (str): Name of the oneof field. + full_name (str): Full name of the oneof field, including package name. + index (int): 0-based index giving the order of the oneof field inside + its containing type. + containing_type (Descriptor): :class:`Descriptor` of the protocol message + type that contains this field. Set by the :class:`Descriptor` constructor + if we're passed into one. + fields (list[FieldDescriptor]): The list of field descriptors this + oneof can contain. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.OneofDescriptor + + def __new__( + cls, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindOneofByName(full_name) + + def __init__( + self, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('OneofDescriptor') + + super(OneofDescriptor, self).__init__( + options, serialized_options, 'OneofOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_type = containing_type + self.fields = fields + + +class ServiceDescriptor(_NestedDescriptorBase): + + """Descriptor for a service. + + Attributes: + name (str): Name of the service. + full_name (str): Full name of the service, including package name. + index (int): 0-indexed index giving the order that this services + definition appears within the .proto file. + methods (list[MethodDescriptor]): List of methods provided by this + service. + methods_by_name (dict(str, MethodDescriptor)): Same + :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but + indexed by "name" attribute in each :class:`MethodDescriptor`. + options (descriptor_pb2.ServiceOptions): Service options message or + None to use default service options. + file (FileDescriptor): Reference to file info. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor + + def __new__( + cls, + name=None, + full_name=None, + index=None, + methods=None, + options=None, + serialized_options=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindServiceByName(full_name) + + def __init__(self, name, full_name, index, methods, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + if create_key is not _internal_create_key: + _Deprecated('ServiceDescriptor') + + super(ServiceDescriptor, self).__init__( + options, 'ServiceOptions', name, full_name, file, + None, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + self.index = index + self.methods = methods + self.methods_by_name = dict((m.name, m) for m in methods) + # Set the containing service for each method in this service. + for method in self.methods: + method.containing_service = self + + def FindMethodByName(self, name): + """Searches for the specified method, and returns its descriptor. + + Args: + name (str): Name of the method. + Returns: + MethodDescriptor or None: the descriptor for the requested method, if + found. + """ + return self.methods_by_name.get(name, None) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.ServiceDescriptorProto. + + Args: + proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(ServiceDescriptor, self).CopyToProto(proto) + + +class MethodDescriptor(DescriptorBase): + + """Descriptor for a method in a service. + + Attributes: + name (str): Name of the method within the service. + full_name (str): Full name of method. + index (int): 0-indexed index of the method inside the service. + containing_service (ServiceDescriptor): The service that contains this + method. + input_type (Descriptor): The descriptor of the message that this method + accepts. + output_type (Descriptor): The descriptor of the message that this method + returns. + client_streaming (bool): Whether this method uses client streaming. + server_streaming (bool): Whether this method uses server streaming. + options (descriptor_pb2.MethodOptions or None): Method options message, or + None to use default method options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.MethodDescriptor + + def __new__(cls, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindMethodByName(full_name) + + def __init__(self, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + """The arguments are as described in the description of MethodDescriptor + attributes above. + + Note that containing_service may be None, and may be set later if necessary. + """ + if create_key is not _internal_create_key: + _Deprecated('MethodDescriptor') + + super(MethodDescriptor, self).__init__( + options, serialized_options, 'MethodOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_service = containing_service + self.input_type = input_type + self.output_type = output_type + self.client_streaming = client_streaming + self.server_streaming = server_streaming + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.MethodDescriptorProto. + + Args: + proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. + + Raises: + Error: If self couldn't be serialized, due to too few constructor + arguments. + """ + if self.containing_service is not None: + from google.protobuf import descriptor_pb2 + service_proto = descriptor_pb2.ServiceDescriptorProto() + self.containing_service.CopyToProto(service_proto) + proto.CopyFrom(service_proto.method[self.index]) + else: + raise Error('Descriptor does not contain a service.') + + +class FileDescriptor(DescriptorBase): + """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. + + Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and + :attr:`dependencies` fields are only set by the + :py:mod:`google.protobuf.message_factory` module, and not by the generated + proto code. + + Attributes: + name (str): Name of file, relative to root of source tree. + package (str): Name of the package + syntax (str): string indicating syntax of the file (can be "proto2" or + "proto3") + serialized_pb (bytes): Byte string of serialized + :class:`descriptor_pb2.FileDescriptorProto`. + dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` + objects this :class:`FileDescriptor` depends on. + public_dependencies (list[FileDescriptor]): A subset of + :attr:`dependencies`, which were declared as "public". + message_types_by_name (dict(str, Descriptor)): Mapping from message names + to their :class:`Descriptor`. + enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to + their :class:`EnumDescriptor`. + extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension + names declared at file scope to their :class:`FieldDescriptor`. + services_by_name (dict(str, ServiceDescriptor)): Mapping from services' + names to their :class:`ServiceDescriptor`. + pool (DescriptorPool): The pool this descriptor belongs to. When not + passed to the constructor, the global default pool is used. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FileDescriptor + + def __new__(cls, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + # FileDescriptor() is called from various places, not only from generated + # files, to register dynamic proto files and messages. + # pylint: disable=g-explicit-bool-comparison + if serialized_pb == b'': + # Cpp generated code must be linked in if serialized_pb is '' + try: + return _message.default_pool.FindFileByName(name) + except KeyError: + raise RuntimeError('Please link in cpp generated lib for %s' % (name)) + elif serialized_pb: + return _message.default_pool.AddSerializedFile(serialized_pb) + else: + return super(FileDescriptor, cls).__new__(cls) + + def __init__(self, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + """Constructor.""" + if create_key is not _internal_create_key: + _Deprecated('FileDescriptor') + + super(FileDescriptor, self).__init__( + options, serialized_options, 'FileOptions') + + if pool is None: + from google.protobuf import descriptor_pool + pool = descriptor_pool.Default() + self.pool = pool + self.message_types_by_name = {} + self.name = name + self.package = package + self.syntax = syntax or "proto2" + self.serialized_pb = serialized_pb + + self.enum_types_by_name = {} + self.extensions_by_name = {} + self.services_by_name = {} + self.dependencies = (dependencies or []) + self.public_dependencies = (public_dependencies or []) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.FileDescriptorProto. + + Args: + proto: An empty descriptor_pb2.FileDescriptorProto. + """ + proto.ParseFromString(self.serialized_pb) + + +def _ParseOptions(message, string): + """Parses serialized options. + + This helper function is used to parse serialized options in generated + proto2 files. It must not be used outside proto2. + """ + message.ParseFromString(string) + return message + + +def _ToCamelCase(name): + """Converts name to camel-case and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + if result: + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + # Lower-case the first letter. + if result and result[0].isupper(): + result[0] = result[0].lower() + return ''.join(result) + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _ToJsonName(name): + """Converts name to Json name and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + return ''.join(result) + + +def MakeDescriptor(desc_proto, package='', build_file_if_cpp=True, + syntax=None): + """Make a protobuf Descriptor given a DescriptorProto protobuf. + + Handles nested descriptors. Note that this is limited to the scope of defining + a message inside of another message. Composite fields can currently only be + resolved if the message is defined in the same scope as the field. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: Optional package name for the new message Descriptor (string). + build_file_if_cpp: Update the C++ descriptor pool if api matches. + Set to False on recursion, so no duplicates are created. + syntax: The syntax/semantics that should be used. Set to "proto3" to get + proto3 field presence semantics. + Returns: + A Descriptor for protobuf messages. + """ + if api_implementation.Type() == 'cpp' and build_file_if_cpp: + # The C++ implementation requires all descriptors to be backed by the same + # definition in the C++ descriptor pool. To do this, we build a + # FileDescriptorProto with the same definition as this descriptor and build + # it into the pool. + from google.protobuf import descriptor_pb2 + file_descriptor_proto = descriptor_pb2.FileDescriptorProto() + file_descriptor_proto.message_type.add().MergeFrom(desc_proto) + + # Generate a random name for this proto file to prevent conflicts with any + # imported ones. We need to specify a file name so the descriptor pool + # accepts our FileDescriptorProto, but it is not important what that file + # name is actually set to. + proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') + + if package: + file_descriptor_proto.name = os.path.join(package.replace('.', '/'), + proto_name + '.proto') + file_descriptor_proto.package = package + else: + file_descriptor_proto.name = proto_name + '.proto' + + _message.default_pool.Add(file_descriptor_proto) + result = _message.default_pool.FindFileByName(file_descriptor_proto.name) + + if _USE_C_DESCRIPTORS: + return result.message_types_by_name[desc_proto.name] + + full_message_name = [desc_proto.name] + if package: full_message_name.insert(0, package) + + # Create Descriptors for enum types + enum_types = {} + for enum_proto in desc_proto.enum_type: + full_name = '.'.join(full_message_name + [enum_proto.name]) + enum_desc = EnumDescriptor( + enum_proto.name, full_name, None, [ + EnumValueDescriptor(enum_val.name, ii, enum_val.number, + create_key=_internal_create_key) + for ii, enum_val in enumerate(enum_proto.value)], + create_key=_internal_create_key) + enum_types[full_name] = enum_desc + + # Create Descriptors for nested types + nested_types = {} + for nested_proto in desc_proto.nested_type: + full_name = '.'.join(full_message_name + [nested_proto.name]) + # Nested types are just those defined inside of the message, not all types + # used by fields in the message, so no loops are possible here. + nested_desc = MakeDescriptor(nested_proto, + package='.'.join(full_message_name), + build_file_if_cpp=False, + syntax=syntax) + nested_types[full_name] = nested_desc + + fields = [] + for field_proto in desc_proto.field: + full_name = '.'.join(full_message_name + [field_proto.name]) + enum_desc = None + nested_desc = None + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + if field_proto.HasField('type_name'): + type_name = field_proto.type_name + full_type_name = '.'.join(full_message_name + + [type_name[type_name.rfind('.')+1:]]) + if full_type_name in nested_types: + nested_desc = nested_types[full_type_name] + elif full_type_name in enum_types: + enum_desc = enum_types[full_type_name] + # Else type_name references a non-local type, which isn't implemented + field = FieldDescriptor( + field_proto.name, full_name, field_proto.number - 1, + field_proto.number, field_proto.type, + FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), + field_proto.label, None, nested_desc, enum_desc, None, False, None, + options=_OptionsOrNone(field_proto), has_default_value=False, + json_name=json_name, create_key=_internal_create_key) + fields.append(field) + + desc_name = '.'.join(full_message_name) + return Descriptor(desc_proto.name, desc_name, None, None, fields, + list(nested_types.values()), list(enum_types.values()), [], + options=_OptionsOrNone(desc_proto), + create_key=_internal_create_key) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/descriptor_database.py b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_database.py new file mode 100644 index 0000000000..073eddc711 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_database.py @@ -0,0 +1,177 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a container for DescriptorProtos.""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import warnings + + +class Error(Exception): + pass + + +class DescriptorDatabaseConflictingDefinitionError(Error): + """Raised when a proto is added with the same name & different descriptor.""" + + +class DescriptorDatabase(object): + """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" + + def __init__(self): + self._file_desc_protos_by_file = {} + self._file_desc_protos_by_symbol = {} + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this database. + + Args: + file_desc_proto: The FileDescriptorProto to add. + Raises: + DescriptorDatabaseConflictingDefinitionError: if an attempt is made to + add a proto with the same name but different definition than an + existing proto in the database. + """ + proto_name = file_desc_proto.name + if proto_name not in self._file_desc_protos_by_file: + self._file_desc_protos_by_file[proto_name] = file_desc_proto + elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: + raise DescriptorDatabaseConflictingDefinitionError( + '%s already added, but with different descriptor.' % proto_name) + else: + return + + # Add all the top-level descriptors to the index. + package = file_desc_proto.package + for message in file_desc_proto.message_type: + for name in _ExtractSymbols(message, package): + self._AddSymbol(name, file_desc_proto) + for enum in file_desc_proto.enum_type: + self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) + for enum_value in enum.value: + self._file_desc_protos_by_symbol[ + '.'.join((package, enum_value.name))] = file_desc_proto + for extension in file_desc_proto.extension: + self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) + for service in file_desc_proto.service: + self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) + + def FindFileByName(self, name): + """Finds the file descriptor proto by file name. + + Typically the file name is a relative path ending to a .proto file. The + proto with the given name will have to have been added to this database + using the Add method or else an error will be raised. + + Args: + name: The file name to find. + + Returns: + The file descriptor proto matching the name. + + Raises: + KeyError if no file by the given name was added. + """ + + return self._file_desc_protos_by_file[name] + + def FindFileContainingSymbol(self, symbol): + """Finds the file descriptor proto containing the specified symbol. + + The symbol should be a fully qualified name including the file descriptor's + package and any containing messages. Some examples: + + 'some.package.name.Message' + 'some.package.name.Message.NestedEnum' + 'some.package.name.Message.some_field' + + The file descriptor proto containing the specified symbol must be added to + this database using the Add method or else an error will be raised. + + Args: + symbol: The fully qualified symbol name. + + Returns: + The file descriptor proto containing the symbol. + + Raises: + KeyError if no file contains the specified symbol. + """ + try: + return self._file_desc_protos_by_symbol[symbol] + except KeyError: + # Fields, enum values, and nested extensions are not in + # _file_desc_protos_by_symbol. Try to find the top level + # descriptor. Non-existent nested symbol under a valid top level + # descriptor can also be found. The behavior is the same with + # protobuf C++. + top_level, _, _ = symbol.rpartition('.') + try: + return self._file_desc_protos_by_symbol[top_level] + except KeyError: + # Raise the original symbol as a KeyError for better diagnostics. + raise KeyError(symbol) + + def FindFileContainingExtension(self, extendee_name, extension_number): + # TODO(jieluo): implement this API. + return None + + def FindAllExtensionNumbers(self, extendee_name): + # TODO(jieluo): implement this API. + return [] + + def _AddSymbol(self, name, file_desc_proto): + if name in self._file_desc_protos_by_symbol: + warn_msg = ('Conflict register for file "' + file_desc_proto.name + + '": ' + name + + ' is already defined in file "' + + self._file_desc_protos_by_symbol[name].name + '"') + warnings.warn(warn_msg, RuntimeWarning) + self._file_desc_protos_by_symbol[name] = file_desc_proto + + +def _ExtractSymbols(desc_proto, package): + """Pulls out all the symbols from a descriptor proto. + + Args: + desc_proto: The proto to extract symbols from. + package: The package containing the descriptor type. + + Yields: + The fully qualified name found in the descriptor. + """ + message_name = package + '.' + desc_proto.name if package else desc_proto.name + yield message_name + for nested_type in desc_proto.nested_type: + for symbol in _ExtractSymbols(nested_type, message_name): + yield symbol + for enum_type in desc_proto.enum_type: + yield '.'.join((message_name, enum_type.name)) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pb2.py new file mode 100644 index 0000000000..f570386432 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pb2.py @@ -0,0 +1,1925 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/descriptor.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR = _descriptor.FileDescriptor( + name='google/protobuf/descriptor.proto', + package='google.protobuf', + syntax='proto2', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' + ) +else: + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') + +if _descriptor._USE_C_DESCRIPTORS == False: + _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='google.protobuf.FieldDescriptorProto.Type', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='TYPE_DOUBLE', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FLOAT', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT64', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT64', index=3, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT32', index=4, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED64', index=5, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED32', index=6, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BOOL', index=7, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_STRING', index=8, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_GROUP', index=9, number=10, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_MESSAGE', index=10, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BYTES', index=11, number=12, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT32', index=12, number=13, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_ENUM', index=13, number=14, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED32', index=14, number=15, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED64', index=15, number=16, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT32', index=16, number=17, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT64', index=17, number=18, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) + + _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( + name='Label', + full_name='google.protobuf.FieldDescriptorProto.Label', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='LABEL_OPTIONAL', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REQUIRED', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REPEATED', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) + + _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( + name='OptimizeMode', + full_name='google.protobuf.FileOptions.OptimizeMode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPEED', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CODE_SIZE', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LITE_RUNTIME', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) + + _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( + name='CType', + full_name='google.protobuf.FieldOptions.CType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CORD', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STRING_PIECE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) + + _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( + name='JSType', + full_name='google.protobuf.FieldOptions.JSType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='JS_NORMAL', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_STRING', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_NUMBER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) + + _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( + name='IdempotencyLevel', + full_name='google.protobuf.MethodOptions.IdempotencyLevel', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDEMPOTENCY_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NO_SIDE_EFFECTS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IDEMPOTENT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) + + + _FILEDESCRIPTORSET = _descriptor.Descriptor( + name='FileDescriptorSet', + full_name='google.protobuf.FileDescriptorSet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEDESCRIPTORPROTO = _descriptor.Descriptor( + name='FileDescriptorProto', + full_name='google.protobuf.FileDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, + number=10, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, + number=11, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( + name='ExtensionRange', + full_name='google.protobuf.DescriptorProto.ExtensionRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( + name='ReservedRange', + full_name='google.protobuf.DescriptorProto.ReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO = _descriptor.Descriptor( + name='DescriptorProto', + full_name='google.protobuf.DescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.DescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field', full_name='google.protobuf.DescriptorProto.field', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.options', index=7, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( + name='ExtensionRangeOptions', + full_name='google.protobuf.ExtensionRangeOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( + name='FieldDescriptorProto', + full_name='google.protobuf.FieldDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, + number=17, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDDESCRIPTORPROTO_TYPE, + _FIELDDESCRIPTORPROTO_LABEL, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( + name='OneofDescriptorProto', + full_name='google.protobuf.OneofDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( + name='EnumReservedRange', + full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumDescriptorProto', + full_name='google.protobuf.EnumDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, + number=5, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumValueDescriptorProto', + full_name='google.protobuf.EnumValueDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( + name='ServiceDescriptorProto', + full_name='google.protobuf.ServiceDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _METHODDESCRIPTORPROTO = _descriptor.Descriptor( + name='MethodDescriptorProto', + full_name='google.protobuf.MethodDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEOPTIONS = _descriptor.Descriptor( + name='FileOptions', + full_name='google.protobuf.FileOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, + number=20, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, + number=27, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, + number=9, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, + number=16, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, + number=17, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, + number=18, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10, + number=42, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11, + number=23, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12, + number=31, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13, + number=36, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14, + number=37, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15, + number=39, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16, + number=40, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17, + number=41, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18, + number=44, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19, + number=45, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FILEOPTIONS_OPTIMIZEMODE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _MESSAGEOPTIONS = _descriptor.Descriptor( + name='MessageOptions', + full_name='google.protobuf.MessageOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDOPTIONS = _descriptor.Descriptor( + name='FieldOptions', + full_name='google.protobuf.FieldOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, + number=6, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, + number=15, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDOPTIONS_CTYPE, + _FIELDOPTIONS_JSTYPE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ONEOFOPTIONS = _descriptor.Descriptor( + name='OneofOptions', + full_name='google.protobuf.OneofOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMOPTIONS = _descriptor.Descriptor( + name='EnumOptions', + full_name='google.protobuf.EnumOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMVALUEOPTIONS = _descriptor.Descriptor( + name='EnumValueOptions', + full_name='google.protobuf.EnumValueOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _SERVICEOPTIONS = _descriptor.Descriptor( + name='ServiceOptions', + full_name='google.protobuf.ServiceOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _METHODOPTIONS = _descriptor.Descriptor( + name='MethodOptions', + full_name='google.protobuf.MethodOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, + number=34, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _METHODOPTIONS_IDEMPOTENCYLEVEL, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( + name='NamePart', + full_name='google.protobuf.UninterpretedOption.NamePart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, + number=2, type=8, cpp_type=7, label=2, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _UNINTERPRETEDOPTION = _descriptor.Descriptor( + name='UninterpretedOption', + full_name='google.protobuf.UninterpretedOption', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='google.protobuf.SourceCodeInfo.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _SOURCECODEINFO = _descriptor.Descriptor( + name='SourceCodeInfo', + full_name='google.protobuf.SourceCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SOURCECODEINFO_LOCATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( + name='Annotation', + full_name='google.protobuf.GeneratedCodeInfo.Annotation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _GENERATEDCODEINFO = _descriptor.Descriptor( + name='GeneratedCodeInfo', + full_name='google.protobuf.GeneratedCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS + _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO + _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS + _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE + _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS + _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE + _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL + _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE + _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS + _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO + _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO + _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS + _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE + _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS + _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO + _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS + _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS + _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE + _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS + _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE + _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE + _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS + _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL + _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS + _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION + _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART + _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO + _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION + _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO + _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION + DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET + DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS + DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS + DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS + DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS + DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS + DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS + DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS + DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION + DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO + DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO + _sym_db.RegisterFileDescriptor(DESCRIPTOR) + +else: + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _FILEDESCRIPTORSET._serialized_start=53 + _FILEDESCRIPTORSET._serialized_end=124 + _FILEDESCRIPTORPROTO._serialized_start=127 + _FILEDESCRIPTORPROTO._serialized_end=602 + _DESCRIPTORPROTO._serialized_start=605 + _DESCRIPTORPROTO._serialized_end=1286 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286 + _EXTENSIONRANGEOPTIONS._serialized_start=1288 + _EXTENSIONRANGEOPTIONS._serialized_end=1391 + _FIELDDESCRIPTORPROTO._serialized_start=1394 + _FIELDDESCRIPTORPROTO._serialized_end=2119 + _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740 + _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050 + _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052 + _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119 + _ONEOFDESCRIPTORPROTO._serialized_start=2121 + _ONEOFDESCRIPTORPROTO._serialized_end=2205 + _ENUMDESCRIPTORPROTO._serialized_start=2208 + _ENUMDESCRIPTORPROTO._serialized_end=2500 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500 + _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502 + _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610 + _SERVICEDESCRIPTORPROTO._serialized_start=2613 + _SERVICEDESCRIPTORPROTO._serialized_end=2757 + _METHODDESCRIPTORPROTO._serialized_start=2760 + _METHODDESCRIPTORPROTO._serialized_end=2953 + _FILEOPTIONS._serialized_start=2956 + _FILEOPTIONS._serialized_end=3761 + _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686 + _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744 + _MESSAGEOPTIONS._serialized_start=3764 + _MESSAGEOPTIONS._serialized_end=4024 + _FIELDOPTIONS._serialized_start=4027 + _FIELDOPTIONS._serialized_end=4473 + _FIELDOPTIONS_CTYPE._serialized_start=4354 + _FIELDOPTIONS_CTYPE._serialized_end=4401 + _FIELDOPTIONS_JSTYPE._serialized_start=4403 + _FIELDOPTIONS_JSTYPE._serialized_end=4456 + _ONEOFOPTIONS._serialized_start=4475 + _ONEOFOPTIONS._serialized_end=4569 + _ENUMOPTIONS._serialized_start=4572 + _ENUMOPTIONS._serialized_end=4719 + _ENUMVALUEOPTIONS._serialized_start=4721 + _ENUMVALUEOPTIONS._serialized_end=4846 + _SERVICEOPTIONS._serialized_start=4848 + _SERVICEOPTIONS._serialized_end=4971 + _METHODOPTIONS._serialized_start=4974 + _METHODOPTIONS._serialized_end=5275 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264 + _UNINTERPRETEDOPTION._serialized_start=5278 + _UNINTERPRETEDOPTION._serialized_end=5564 + _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513 + _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564 + _SOURCECODEINFO._serialized_start=5567 + _SOURCECODEINFO._serialized_end=5780 + _SOURCECODEINFO_LOCATION._serialized_start=5646 + _SOURCECODEINFO_LOCATION._serialized_end=5780 + _GENERATEDCODEINFO._serialized_start=5783 + _GENERATEDCODEINFO._serialized_end=5950 + _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871 + _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pool.py b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pool.py new file mode 100644 index 0000000000..911372a8b0 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/descriptor_pool.py @@ -0,0 +1,1295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides DescriptorPool to use as a container for proto2 descriptors. + +The DescriptorPool is used in conjection with a DescriptorDatabase to maintain +a collection of protocol buffer descriptors for use when dynamically creating +message types at runtime. + +For most applications protocol buffers should be used via modules generated by +the protocol buffer compiler tool. This should only be used when the type of +protocol buffers used in an application or library cannot be predetermined. + +Below is a straightforward example on how to use this class:: + + pool = DescriptorPool() + file_descriptor_protos = [ ... ] + for file_descriptor_proto in file_descriptor_protos: + pool.Add(file_descriptor_proto) + my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') + +The message descriptor can be used in conjunction with the message_factory +module in order to create a protocol buffer class that can be encoded and +decoded. + +If you want to get a Python class for the specified proto, use the +helper functions inside google.protobuf.message_factory +directly instead of this class. +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import collections +import warnings + +from google.protobuf import descriptor +from google.protobuf import descriptor_database +from google.protobuf import text_encoding + + +_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access + + +def _Deprecated(func): + """Mark functions as deprecated.""" + + def NewFunc(*args, **kwargs): + warnings.warn( + 'Call to deprecated function %s(). Note: Do add unlinked descriptors ' + 'to descriptor_pool is wrong. Use Add() or AddSerializedFile() ' + 'instead.' % func.__name__, + category=DeprecationWarning) + return func(*args, **kwargs) + NewFunc.__name__ = func.__name__ + NewFunc.__doc__ = func.__doc__ + NewFunc.__dict__.update(func.__dict__) + return NewFunc + + +def _NormalizeFullyQualifiedName(name): + """Remove leading period from fully-qualified type name. + + Due to b/13860351 in descriptor_database.py, types in the root namespace are + generated with a leading period. This function removes that prefix. + + Args: + name (str): The fully-qualified symbol name. + + Returns: + str: The normalized fully-qualified symbol name. + """ + return name.lstrip('.') + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) + + +class DescriptorPool(object): + """A collection of protobufs dynamically constructed by descriptor protos.""" + + if _USE_C_DESCRIPTORS: + + def __new__(cls, descriptor_db=None): + # pylint: disable=protected-access + return descriptor._message.DescriptorPool(descriptor_db) + + def __init__(self, descriptor_db=None): + """Initializes a Pool of proto buffs. + + The descriptor_db argument to the constructor is provided to allow + specialized file descriptor proto lookup code to be triggered on demand. An + example would be an implementation which will read and compile a file + specified in a call to FindFileByName() and not require the call to Add() + at all. Results from this database will be cached internally here as well. + + Args: + descriptor_db: A secondary source of file descriptors. + """ + + self._internal_db = descriptor_database.DescriptorDatabase() + self._descriptor_db = descriptor_db + self._descriptors = {} + self._enum_descriptors = {} + self._service_descriptors = {} + self._file_descriptors = {} + self._toplevel_extensions = {} + # TODO(jieluo): Remove _file_desc_by_toplevel_extension after + # maybe year 2020 for compatibility issue (with 3.4.1 only). + self._file_desc_by_toplevel_extension = {} + self._top_enum_values = {} + # We store extensions in two two-level mappings: The first key is the + # descriptor of the message being extended, the second key is the extension + # full name or its tag number. + self._extensions_by_name = collections.defaultdict(dict) + self._extensions_by_number = collections.defaultdict(dict) + + def _CheckConflictRegister(self, desc, desc_name, file_name): + """Check if the descriptor name conflicts with another of the same name. + + Args: + desc: Descriptor of a message, enum, service, extension or enum value. + desc_name (str): the full name of desc. + file_name (str): The file name of descriptor. + """ + for register, descriptor_type in [ + (self._descriptors, descriptor.Descriptor), + (self._enum_descriptors, descriptor.EnumDescriptor), + (self._service_descriptors, descriptor.ServiceDescriptor), + (self._toplevel_extensions, descriptor.FieldDescriptor), + (self._top_enum_values, descriptor.EnumValueDescriptor)]: + if desc_name in register: + old_desc = register[desc_name] + if isinstance(old_desc, descriptor.EnumValueDescriptor): + old_file = old_desc.type.file.name + else: + old_file = old_desc.file.name + + if not isinstance(desc, descriptor_type) or ( + old_file != file_name): + error_msg = ('Conflict register for file "' + file_name + + '": ' + desc_name + + ' is already defined in file "' + + old_file + '". Please fix the conflict by adding ' + 'package name on the proto file, or use different ' + 'name for the duplication.') + if isinstance(desc, descriptor.EnumValueDescriptor): + error_msg += ('\nNote: enum values appear as ' + 'siblings of the enum type instead of ' + 'children of it.') + + raise TypeError(error_msg) + + return + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + file_desc_proto (FileDescriptorProto): The file descriptor to add. + """ + + self._internal_db.Add(file_desc_proto) + + def AddSerializedFile(self, serialized_file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + serialized_file_desc_proto (bytes): A bytes string, serialization of the + :class:`FileDescriptorProto` to add. + + Returns: + FileDescriptor: Descriptor for the added file. + """ + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( + serialized_file_desc_proto) + file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) + file_desc.serialized_pb = serialized_file_desc_proto + return file_desc + + # Add Descriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddDescriptor(self, desc): + self._AddDescriptor(desc) + + # Never call this method. It is for internal usage only. + def _AddDescriptor(self, desc): + """Adds a Descriptor to the pool, non-recursively. + + If the Descriptor contains nested messages or enums, the caller must + explicitly register them. This method also registers the FileDescriptor + associated with the message. + + Args: + desc: A Descriptor. + """ + if not isinstance(desc, descriptor.Descriptor): + raise TypeError('Expected instance of descriptor.Descriptor.') + + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + + self._descriptors[desc.full_name] = desc + self._AddFileDescriptor(desc.file) + + # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddEnumDescriptor(self, enum_desc): + self._AddEnumDescriptor(enum_desc) + + # Never call this method. It is for internal usage only. + def _AddEnumDescriptor(self, enum_desc): + """Adds an EnumDescriptor to the pool. + + This method also registers the FileDescriptor associated with the enum. + + Args: + enum_desc: An EnumDescriptor. + """ + + if not isinstance(enum_desc, descriptor.EnumDescriptor): + raise TypeError('Expected instance of descriptor.EnumDescriptor.') + + file_name = enum_desc.file.name + self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) + self._enum_descriptors[enum_desc.full_name] = enum_desc + + # Top enum values need to be indexed. + # Count the number of dots to see whether the enum is toplevel or nested + # in a message. We cannot use enum_desc.containing_type at this stage. + if enum_desc.file.package: + top_level = (enum_desc.full_name.count('.') + - enum_desc.file.package.count('.') == 1) + else: + top_level = enum_desc.full_name.count('.') == 0 + if top_level: + file_name = enum_desc.file.name + package = enum_desc.file.package + for enum_value in enum_desc.values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, enum_value.name))) + self._CheckConflictRegister(enum_value, full_name, file_name) + self._top_enum_values[full_name] = enum_value + self._AddFileDescriptor(enum_desc.file) + + # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddServiceDescriptor(self, service_desc): + self._AddServiceDescriptor(service_desc) + + # Never call this method. It is for internal usage only. + def _AddServiceDescriptor(self, service_desc): + """Adds a ServiceDescriptor to the pool. + + Args: + service_desc: A ServiceDescriptor. + """ + + if not isinstance(service_desc, descriptor.ServiceDescriptor): + raise TypeError('Expected instance of descriptor.ServiceDescriptor.') + + self._CheckConflictRegister(service_desc, service_desc.full_name, + service_desc.file.name) + self._service_descriptors[service_desc.full_name] = service_desc + + # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddExtensionDescriptor(self, extension): + self._AddExtensionDescriptor(extension) + + # Never call this method. It is for internal usage only. + def _AddExtensionDescriptor(self, extension): + """Adds a FieldDescriptor describing an extension to the pool. + + Args: + extension: A FieldDescriptor. + + Raises: + AssertionError: when another extension with the same number extends the + same message. + TypeError: when the specified extension is not a + descriptor.FieldDescriptor. + """ + if not (isinstance(extension, descriptor.FieldDescriptor) and + extension.is_extension): + raise TypeError('Expected an extension descriptor.') + + if extension.extension_scope is None: + self._toplevel_extensions[extension.full_name] = extension + + try: + existing_desc = self._extensions_by_number[ + extension.containing_type][extension.number] + except KeyError: + pass + else: + if extension is not existing_desc: + raise AssertionError( + 'Extensions "%s" and "%s" both try to extend message type "%s" ' + 'with field number %d.' % + (extension.full_name, existing_desc.full_name, + extension.containing_type.full_name, extension.number)) + + self._extensions_by_number[extension.containing_type][ + extension.number] = extension + self._extensions_by_name[extension.containing_type][ + extension.full_name] = extension + + # Also register MessageSet extensions with the type name. + if _IsMessageSetExtension(extension): + self._extensions_by_name[extension.containing_type][ + extension.message_type.full_name] = extension + + @_Deprecated + def AddFileDescriptor(self, file_desc): + self._InternalAddFileDescriptor(file_desc) + + # Never call this method. It is for internal usage only. + def _InternalAddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + self._AddFileDescriptor(file_desc) + # TODO(jieluo): This is a temporary solution for FieldDescriptor.file. + # FieldDescriptor.file is added in code gen. Remove this solution after + # maybe 2020 for compatibility reason (with 3.4.1 only). + for extension in file_desc.extensions_by_name.values(): + self._file_desc_by_toplevel_extension[ + extension.full_name] = file_desc + + def _AddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + if not isinstance(file_desc, descriptor.FileDescriptor): + raise TypeError('Expected instance of descriptor.FileDescriptor.') + self._file_descriptors[file_desc.name] = file_desc + + def FindFileByName(self, file_name): + """Gets a FileDescriptor by file name. + + Args: + file_name (str): The path to the file to get a descriptor for. + + Returns: + FileDescriptor: The descriptor for the named file. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + try: + return self._file_descriptors[file_name] + except KeyError: + pass + + try: + file_proto = self._internal_db.FindFileByName(file_name) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileByName(file_name) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file named %s' % file_name) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def FindFileContainingSymbol(self, symbol): + """Gets the FileDescriptor for the file containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + symbol = _NormalizeFullyQualifiedName(symbol) + try: + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + pass + + try: + # Try fallback database. Build and find again if possible. + self._FindFileContainingSymbolInDb(symbol) + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + raise KeyError('Cannot find a file containing %s' % symbol) + + def _InternalFindFileContainingSymbol(self, symbol): + """Gets the already built FileDescriptor containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + try: + return self._descriptors[symbol].file + except KeyError: + pass + + try: + return self._enum_descriptors[symbol].file + except KeyError: + pass + + try: + return self._service_descriptors[symbol].file + except KeyError: + pass + + try: + return self._top_enum_values[symbol].type.file + except KeyError: + pass + + try: + return self._file_desc_by_toplevel_extension[symbol] + except KeyError: + pass + + # Try fields, enum values and nested extensions inside a message. + top_name, _, sub_name = symbol.rpartition('.') + try: + message = self.FindMessageTypeByName(top_name) + assert (sub_name in message.extensions_by_name or + sub_name in message.fields_by_name or + sub_name in message.enum_values_by_name) + return message.file + except (KeyError, AssertionError): + raise KeyError('Cannot find a file containing %s' % symbol) + + def FindMessageTypeByName(self, full_name): + """Loads the named descriptor from the pool. + + Args: + full_name (str): The full name of the descriptor to load. + + Returns: + Descriptor: The descriptor for the named type. + + Raises: + KeyError: if the message cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._descriptors[full_name] + + def FindEnumTypeByName(self, full_name): + """Loads the named enum descriptor from the pool. + + Args: + full_name (str): The full name of the enum descriptor to load. + + Returns: + EnumDescriptor: The enum descriptor for the named type. + + Raises: + KeyError: if the enum cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._enum_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._enum_descriptors[full_name] + + def FindFieldByName(self, full_name): + """Loads the named field descriptor from the pool. + + Args: + full_name (str): The full name of the field descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named field. + + Raises: + KeyError: if the field cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, field_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.fields_by_name[field_name] + + def FindOneofByName(self, full_name): + """Loads the named oneof descriptor from the pool. + + Args: + full_name (str): The full name of the oneof descriptor to load. + + Returns: + OneofDescriptor: The oneof descriptor for the named oneof. + + Raises: + KeyError: if the oneof cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, oneof_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.oneofs_by_name[oneof_name] + + def FindExtensionByName(self, full_name): + """Loads the named extension descriptor from the pool. + + Args: + full_name (str): The full name of the extension descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named extension. + + Raises: + KeyError: if the extension cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + try: + # The proto compiler does not give any link between the FileDescriptor + # and top-level extensions unless the FileDescriptorProto is added to + # the DescriptorDatabase, but this can impact memory usage. + # So we registered these extensions by name explicitly. + return self._toplevel_extensions[full_name] + except KeyError: + pass + message_name, _, extension_name = full_name.rpartition('.') + try: + # Most extensions are nested inside a message. + scope = self.FindMessageTypeByName(message_name) + except KeyError: + # Some extensions are defined at file scope. + scope = self._FindFileContainingSymbolInDb(full_name) + return scope.extensions_by_name[extension_name] + + def FindExtensionByNumber(self, message_descriptor, number): + """Gets the extension of the specified message with the specified number. + + Extensions have to be registered to this pool by calling :func:`Add` or + :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): descriptor of the extended message. + number (int): Number of the extension field. + + Returns: + FieldDescriptor: The descriptor for the extension. + + Raises: + KeyError: when no extension with the given number is known for the + specified message. + """ + try: + return self._extensions_by_number[message_descriptor][number] + except KeyError: + self._TryLoadExtensionFromDB(message_descriptor, number) + return self._extensions_by_number[message_descriptor][number] + + def FindAllExtensions(self, message_descriptor): + """Gets all the known extensions of a given message. + + Extensions have to be registered to this pool by build related + :func:`Add` or :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): Descriptor of the extended message. + + Returns: + list[FieldDescriptor]: Field descriptors describing the extensions. + """ + # Fallback to descriptor db if FindAllExtensionNumbers is provided. + if self._descriptor_db and hasattr( + self._descriptor_db, 'FindAllExtensionNumbers'): + full_name = message_descriptor.full_name + all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) + for number in all_numbers: + if number in self._extensions_by_number[message_descriptor]: + continue + self._TryLoadExtensionFromDB(message_descriptor, number) + + return list(self._extensions_by_number[message_descriptor].values()) + + def _TryLoadExtensionFromDB(self, message_descriptor, number): + """Try to Load extensions from descriptor db. + + Args: + message_descriptor: descriptor of the extended message. + number: the extension number that needs to be loaded. + """ + if not self._descriptor_db: + return + # Only supported when FindFileContainingExtension is provided. + if not hasattr( + self._descriptor_db, 'FindFileContainingExtension'): + return + + full_name = message_descriptor.full_name + file_proto = self._descriptor_db.FindFileContainingExtension( + full_name, number) + + if file_proto is None: + return + + try: + self._ConvertFileProtoToFileDescriptor(file_proto) + except: + warn_msg = ('Unable to load proto file %s for extension number %d.' % + (file_proto.name, number)) + warnings.warn(warn_msg, RuntimeWarning) + + def FindServiceByName(self, full_name): + """Loads the named service descriptor from the pool. + + Args: + full_name (str): The full name of the service descriptor to load. + + Returns: + ServiceDescriptor: The service descriptor for the named service. + + Raises: + KeyError: if the service cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._service_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._service_descriptors[full_name] + + def FindMethodByName(self, full_name): + """Loads the named service method descriptor from the pool. + + Args: + full_name (str): The full name of the method descriptor to load. + + Returns: + MethodDescriptor: The method descriptor for the service method. + + Raises: + KeyError: if the method cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + service_name, _, method_name = full_name.rpartition('.') + service_descriptor = self.FindServiceByName(service_name) + return service_descriptor.methods_by_name[method_name] + + def _FindFileContainingSymbolInDb(self, symbol): + """Finds the file in descriptor DB containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: The file that contains the specified symbol. + + Raises: + KeyError: if the file cannot be found in the descriptor database. + """ + try: + file_proto = self._internal_db.FindFileContainingSymbol(symbol) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file containing %s' % symbol) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def _ConvertFileProtoToFileDescriptor(self, file_proto): + """Creates a FileDescriptor from a proto or returns a cached copy. + + This method also has the side effect of loading all the symbols found in + the file into the appropriate dictionaries in the pool. + + Args: + file_proto: The proto to convert. + + Returns: + A FileDescriptor matching the passed in proto. + """ + if file_proto.name not in self._file_descriptors: + built_deps = list(self._GetDeps(file_proto.dependency)) + direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] + public_deps = [direct_deps[i] for i in file_proto.public_dependency] + + file_descriptor = descriptor.FileDescriptor( + pool=self, + name=file_proto.name, + package=file_proto.package, + syntax=file_proto.syntax, + options=_OptionsOrNone(file_proto), + serialized_pb=file_proto.SerializeToString(), + dependencies=direct_deps, + public_dependencies=public_deps, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope = {} + + # This loop extracts all the message and enum types from all the + # dependencies of the file_proto. This is necessary to create the + # scope of available message types when defining the passed in + # file proto. + for dependency in built_deps: + scope.update(self._ExtractSymbols( + dependency.message_types_by_name.values())) + scope.update((_PrefixWithDot(enum.full_name), enum) + for enum in dependency.enum_types_by_name.values()) + + for message_type in file_proto.message_type: + message_desc = self._ConvertMessageDescriptor( + message_type, file_proto.package, file_descriptor, scope, + file_proto.syntax) + file_descriptor.message_types_by_name[message_desc.name] = ( + message_desc) + + for enum_type in file_proto.enum_type: + file_descriptor.enum_types_by_name[enum_type.name] = ( + self._ConvertEnumDescriptor(enum_type, file_proto.package, + file_descriptor, None, scope, True)) + + for index, extension_proto in enumerate(file_proto.extension): + extension_desc = self._MakeFieldDescriptor( + extension_proto, file_proto.package, index, file_descriptor, + is_extension=True) + extension_desc.containing_type = self._GetTypeFromScope( + file_descriptor.package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, + file_descriptor.package, scope) + file_descriptor.extensions_by_name[extension_desc.name] = ( + extension_desc) + self._file_desc_by_toplevel_extension[extension_desc.full_name] = ( + file_descriptor) + + for desc_proto in file_proto.message_type: + self._SetAllFieldTypes(file_proto.package, desc_proto, scope) + + if file_proto.package: + desc_proto_prefix = _PrefixWithDot(file_proto.package) + else: + desc_proto_prefix = '' + + for desc_proto in file_proto.message_type: + desc = self._GetTypeFromScope( + desc_proto_prefix, desc_proto.name, scope) + file_descriptor.message_types_by_name[desc_proto.name] = desc + + for index, service_proto in enumerate(file_proto.service): + file_descriptor.services_by_name[service_proto.name] = ( + self._MakeServiceDescriptor(service_proto, index, scope, + file_proto.package, file_descriptor)) + + self._file_descriptors[file_proto.name] = file_descriptor + + # Add extensions to the pool + file_desc = self._file_descriptors[file_proto.name] + for extension in file_desc.extensions_by_name.values(): + self._AddExtensionDescriptor(extension) + for message_type in file_desc.message_types_by_name.values(): + for extension in message_type.extensions: + self._AddExtensionDescriptor(extension) + + return file_desc + + def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, + scope=None, syntax=None): + """Adds the proto to the pool in the specified package. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: The package the proto should be located in. + file_desc: The file containing this message. + scope: Dict mapping short and full symbols to message and enum types. + syntax: string indicating syntax of the file ("proto2" or "proto3") + + Returns: + The added descriptor. + """ + + if package: + desc_name = '.'.join((package, desc_proto.name)) + else: + desc_name = desc_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + if scope is None: + scope = {} + + nested = [ + self._ConvertMessageDescriptor( + nested, desc_name, file_desc, scope, syntax) + for nested in desc_proto.nested_type] + enums = [ + self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, + scope, False) + for enum in desc_proto.enum_type] + fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) + for index, field in enumerate(desc_proto.field)] + extensions = [ + self._MakeFieldDescriptor(extension, desc_name, index, file_desc, + is_extension=True) + for index, extension in enumerate(desc_proto.extension)] + oneofs = [ + # pylint: disable=g-complex-comprehension + descriptor.OneofDescriptor( + desc.name, + '.'.join((desc_name, desc.name)), + index, + None, + [], + _OptionsOrNone(desc), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for index, desc in enumerate(desc_proto.oneof_decl) + ] + extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] + if extension_ranges: + is_extendable = True + else: + is_extendable = False + desc = descriptor.Descriptor( + name=desc_proto.name, + full_name=desc_name, + filename=file_name, + containing_type=None, + fields=fields, + oneofs=oneofs, + nested_types=nested, + enum_types=enums, + extensions=extensions, + options=_OptionsOrNone(desc_proto), + is_extendable=is_extendable, + extension_ranges=extension_ranges, + file=file_desc, + serialized_start=None, + serialized_end=None, + syntax=syntax, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for nested in desc.nested_types: + nested.containing_type = desc + for enum in desc.enum_types: + enum.containing_type = desc + for field_index, field_desc in enumerate(desc_proto.field): + if field_desc.HasField('oneof_index'): + oneof_index = field_desc.oneof_index + oneofs[oneof_index].fields.append(fields[field_index]) + fields[field_index].containing_oneof = oneofs[oneof_index] + + scope[_PrefixWithDot(desc_name)] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._descriptors[desc_name] = desc + return desc + + def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, + containing_type=None, scope=None, top_level=False): + """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. + + Args: + enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the enum descriptor. + containing_type: The type containing this enum. + scope: Scope containing available types. + top_level: If True, the enum is a top level symbol. If False, the enum + is defined inside a message. + + Returns: + The added descriptor + """ + + if package: + enum_name = '.'.join((package, enum_proto.name)) + else: + enum_name = enum_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + values = [self._MakeEnumValueDescriptor(value, index) + for index, value in enumerate(enum_proto.value)] + desc = descriptor.EnumDescriptor(name=enum_proto.name, + full_name=enum_name, + filename=file_name, + file=file_desc, + values=values, + containing_type=containing_type, + options=_OptionsOrNone(enum_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope['.%s' % enum_name] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._enum_descriptors[enum_name] = desc + + # Add top level enum values. + if top_level: + for value in values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, value.name))) + self._CheckConflictRegister(value, full_name, file_name) + self._top_enum_values[full_name] = value + + return desc + + def _MakeFieldDescriptor(self, field_proto, message_name, index, + file_desc, is_extension=False): + """Creates a field descriptor from a FieldDescriptorProto. + + For message and enum type fields, this method will do a look up + in the pool for the appropriate descriptor for that type. If it + is unavailable, it will fall back to the _source function to + create it. If this type is still unavailable, construction will + fail. + + Args: + field_proto: The proto describing the field. + message_name: The name of the containing message. + index: Index of the field + file_desc: The file containing the field descriptor. + is_extension: Indication that this field is for an extension. + + Returns: + An initialized FieldDescriptor object + """ + + if message_name: + full_name = '.'.join((message_name, field_proto.name)) + else: + full_name = field_proto.name + + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + + return descriptor.FieldDescriptor( + name=field_proto.name, + full_name=full_name, + index=index, + number=field_proto.number, + type=field_proto.type, + cpp_type=None, + message_type=None, + enum_type=None, + containing_type=None, + label=field_proto.label, + has_default_value=False, + default_value=None, + is_extension=is_extension, + extension_scope=None, + options=_OptionsOrNone(field_proto), + json_name=json_name, + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _SetAllFieldTypes(self, package, desc_proto, scope): + """Sets all the descriptor's fields's types. + + This method also sets the containing types on any extensions. + + Args: + package: The current package of desc_proto. + desc_proto: The message descriptor to update. + scope: Enclosing scope of available types. + """ + + package = _PrefixWithDot(package) + + main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) + + if package == '.': + nested_package = _PrefixWithDot(desc_proto.name) + else: + nested_package = '.'.join([package, desc_proto.name]) + + for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): + self._SetFieldType(field_proto, field_desc, nested_package, scope) + + for extension_proto, extension_desc in ( + zip(desc_proto.extension, main_desc.extensions)): + extension_desc.containing_type = self._GetTypeFromScope( + nested_package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, nested_package, scope) + + for nested_type in desc_proto.nested_type: + self._SetAllFieldTypes(nested_package, nested_type, scope) + + def _SetFieldType(self, field_proto, field_desc, package, scope): + """Sets the field's type, cpp_type, message_type and enum_type. + + Args: + field_proto: Data about the field in proto format. + field_desc: The descriptor to modify. + package: The package the field's container is in. + scope: Enclosing scope of available types. + """ + if field_proto.type_name: + desc = self._GetTypeFromScope(package, field_proto.type_name, scope) + else: + desc = None + + if not field_proto.HasField('type'): + if isinstance(desc, descriptor.Descriptor): + field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE + else: + field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM + + field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( + field_proto.type) + + if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE + or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): + field_desc.message_type = desc + + if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.enum_type = desc + + if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: + field_desc.has_default_value = False + field_desc.default_value = [] + elif field_proto.HasField('default_value'): + field_desc.has_default_value = True + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = float(field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = field_proto.default_value + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = field_proto.default_value.lower() == 'true' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values_by_name[ + field_proto.default_value].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = text_encoding.CUnescape( + field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = int(field_proto.default_value) + else: + field_desc.has_default_value = False + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = 0.0 + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = u'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = False + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values[0].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = b'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = 0 + + field_desc.type = field_proto.type + + def _MakeEnumValueDescriptor(self, value_proto, index): + """Creates a enum value descriptor object from a enum value proto. + + Args: + value_proto: The proto describing the enum value. + index: The index of the enum value. + + Returns: + An initialized EnumValueDescriptor object. + """ + + return descriptor.EnumValueDescriptor( + name=value_proto.name, + index=index, + number=value_proto.number, + options=_OptionsOrNone(value_proto), + type=None, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _MakeServiceDescriptor(self, service_proto, service_index, scope, + package, file_desc): + """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. + + Args: + service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. + service_index: The index of the service in the File. + scope: Dict mapping short and full symbols to message and enum types. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the service descriptor. + + Returns: + The added descriptor. + """ + + if package: + service_name = '.'.join((package, service_proto.name)) + else: + service_name = service_proto.name + + methods = [self._MakeMethodDescriptor(method_proto, service_name, package, + scope, index) + for index, method_proto in enumerate(service_proto.method)] + desc = descriptor.ServiceDescriptor( + name=service_proto.name, + full_name=service_name, + index=service_index, + methods=methods, + options=_OptionsOrNone(service_proto), + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._service_descriptors[service_name] = desc + return desc + + def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, + index): + """Creates a method descriptor from a MethodDescriptorProto. + + Args: + method_proto: The proto describing the method. + service_name: The name of the containing service. + package: Optional package name to look up for types. + scope: Scope containing available types. + index: Index of the method in the service. + + Returns: + An initialized MethodDescriptor object. + """ + full_name = '.'.join((service_name, method_proto.name)) + input_type = self._GetTypeFromScope( + package, method_proto.input_type, scope) + output_type = self._GetTypeFromScope( + package, method_proto.output_type, scope) + return descriptor.MethodDescriptor( + name=method_proto.name, + full_name=full_name, + index=index, + containing_service=None, + input_type=input_type, + output_type=output_type, + client_streaming=method_proto.client_streaming, + server_streaming=method_proto.server_streaming, + options=_OptionsOrNone(method_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _ExtractSymbols(self, descriptors): + """Pulls out all the symbols from descriptor protos. + + Args: + descriptors: The messages to extract descriptors from. + Yields: + A two element tuple of the type name and descriptor object. + """ + + for desc in descriptors: + yield (_PrefixWithDot(desc.full_name), desc) + for symbol in self._ExtractSymbols(desc.nested_types): + yield symbol + for enum in desc.enum_types: + yield (_PrefixWithDot(enum.full_name), enum) + + def _GetDeps(self, dependencies, visited=None): + """Recursively finds dependencies for file protos. + + Args: + dependencies: The names of the files being depended on. + visited: The names of files already found. + + Yields: + Each direct and indirect dependency. + """ + + visited = visited or set() + for dependency in dependencies: + if dependency not in visited: + visited.add(dependency) + dep_desc = self.FindFileByName(dependency) + yield dep_desc + public_files = [d.name for d in dep_desc.public_dependencies] + yield from self._GetDeps(public_files, visited) + + def _GetTypeFromScope(self, package, type_name, scope): + """Finds a given type name in the current scope. + + Args: + package: The package the proto should be located in. + type_name: The name of the type to be found in the scope. + scope: Dict mapping short and full symbols to message and enum types. + + Returns: + The descriptor for the requested type. + """ + if type_name not in scope: + components = _PrefixWithDot(package).split('.') + while components: + possible_match = '.'.join(components + [type_name]) + if possible_match in scope: + type_name = possible_match + break + else: + components.pop(-1) + return scope[type_name] + + +def _PrefixWithDot(name): + return name if name.startswith('.') else '.%s' % name + + +if _USE_C_DESCRIPTORS: + # TODO(amauryfa): This pool could be constructed from Python code, when we + # support a flag like 'use_cpp_generated_pool=True'. + # pylint: disable=protected-access + _DEFAULT = descriptor._message.default_pool +else: + _DEFAULT = DescriptorPool() + + +def Default(): + return _DEFAULT diff --git a/openpype/hosts/hiero/vendor/google/protobuf/duration_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/duration_pb2.py new file mode 100644 index 0000000000..a8ecc07bdf --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/duration_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/duration.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\"*\n\x08\x44uration\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DURATION._serialized_start=51 + _DURATION._serialized_end=93 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/empty_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/empty_pb2.py new file mode 100644 index 0000000000..0b4d554db3 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/empty_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/empty.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _EMPTY._serialized_start=48 + _EMPTY._serialized_end=55 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/field_mask_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/field_mask_pb2.py new file mode 100644 index 0000000000..80a4e96e59 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/field_mask_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/field_mask.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"\x1a\n\tFieldMask\x12\r\n\x05paths\x18\x01 \x03(\tB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _FIELDMASK._serialized_start=53 + _FIELDMASK._serialized_end=79 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/__init__.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/_parameterized.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/_parameterized.py new file mode 100644 index 0000000000..afdbb78c36 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/_parameterized.py @@ -0,0 +1,443 @@ +#! /usr/bin/env python +# +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Adds support for parameterized tests to Python's unittest TestCase class. + +A parameterized test is a method in a test case that is invoked with different +argument tuples. + +A simple example: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + (1, 2, 3), + (4, 5, 9), + (1, 1, 3)) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Each invocation is a separate test case and properly isolated just +like a normal test method, with its own setUp/tearDown cycle. In the +example above, there are three separate testcases, one of which will +fail due to an assertion error (1 + 1 != 3). + +Parameters for individual test cases can be tuples (with positional parameters) +or dictionaries (with named parameters): + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + {'op1': 1, 'op2': 2, 'result': 3}, + {'op1': 4, 'op2': 5, 'result': 9}, + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +If a parameterized test fails, the error message will show the +original test name (which is modified internally) and the arguments +for the specific invocation, which are part of the string returned by +the shortDescription() method on test cases. + +The id method of the test, used internally by the unittest framework, +is also modified to show the arguments. To make sure that test names +stay the same across several invocations, object representations like + + >>> class Foo(object): + ... pass + >>> repr(Foo()) + '<__main__.Foo object at 0x23d8610>' + +are turned into '<__main__.Foo>'. For even more descriptive names, +especially in test logs, you can use the named_parameters decorator. In +this case, only tuples are supported, and the first parameters has to +be a string (or an object that returns an apt name when converted via +str()): + + class NamedExample(parameterized.TestCase): + @parameterized.named_parameters( + ('Normal', 'aa', 'aaa', True), + ('EmptyPrefix', '', 'abc', True), + ('BothEmpty', '', '', True)) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, strings.startswith(prefix)) + +Named tests also have the benefit that they can be run individually +from the command line: + + $ testmodule.py NamedExample.testStartsWithNormal + . + -------------------------------------------------------------------- + Ran 1 test in 0.000s + + OK + +Parameterized Classes +===================== +If invocation arguments are shared across test methods in a single +TestCase class, instead of decorating all test methods +individually, the class itself can be decorated: + + @parameterized.parameters( + (1, 2, 3) + (4, 5, 9)) + class ArithmeticTest(parameterized.TestCase): + def testAdd(self, arg1, arg2, result): + self.assertEqual(arg1 + arg2, result) + + def testSubtract(self, arg2, arg2, result): + self.assertEqual(result - arg1, arg2) + +Inputs from Iterables +===================== +If parameters should be shared across several test cases, or are dynamically +created from other sources, a single non-tuple iterable can be passed into +the decorator. This iterable will be used to obtain the test cases: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + c.op1, c.op2, c.result for c in testcases + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Single-Argument Test Methods +============================ +If a test method takes only one argument, the single argument does not need to +be wrapped into a tuple: + + class NegativeNumberExample(parameterized.TestCase): + @parameterized.parameters( + -1, -3, -4, -5 + ) + def testIsNegative(self, arg): + self.assertTrue(IsNegative(arg)) +""" + +__author__ = 'tmarek@google.com (Torsten Marek)' + +import functools +import re +import types +import unittest +import uuid + +try: + # Since python 3 + import collections.abc as collections_abc +except ImportError: + # Won't work after python 3.8 + import collections as collections_abc + +ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') +_SEPARATOR = uuid.uuid1().hex +_FIRST_ARG = object() +_ARGUMENT_REPR = object() + + +def _CleanRepr(obj): + return ADDR_RE.sub(r'<\1>', repr(obj)) + + +# Helper function formerly from the unittest module, removed from it in +# Python 2.7. +def _StrClass(cls): + return '%s.%s' % (cls.__module__, cls.__name__) + + +def _NonStringIterable(obj): + return (isinstance(obj, collections_abc.Iterable) and + not isinstance(obj, str)) + + +def _FormatParameterList(testcase_params): + if isinstance(testcase_params, collections_abc.Mapping): + return ', '.join('%s=%s' % (argname, _CleanRepr(value)) + for argname, value in testcase_params.items()) + elif _NonStringIterable(testcase_params): + return ', '.join(map(_CleanRepr, testcase_params)) + else: + return _FormatParameterList((testcase_params,)) + + +class _ParameterizedTestIter(object): + """Callable and iterable class for producing new test cases.""" + + def __init__(self, test_method, testcases, naming_type): + """Returns concrete test functions for a test and a list of parameters. + + The naming_type is used to determine the name of the concrete + functions as reported by the unittest framework. If naming_type is + _FIRST_ARG, the testcases must be tuples, and the first element must + have a string representation that is a valid Python identifier. + + Args: + test_method: The decorated test method. + testcases: (list of tuple/dict) A list of parameter + tuples/dicts for individual test invocations. + naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. + """ + self._test_method = test_method + self.testcases = testcases + self._naming_type = naming_type + + def __call__(self, *args, **kwargs): + raise RuntimeError('You appear to be running a parameterized test case ' + 'without having inherited from parameterized.' + 'TestCase. This is bad because none of ' + 'your test cases are actually being run.') + + def __iter__(self): + test_method = self._test_method + naming_type = self._naming_type + + def MakeBoundParamTest(testcase_params): + @functools.wraps(test_method) + def BoundParamTest(self): + if isinstance(testcase_params, collections_abc.Mapping): + test_method(self, **testcase_params) + elif _NonStringIterable(testcase_params): + test_method(self, *testcase_params) + else: + test_method(self, testcase_params) + + if naming_type is _FIRST_ARG: + # Signal the metaclass that the name of the test function is unique + # and descriptive. + BoundParamTest.__x_use_name__ = True + BoundParamTest.__name__ += str(testcase_params[0]) + testcase_params = testcase_params[1:] + elif naming_type is _ARGUMENT_REPR: + # __x_extra_id__ is used to pass naming information to the __new__ + # method of TestGeneratorMetaclass. + # The metaclass will make sure to create a unique, but nondescriptive + # name for this test. + BoundParamTest.__x_extra_id__ = '(%s)' % ( + _FormatParameterList(testcase_params),) + else: + raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) + + BoundParamTest.__doc__ = '%s(%s)' % ( + BoundParamTest.__name__, _FormatParameterList(testcase_params)) + if test_method.__doc__: + BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) + return BoundParamTest + return (MakeBoundParamTest(c) for c in self.testcases) + + +def _IsSingletonList(testcases): + """True iff testcases contains only a single non-tuple element.""" + return len(testcases) == 1 and not isinstance(testcases[0], tuple) + + +def _ModifyClass(class_object, testcases, naming_type): + assert not getattr(class_object, '_id_suffix', None), ( + 'Cannot add parameters to %s,' + ' which already has parameterized methods.' % (class_object,)) + class_object._id_suffix = id_suffix = {} + # We change the size of __dict__ while we iterate over it, + # which Python 3.x will complain about, so use copy(). + for name, obj in class_object.__dict__.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) + and isinstance(obj, types.FunctionType)): + delattr(class_object, name) + methods = {} + _UpdateClassDictForParamTestCase( + methods, id_suffix, name, + _ParameterizedTestIter(obj, testcases, naming_type)) + for name, meth in methods.items(): + setattr(class_object, name, meth) + + +def _ParameterDecorator(naming_type, testcases): + """Implementation of the parameterization decorators. + + Args: + naming_type: The naming type. + testcases: Testcase parameters. + + Returns: + A function for modifying the decorated object. + """ + def _Apply(obj): + if isinstance(obj, type): + _ModifyClass( + obj, + list(testcases) if not isinstance(testcases, collections_abc.Sequence) + else testcases, + naming_type) + return obj + else: + return _ParameterizedTestIter(obj, testcases, naming_type) + + if _IsSingletonList(testcases): + assert _NonStringIterable(testcases[0]), ( + 'Single parameter argument must be a non-string iterable') + testcases = testcases[0] + + return _Apply + + +def parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples/dicts/objects (for tests + with only one argument). + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_ARGUMENT_REPR, testcases) + + +def named_parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. The first element of + each parameter tuple should be a string and will be appended to the + name of the test method. + + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_FIRST_ARG, testcases) + + +class TestGeneratorMetaclass(type): + """Metaclass for test cases with test generators. + + A test generator is an iterable in a testcase that produces callables. These + callables must be single-argument methods. These methods are injected into + the class namespace and the original iterable is removed. If the name of the + iterable conforms to the test pattern, the injected methods will be picked + up as tests by the unittest framework. + + In general, it is supposed to be used in conjunction with the + parameters decorator. + """ + + def __new__(mcs, class_name, bases, dct): + dct['_id_suffix'] = id_suffix = {} + for name, obj in dct.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) and + _NonStringIterable(obj)): + iterator = iter(obj) + dct.pop(name) + _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) + + return type.__new__(mcs, class_name, bases, dct) + + +def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): + """Adds individual test cases to a dictionary. + + Args: + dct: The target dictionary. + id_suffix: The dictionary for mapping names to test IDs. + name: The original name of the test case. + iterator: The iterator generating the individual test cases. + """ + for idx, func in enumerate(iterator): + assert callable(func), 'Test generators must yield callables, got %r' % ( + func,) + if getattr(func, '__x_use_name__', False): + new_name = func.__name__ + else: + new_name = '%s%s%d' % (name, _SEPARATOR, idx) + assert new_name not in dct, ( + 'Name of parameterized test case "%s" not unique' % (new_name,)) + dct[new_name] = func + id_suffix[new_name] = getattr(func, '__x_extra_id__', '') + + +class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): + """Base class for test cases using the parameters decorator.""" + + def _OriginalName(self): + return self._testMethodName.split(_SEPARATOR)[0] + + def __str__(self): + return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) + + def id(self): # pylint: disable=invalid-name + """Returns the descriptive ID of the test. + + This is used internally by the unittesting framework to get a name + for the test to be used in reports. + + Returns: + The test id. + """ + return '%s.%s%s' % (_StrClass(self.__class__), + self._OriginalName(), + self._id_suffix.get(self._testMethodName, '')) + + +def CoopTestCase(other_base_class): + """Returns a new base class with a cooperative metaclass base. + + This enables the TestCase to be used in combination + with other base classes that have custom metaclasses, such as + mox.MoxTestBase. + + Only works with metaclasses that do not override type.__new__. + + Example: + + import google3 + import mox + + from google3.testing.pybase import parameterized + + class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): + ... + + Args: + other_base_class: (class) A test case base class. + + Returns: + A new class object. + """ + metaclass = type( + 'CoopMetaclass', + (other_base_class.__metaclass__, + TestGeneratorMetaclass), {}) + return metaclass( + 'CoopTestCase', + (other_base_class, TestCase), {}) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/api_implementation.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/api_implementation.py new file mode 100644 index 0000000000..7fef237670 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/api_implementation.py @@ -0,0 +1,112 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Determine which implementation of the protobuf API is used in this process. +""" + +import os +import sys +import warnings + +try: + # pylint: disable=g-import-not-at-top + from google.protobuf.internal import _api_implementation + # The compile-time constants in the _api_implementation module can be used to + # switch to a certain implementation of the Python API at build time. + _api_version = _api_implementation.api_version +except ImportError: + _api_version = -1 # Unspecified by compiler flags. + +if _api_version == 1: + raise ValueError('api_version=1 is no longer supported.') + + +_default_implementation_type = ('cpp' if _api_version > 0 else 'python') + + +# This environment variable can be used to switch to a certain implementation +# of the Python API, overriding the compile-time constants in the +# _api_implementation module. Right now only 'python' and 'cpp' are valid +# values. Any other value will be ignored. +_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', + _default_implementation_type) + +if _implementation_type != 'python': + _implementation_type = 'cpp' + +if 'PyPy' in sys.version and _implementation_type == 'cpp': + warnings.warn('PyPy does not work yet with cpp protocol buffers. ' + 'Falling back to the python implementation.') + _implementation_type = 'python' + + +# Detect if serialization should be deterministic by default +try: + # The presence of this module in a build allows the proto implementation to + # be upgraded merely via build deps. + # + # NOTE: Merely importing this automatically enables deterministic proto + # serialization for C++ code, but we still need to export it as a boolean so + # that we can do the same for `_implementation_type == 'python'`. + # + # NOTE2: It is possible for C++ code to enable deterministic serialization by + # default _without_ affecting Python code, if the C++ implementation is not in + # use by this module. That is intended behavior, so we don't actually expose + # this boolean outside of this module. + # + # pylint: disable=g-import-not-at-top,unused-import + from google.protobuf import enable_deterministic_proto_serialization + _python_deterministic_proto_serialization = True +except ImportError: + _python_deterministic_proto_serialization = False + + +# Usage of this function is discouraged. Clients shouldn't care which +# implementation of the API is in use. Note that there is no guarantee +# that differences between APIs will be maintained. +# Please don't use this function if possible. +def Type(): + return _implementation_type + + +def _SetType(implementation_type): + """Never use! Only for protobuf benchmark.""" + global _implementation_type + _implementation_type = implementation_type + + +# See comment on 'Type' above. +def Version(): + return 2 + + +# For internal use only +def IsPythonDefaultSerializationDeterministic(): + return _python_deterministic_proto_serialization diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/builder.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/builder.py new file mode 100644 index 0000000000..64353ee4af --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/builder.py @@ -0,0 +1,130 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Builds descriptors, message classes and services for generated _pb2.py. + +This file is only called in python generated _pb2.py files. It builds +descriptors, message classes and services that users can directly use +in generated code. +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +_sym_db = _symbol_database.Default() + + +def BuildMessageAndEnumDescriptors(file_des, module): + """Builds message and enum descriptors. + + Args: + file_des: FileDescriptor of the .proto file + module: Generated _pb2 module + """ + + def BuildNestedDescriptors(msg_des, prefix): + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + module_name = prefix + name.upper() + module[module_name] = nested_msg + BuildNestedDescriptors(nested_msg, module_name + '_') + for enum_des in msg_des.enum_types: + module[prefix + enum_des.name.upper()] = enum_des + + for (name, msg_des) in file_des.message_types_by_name.items(): + module_name = '_' + name.upper() + module[module_name] = msg_des + BuildNestedDescriptors(msg_des, module_name + '_') + + +def BuildTopDescriptorsAndMessages(file_des, module_name, module): + """Builds top level descriptors and message classes. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + + def BuildMessage(msg_des): + create_dict = {} + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + create_dict[name] = BuildMessage(nested_msg) + create_dict['DESCRIPTOR'] = msg_des + create_dict['__module__'] = module_name + message_class = _reflection.GeneratedProtocolMessageType( + msg_des.name, (_message.Message,), create_dict) + _sym_db.RegisterMessage(message_class) + return message_class + + # top level enums + for (name, enum_des) in file_des.enum_types_by_name.items(): + module['_' + name.upper()] = enum_des + module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) + for enum_value in enum_des.values: + module[enum_value.name] = enum_value.number + + # top level extensions + for (name, extension_des) in file_des.extensions_by_name.items(): + module[name.upper() + '_FIELD_NUMBER'] = extension_des.number + module[name] = extension_des + + # services + for (name, service) in file_des.services_by_name.items(): + module['_' + name.upper()] = service + + # Build messages. + for (name, msg_des) in file_des.message_types_by_name.items(): + module[name] = BuildMessage(msg_des) + + +def BuildServices(file_des, module_name, module): + """Builds services classes and services stub class. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import service as _service + from google.protobuf import service_reflection + # pylint: enable=g-import-not-at-top + for (name, service) in file_des.services_by_name.items(): + module[name] = service_reflection.GeneratedServiceType( + name, (_service.Service,), + dict(DESCRIPTOR=service, __module__=module_name)) + stub_name = name + '_Stub' + module[stub_name] = service_reflection.GeneratedServiceStubType( + stub_name, (module[name],), + dict(DESCRIPTOR=service, __module__=module_name)) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/containers.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/containers.py new file mode 100644 index 0000000000..29fbb53d2f --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/containers.py @@ -0,0 +1,710 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains container classes to represent different protocol buffer types. + +This file defines container classes which represent categories of protocol +buffer field types which need extra maintenance. Currently these categories +are: + +- Repeated scalar fields - These are all repeated fields which aren't + composite (e.g. they are of simple types like int32, string, etc). +- Repeated composite fields - Repeated fields which are composite. This + includes groups and nested messages. +""" + +import collections.abc +import copy +import pickle +from typing import ( + Any, + Iterable, + Iterator, + List, + MutableMapping, + MutableSequence, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + overload, +) + + +_T = TypeVar('_T') +_K = TypeVar('_K') +_V = TypeVar('_V') + + +class BaseContainer(Sequence[_T]): + """Base container class.""" + + # Minimizes memory usage and disallows assignment to other attributes. + __slots__ = ['_message_listener', '_values'] + + def __init__(self, message_listener: Any) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The RepeatedScalarFieldContainer will call this object's + Modified() method when it is modified. + """ + self._message_listener = message_listener + self._values = [] + + @overload + def __getitem__(self, key: int) -> _T: + ... + + @overload + def __getitem__(self, key: slice) -> List[_T]: + ... + + def __getitem__(self, key): + """Retrieves item by the specified key.""" + return self._values[key] + + def __len__(self) -> int: + """Returns the number of elements in the container.""" + return len(self._values) + + def __ne__(self, other: Any) -> bool: + """Checks if another instance isn't equal to this one.""" + # The concrete classes should define __eq__. + return not self == other + + __hash__ = None + + def __repr__(self) -> str: + return repr(self._values) + + def sort(self, *args, **kwargs) -> None: + # Continue to support the old sort_function keyword argument. + # This is expected to be a rare occurrence, so use LBYL to avoid + # the overhead of actually catching KeyError. + if 'sort_function' in kwargs: + kwargs['cmp'] = kwargs.pop('sort_function') + self._values.sort(*args, **kwargs) + + def reverse(self) -> None: + self._values.reverse() + + +# TODO(slebedev): Remove this. BaseContainer does *not* conform to +# MutableSequence, only its subclasses do. +collections.abc.MutableSequence.register(BaseContainer) + + +class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, type-checked, list-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_type_checker'] + + def __init__( + self, + message_listener: Any, + type_checker: Any, + ) -> None: + """Args: + + message_listener: A MessageListener implementation. The + RepeatedScalarFieldContainer will call this object's Modified() method + when it is modified. + type_checker: A type_checkers.ValueChecker instance to run on elements + inserted into this container. + """ + super().__init__(message_listener) + self._type_checker = type_checker + + def append(self, value: _T) -> None: + """Appends an item to the list. Similar to list.append().""" + self._values.append(self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position. Similar to list.insert().""" + self._values.insert(key, self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given iterable. Similar to list.extend().""" + if elem_seq is None: + return + try: + elem_seq_iter = iter(elem_seq) + except TypeError: + if not elem_seq: + # silently ignore falsy inputs :-/. + # TODO(ptucker): Deprecate this behavior. b/18413862 + return + raise + + new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] + if new_values: + self._values.extend(new_values) + self._message_listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one. We do not check the types of the individual fields. + """ + self._values.extend(other) + self._message_listener.Modified() + + def remove(self, elem: _T): + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value) -> None: + """Sets the item on the specified position.""" + if isinstance(key, slice): + if key.step is not None: + raise ValueError('Extended slices not supported') + self._values[key] = map(self._type_checker.CheckValue, value) + self._message_listener.Modified() + else: + self._values[key] = self._type_checker.CheckValue(value) + self._message_listener.Modified() + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + # Special case for the same type which should be common and fast. + if isinstance(other, self.__class__): + return other._values == self._values + # We are presumably comparing against some other sequence type. + return other == self._values + + def __deepcopy__( + self, + unused_memo: Any = None, + ) -> 'RepeatedScalarFieldContainer[_T]': + clone = RepeatedScalarFieldContainer( + copy.deepcopy(self._message_listener), self._type_checker) + clone.MergeFrom(self) + return clone + + def __reduce__(self, **kwargs) -> NoReturn: + raise pickle.PickleError( + "Can't pickle repeated scalar fields, convert to list first") + + +# TODO(slebedev): Constrain T to be a subtype of Message. +class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, list-like container for holding repeated composite fields.""" + + # Disallows assignment to other attributes. + __slots__ = ['_message_descriptor'] + + def __init__(self, message_listener: Any, message_descriptor: Any) -> None: + """ + Note that we pass in a descriptor instead of the generated directly, + since at the time we construct a _RepeatedCompositeFieldContainer we + haven't yet necessarily initialized the type that will be contained in the + container. + + Args: + message_listener: A MessageListener implementation. + The RepeatedCompositeFieldContainer will call this object's + Modified() method when it is modified. + message_descriptor: A Descriptor instance describing the protocol type + that should be present in this container. We'll use the + _concrete_class field of this descriptor when the client calls add(). + """ + super().__init__(message_listener) + self._message_descriptor = message_descriptor + + def add(self, **kwargs: Any) -> _T: + """Adds a new element at the end of the list and returns it. Keyword + arguments may be used to initialize the element. + """ + new_element = self._message_descriptor._concrete_class(**kwargs) + new_element._SetListener(self._message_listener) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + return new_element + + def append(self, value: _T) -> None: + """Appends one element by copying the message.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position by copying.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.insert(key, new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given sequence of elements of the same type + + as this one, copying each individual message. + """ + message_class = self._message_descriptor._concrete_class + listener = self._message_listener + values = self._values + for message in elem_seq: + new_element = message_class() + new_element._SetListener(listener) + new_element.MergeFrom(message) + values.append(new_element) + listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one, copying each individual message. + """ + self.extend(other) + + def remove(self, elem: _T) -> None: + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value): + # This method is implemented to make RepeatedCompositeFieldContainer + # structurally compatible with typing.MutableSequence. It is + # otherwise unsupported and will always raise an error. + raise TypeError( + f'{self.__class__.__name__} object does not support item assignment') + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + if not isinstance(other, self.__class__): + raise TypeError('Can only compare repeated composite fields against ' + 'other repeated composite fields.') + return self._values == other._values + + +class ScalarMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', + '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + key_checker: Any, + value_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._key_checker = key_checker + self._value_checker = value_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + try: + return self._values[key] + except KeyError: + key = self._key_checker.CheckValue(key) + val = self._value_checker.DefaultValue() + self._values[key] = val + return val + + def __contains__(self, item: _K) -> bool: + # We check the key's type to match the strong-typing flavor of the API. + # Also this makes it easier to match the behavior of the C++ implementation. + self._key_checker.CheckValue(item) + return item in self._values + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __setitem__(self, key: _K, value: _V) -> _T: + checked_key = self._key_checker.CheckValue(key) + checked_value = self._value_checker.CheckValue(value) + self._values[checked_key] = checked_value + self._message_listener.Modified() + + def __delitem__(self, key: _K) -> None: + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: + self._values.update(other._values) + self._message_listener.Modified() + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class MessageMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for with submessage values.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_values', '_message_listener', + '_message_descriptor', '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + message_descriptor: Any, + key_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._message_descriptor = message_descriptor + self._key_checker = key_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + key = self._key_checker.CheckValue(key) + try: + return self._values[key] + except KeyError: + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + self._values[key] = new_element + self._message_listener.Modified() + return new_element + + def get_or_create(self, key: _K) -> _V: + """get_or_create() is an alias for getitem (ie. map[key]). + + Args: + key: The key to get or create in the map. + + This is useful in cases where you want to be explicit that the call is + mutating the map. This can avoid lint errors for statements like this + that otherwise would appear to be pointless statements: + + msg.my_map[key] + """ + return self[key] + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __contains__(self, item: _K) -> bool: + item = self._key_checker.CheckValue(item) + return item in self._values + + def __setitem__(self, key: _K, value: _V) -> NoReturn: + raise ValueError('May not set values directly, call my_map[key].foo = 5') + + def __delitem__(self, key: _K) -> None: + key = self._key_checker.CheckValue(key) + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: + # pylint: disable=protected-access + for key in other._values: + # According to documentation: "When parsing from the wire or when merging, + # if there are duplicate map keys the last key seen is used". + if key in self: + del self[key] + self[key].CopyFrom(other[key]) + # self._message_listener.Modified() not required here, because + # mutations to submessages already propagate. + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class _UnknownField: + """A parsed unknown field.""" + + # Disallows assignment to other attributes. + __slots__ = ['_field_number', '_wire_type', '_data'] + + def __init__(self, field_number, wire_type, data): + self._field_number = field_number + self._wire_type = wire_type + self._data = data + return + + def __lt__(self, other): + # pylint: disable=protected-access + return self._field_number < other._field_number + + def __eq__(self, other): + if self is other: + return True + # pylint: disable=protected-access + return (self._field_number == other._field_number and + self._wire_type == other._wire_type and + self._data == other._data) + + +class UnknownFieldRef: # pylint: disable=missing-class-docstring + + def __init__(self, parent, index): + self._parent = parent + self._index = index + + def _check_valid(self): + if not self._parent: + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + if self._index >= len(self._parent): + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + + @property + def field_number(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._field_number + + @property + def wire_type(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._wire_type + + @property + def data(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._data + + +class UnknownFieldSet: + """UnknownField container""" + + # Disallows assignment to other attributes. + __slots__ = ['_values'] + + def __init__(self): + self._values = [] + + def __getitem__(self, index): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + size = len(self._values) + if index < 0: + index += size + if index < 0 or index >= size: + raise IndexError('index %d out of range'.index) + + return UnknownFieldRef(self, index) + + def _internal_get(self, index): + return self._values[index] + + def __len__(self): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + return len(self._values) + + def _add(self, field_number, wire_type, data): + unknown_field = _UnknownField(field_number, wire_type, data) + self._values.append(unknown_field) + return unknown_field + + def __iter__(self): + for i in range(len(self)): + yield UnknownFieldRef(self, i) + + def _extend(self, other): + if other is None: + return + # pylint: disable=protected-access + self._values.extend(other._values) + + def __eq__(self, other): + if self is other: + return True + # Sort unknown fields because their order shouldn't + # affect equality test. + values = list(self._values) + if other is None: + return not values + values.sort() + # pylint: disable=protected-access + other_values = sorted(other._values) + return values == other_values + + def _clear(self): + for value in self._values: + # pylint: disable=protected-access + if isinstance(value._data, UnknownFieldSet): + value._data._clear() # pylint: disable=protected-access + self._values = None diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/decoder.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/decoder.py new file mode 100644 index 0000000000..bc1b7b785c --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/decoder.py @@ -0,0 +1,1029 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Code for decoding protocol buffer primitives. + +This code is very similar to encoder.py -- read the docs for that module first. + +A "decoder" is a function with the signature: + Decode(buffer, pos, end, message, field_dict) +The arguments are: + buffer: The string containing the encoded message. + pos: The current position in the string. + end: The position in the string where the current message ends. May be + less than len(buffer) if we're reading a sub-message. + message: The message object into which we're parsing. + field_dict: message._fields (avoids a hashtable lookup). +The decoder reads the field and stores it into field_dict, returning the new +buffer position. A decoder for a repeated field may proactively decode all of +the elements of that field, if they appear consecutively. + +Note that decoders may throw any of the following: + IndexError: Indicates a truncated message. + struct.error: Unpacking of a fixed-width field failed. + message.DecodeError: Other errors. + +Decoders are expected to raise an exception if they are called with pos > end. +This allows callers to be lax about bounds checking: it's fineto read past +"end" as long as you are sure that someone else will notice and throw an +exception later on. + +Something up the call stack is expected to catch IndexError and struct.error +and convert them to message.DecodeError. + +Decoders are constructed using decoder constructors with the signature: + MakeDecoder(field_number, is_repeated, is_packed, key, new_default) +The arguments are: + field_number: The field number of the field we want to decode. + is_repeated: Is the field a repeated field? (bool) + is_packed: Is the field a packed field? (bool) + key: The key to use when looking up the field within field_dict. + (This is actually the FieldDescriptor but nothing in this + file should depend on that.) + new_default: A function which takes a message object as a parameter and + returns a new instance of the default value for this field. + (This is called for repeated fields and sub-messages, when an + instance does not already exist.) + +As with encoders, we define a decoder constructor for every type of field. +Then, for every field of every message class we construct an actual decoder. +That decoder goes into a dict indexed by tag, so when we decode a message +we repeatedly read a tag, look up the corresponding decoder, and invoke it. +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +import math +import struct + +from google.protobuf.internal import containers +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import message + + +# This is not for optimization, but rather to avoid conflicts with local +# variables named "message". +_DecodeError = message.DecodeError + + +def _VarintDecoder(mask, result_type): + """Return an encoder for a basic varint value (does not include tag). + + Decoded values will be bitwise-anded with the given mask before being + returned, e.g. to limit them to 32 bits. The returned decoder does not + take the usual "end" parameter -- the caller is expected to do bounds checking + after the fact (often the caller can defer such checking until later). The + decoder returns a (value, new_pos) pair. + """ + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + + +def _SignedVarintDecoder(bits, result_type): + """Like _VarintDecoder() but decodes signed values.""" + + signbit = 1 << (bits - 1) + mask = (1 << bits) - 1 + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = (result ^ signbit) - signbit + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + +# All 32-bit and 64-bit values are represented as int. +_DecodeVarint = _VarintDecoder((1 << 64) - 1, int) +_DecodeSignedVarint = _SignedVarintDecoder(64, int) + +# Use these versions for values which must be limited to 32 bits. +_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) +_DecodeSignedVarint32 = _SignedVarintDecoder(32, int) + + +def ReadTag(buffer, pos): + """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. + + We return the raw bytes of the tag rather than decoding them. The raw + bytes can then be used to look up the proper decoder. This effectively allows + us to trade some work that would be done in pure-python (decoding a varint) + for work that is done in C (searching for a byte string in a hash table). + In a low-level language it would be much cheaper to decode the varint and + use that, but not in Python. + + Args: + buffer: memoryview object of the encoded bytes + pos: int of the current position to start from + + Returns: + Tuple[bytes, int] of the tag data and new position. + """ + start = pos + while buffer[pos] & 0x80: + pos += 1 + pos += 1 + + tag_bytes = buffer[start:pos].tobytes() + return tag_bytes, pos + + +# -------------------------------------------------------------------- + + +def _SimpleDecoder(wire_type, decode_value): + """Return a constructor for a decoder for fields of a particular type. + + Args: + wire_type: The field's wire type. + decode_value: A function which decodes an individual value, e.g. + _DecodeVarint() + """ + + def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + if is_packed: + local_DecodeVarint = _DecodeVarint + def DecodePackedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + (endpoint, pos) = local_DecodeVarint(buffer, pos) + endpoint += pos + if endpoint > end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + (element, pos) = decode_value(buffer, pos) + value.append(element) + if pos > endpoint: + del value[-1] # Discard corrupt value. + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_type) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = decode_value(buffer, pos) + value.append(element) + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (new_value, pos) = decode_value(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not new_value: + field_dict.pop(key, None) + else: + field_dict[key] = new_value + return pos + return DecodeField + + return SpecificDecoder + + +def _ModifiedDecoder(wire_type, decode_value, modify_value): + """Like SimpleDecoder but additionally invokes modify_value on every value + before storing it. Usually modify_value is ZigZagDecode. + """ + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + def InnerDecode(buffer, pos): + (result, new_pos) = decode_value(buffer, pos) + return (modify_value(result), new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _StructPackDecoder(wire_type, format): + """Return a constructor for a decoder for a fixed-width field. + + Args: + wire_type: The field's wire type. + format: The format string to pass to struct.unpack(). + """ + + value_size = struct.calcsize(format) + local_unpack = struct.unpack + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + + def InnerDecode(buffer, pos): + new_pos = pos + value_size + result = local_unpack(format, buffer[pos:new_pos])[0] + return (result, new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _FloatDecoder(): + """Returns a decoder for a float field. + + This code works around a bug in struct.unpack for non-finite 32-bit + floating-point values. + """ + + local_unpack = struct.unpack + + def InnerDecode(buffer, pos): + """Decode serialized float to a float and new position. + + Args: + buffer: memoryview of the serialized bytes + pos: int, position in the memory view to start at. + + Returns: + Tuple[float, int] of the deserialized float value and new position + in the serialized data. + """ + # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign + # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. + new_pos = pos + 4 + float_bytes = buffer[pos:new_pos].tobytes() + + # If this value has all its exponent bits set, then it's non-finite. + # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. + # To avoid that, we parse it specially. + if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): + # If at least one significand bit is set... + if float_bytes[0:3] != b'\x00\x00\x80': + return (math.nan, new_pos) + # If sign bit is set... + if float_bytes[3:4] == b'\xFF': + return (-math.inf, new_pos) + return (math.inf, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack('= b'\xF0') + and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): + return (math.nan, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack(' end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + value_start_pos = pos + (element, pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + if pos > endpoint: + if element in enum_type.values_by_number: + del value[-1] # Discard corrupt value. + else: + del message._unknown_fields[-1] + # pylint: disable=protected-access + del message._unknown_field_set._values[-1] + # pylint: enable=protected-access + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (tag_bytes, buffer[pos:new_pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value_start_pos = pos + (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not enum_value: + field_dict.pop(key, None) + return pos + # pylint: disable=protected-access + if enum_value in enum_type.values_by_number: + field_dict[key] = enum_value + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, enum_value) + # pylint: enable=protected-access + return pos + return DecodeField + + +# -------------------------------------------------------------------- + + +Int32Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) + +Int64Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) + +UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) +UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) + +SInt32Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) +SInt64Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, ' end: + raise _DecodeError('Truncated string.') + value.append(_ConvertToUnicode(buffer[pos:new_pos])) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) + return new_pos + return DecodeField + + +def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for a bytes field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + value.append(buffer[pos:new_pos].tobytes()) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = buffer[pos:new_pos].tobytes() + return new_pos + return DecodeField + + +def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a group field.""" + + end_tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_END_GROUP) + end_tag_len = len(end_tag_bytes) + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_START_GROUP) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value.add()._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + return new_pos + return DecodeField + + +def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a message field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + return new_pos + return DecodeField + + +# -------------------------------------------------------------------- + +MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) + +def MessageSetItemDecoder(descriptor): + """Returns a decoder for a MessageSet item. + + The parameter is the message Descriptor. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + + type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) + message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) + item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) + + local_ReadTag = ReadTag + local_DecodeVarint = _DecodeVarint + local_SkipField = SkipField + + def DecodeItem(buffer, pos, end, message, field_dict): + """Decode serialized message set to its value and new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + message_set_item_start = pos + type_id = -1 + message_start = -1 + message_end = -1 + + # Technically, type_id and message can appear in any order, so we need + # a little loop here. + while 1: + (tag_bytes, pos) = local_ReadTag(buffer, pos) + if tag_bytes == type_id_tag_bytes: + (type_id, pos) = local_DecodeVarint(buffer, pos) + elif tag_bytes == message_tag_bytes: + (size, message_start) = local_DecodeVarint(buffer, pos) + pos = message_end = message_start + size + elif tag_bytes == item_end_tag_bytes: + break + else: + pos = SkipField(buffer, pos, end, tag_bytes) + if pos == -1: + raise _DecodeError('Missing group end tag.') + + if pos > end: + raise _DecodeError('Truncated message.') + + if type_id == -1: + raise _DecodeError('MessageSet item missing type_id.') + if message_start == -1: + raise _DecodeError('MessageSet item missing message.') + + extension = message.Extensions._FindExtensionByNumber(type_id) + # pylint: disable=protected-access + if extension is not None: + value = field_dict.get(extension) + if value is None: + message_type = extension.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + message._FACTORY.GetPrototype(message_type) + value = field_dict.setdefault( + extension, message_type._concrete_class()) + if value._InternalParse(buffer, message_start,message_end) != message_end: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + type_id, + wire_format.WIRETYPE_LENGTH_DELIMITED, + buffer[message_start:message_end].tobytes()) + # pylint: enable=protected-access + + return pos + + return DecodeItem + +# -------------------------------------------------------------------- + +def MapDecoder(field_descriptor, new_default, is_message_map): + """Returns a decoder for a map field.""" + + key = field_descriptor + tag_bytes = encoder.TagBytes(field_descriptor.number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + local_DecodeVarint = _DecodeVarint + # Can't read _concrete_class yet; might not be initialized. + message_type = field_descriptor.message_type + + def DecodeMap(buffer, pos, end, message, field_dict): + submsg = message_type._concrete_class() + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + submsg.Clear() + if submsg._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + + if is_message_map: + value[submsg.key].CopyFrom(submsg.value) + else: + value[submsg.key] = submsg.value + + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + + return DecodeMap + +# -------------------------------------------------------------------- +# Optimization is not as heavy here because calls to SkipField() are rare, +# except for handling end-group tags. + +def _SkipVarint(buffer, pos, end): + """Skip a varint value. Returns the new position.""" + # Previously ord(buffer[pos]) raised IndexError when pos is out of range. + # With this code, ord(b'') raises TypeError. Both are handled in + # python_message.py to generate a 'Truncated message' error. + while ord(buffer[pos:pos+1].tobytes()) & 0x80: + pos += 1 + pos += 1 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + +def _SkipFixed64(buffer, pos, end): + """Skip a fixed64 value. Returns the new position.""" + + pos += 8 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed64(buffer, pos): + """Decode a fixed64.""" + new_pos = pos + 8 + return (struct.unpack(' end: + raise _DecodeError('Truncated message.') + return pos + + +def _SkipGroup(buffer, pos, end): + """Skip sub-group. Returns the new position.""" + + while 1: + (tag_bytes, pos) = ReadTag(buffer, pos) + new_pos = SkipField(buffer, pos, end, tag_bytes) + if new_pos == -1: + return pos + pos = new_pos + + +def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): + """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" + + unknown_field_set = containers.UnknownFieldSet() + while end_pos is None or pos < end_pos: + (tag_bytes, pos) = ReadTag(buffer, pos) + (tag, _) = _DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if wire_type == wire_format.WIRETYPE_END_GROUP: + break + (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + + return (unknown_field_set, pos) + + +def _DecodeUnknownField(buffer, pos, wire_type): + """Decode a unknown field. Returns the UnknownField and new position.""" + + if wire_type == wire_format.WIRETYPE_VARINT: + (data, pos) = _DecodeVarint(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED64: + (data, pos) = _DecodeFixed64(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED32: + (data, pos) = _DecodeFixed32(buffer, pos) + elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: + (size, pos) = _DecodeVarint(buffer, pos) + data = buffer[pos:pos+size].tobytes() + pos += size + elif wire_type == wire_format.WIRETYPE_START_GROUP: + (data, pos) = _DecodeUnknownFieldSet(buffer, pos) + elif wire_type == wire_format.WIRETYPE_END_GROUP: + return (0, -1) + else: + raise _DecodeError('Wrong wire type in tag.') + + return (data, pos) + + +def _EndGroup(buffer, pos, end): + """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" + + return -1 + + +def _SkipFixed32(buffer, pos, end): + """Skip a fixed32 value. Returns the new position.""" + + pos += 4 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed32(buffer, pos): + """Decode a fixed32.""" + + new_pos = pos + 4 + return (struct.unpack('B').pack + + def EncodeVarint(write, value, unused_deterministic=None): + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeVarint + + +def _SignedVarintEncoder(): + """Return an encoder for a basic signed varint value (does not include + tag).""" + + local_int2byte = struct.Struct('>B').pack + + def EncodeSignedVarint(write, value, unused_deterministic=None): + if value < 0: + value += (1 << 64) + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeSignedVarint + + +_EncodeVarint = _VarintEncoder() +_EncodeSignedVarint = _SignedVarintEncoder() + + +def _VarintBytes(value): + """Encode the given integer as a varint and return the bytes. This is only + called at startup time so it doesn't need to be fast.""" + + pieces = [] + _EncodeVarint(pieces.append, value, True) + return b"".join(pieces) + + +def TagBytes(field_number, wire_type): + """Encode the given tag and return the bytes. Only called at startup.""" + + return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) + +# -------------------------------------------------------------------- +# As with sizers (see above), we have a number of common encoder +# implementations. + + +def _SimpleEncoder(wire_type, encode_value, compute_value_size): + """Return a constructor for an encoder for fields of a particular type. + + Args: + wire_type: The field's wire type, for encoding tags. + encode_value: A function which encodes an individual value, e.g. + _EncodeVarint(). + compute_value_size: A function which computes the size of an individual + value, e.g. _VarintSize(). + """ + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(element) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, element, deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, element, deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, value, deterministic) + return EncodeField + + return SpecificEncoder + + +def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): + """Like SimpleEncoder but additionally invokes modify_value on every value + before passing it to encode_value. Usually modify_value is ZigZagEncode.""" + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(modify_value(element)) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, modify_value(element), deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, modify_value(element), deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, modify_value(value), deterministic) + return EncodeField + + return SpecificEncoder + + +def _StructPackEncoder(wire_type, format): + """Return a constructor for an encoder for a fixed-width field. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + write(local_struct_pack(format, element)) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + write(local_struct_pack(format, element)) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + return write(local_struct_pack(format, value)) + return EncodeField + + return SpecificEncoder + + +def _FloatingPointEncoder(wire_type, format): + """Return a constructor for an encoder for float fields. + + This is like StructPackEncoder, but catches errors that may be due to + passing non-finite floating-point values to struct.pack, and makes a + second attempt to encode those values. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + if value_size == 4: + def EncodeNonFiniteOrRaise(write, value): + # Remember that the serialized form uses little-endian byte order. + if value == _POS_INF: + write(b'\x00\x00\x80\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x80\xFF') + elif value != value: # NaN + write(b'\x00\x00\xC0\x7F') + else: + raise + elif value_size == 8: + def EncodeNonFiniteOrRaise(write, value): + if value == _POS_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') + elif value != value: # NaN + write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') + else: + raise + else: + raise ValueError('Can\'t encode floating-point values that are ' + '%d bytes long (only 4 or 8)' % value_size) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + # This try/except block is going to be faster than any code that + # we could write to check whether element is finite. + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + try: + write(local_struct_pack(format, value)) + except SystemError: + EncodeNonFiniteOrRaise(write, value) + return EncodeField + + return SpecificEncoder + + +# ==================================================================== +# Here we declare an encoder constructor for each field type. These work +# very similarly to sizer constructors, described earlier. + + +Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) + +UInt32Encoder = UInt64Encoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) + +SInt32Encoder = SInt64Encoder = _ModifiedEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, + wire_format.ZigZagEncode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str + ValueType = int + + def __init__(self, enum_type): + """Inits EnumTypeWrapper with an EnumDescriptor.""" + self._enum_type = enum_type + self.DESCRIPTOR = enum_type # pylint: disable=invalid-name + + def Name(self, number): # pylint: disable=invalid-name + """Returns a string containing the name of an enum value.""" + try: + return self._enum_type.values_by_number[number].name + except KeyError: + pass # fall out to break exception chaining + + if not isinstance(number, int): + raise TypeError( + 'Enum value for {} must be an int, but got {} {!r}.'.format( + self._enum_type.name, type(number), number)) + else: + # repr here to handle the odd case when you pass in a boolean. + raise ValueError('Enum {} has no name defined for value {!r}'.format( + self._enum_type.name, number)) + + def Value(self, name): # pylint: disable=invalid-name + """Returns the value corresponding to the given enum name.""" + try: + return self._enum_type.values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise ValueError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) + + def keys(self): + """Return a list of the string names in the enum. + + Returns: + A list of strs, in the order they were defined in the .proto file. + """ + + return [value_descriptor.name + for value_descriptor in self._enum_type.values] + + def values(self): + """Return a list of the integer values in the enum. + + Returns: + A list of ints, in the order they were defined in the .proto file. + """ + + return [value_descriptor.number + for value_descriptor in self._enum_type.values] + + def items(self): + """Return a list of the (name, value) pairs of the enum. + + Returns: + A list of (str, int) pairs, in the order they were defined + in the .proto file. + """ + return [(value_descriptor.name, value_descriptor.number) + for value_descriptor in self._enum_type.values] + + def __getattr__(self, name): + """Returns the value corresponding to the given enum name.""" + try: + return super( + EnumTypeWrapper, + self).__getattribute__('_enum_type').values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise AttributeError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py new file mode 100644 index 0000000000..b346cf283e --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py @@ -0,0 +1,213 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains _ExtensionDict class to represent extensions. +""" + +from google.protobuf.internal import type_checkers +from google.protobuf.descriptor import FieldDescriptor + + +def _VerifyExtensionHandle(message, extension_handle): + """Verify that the given extension handle is valid.""" + + if not isinstance(extension_handle, FieldDescriptor): + raise KeyError('HasExtension() expects an extension handle, got: %s' % + extension_handle) + + if not extension_handle.is_extension: + raise KeyError('"%s" is not an extension.' % extension_handle.full_name) + + if not extension_handle.containing_type: + raise KeyError('"%s" is missing a containing_type.' + % extension_handle.full_name) + + if extension_handle.containing_type is not message.DESCRIPTOR: + raise KeyError('Extension "%s" extends message type "%s", but this ' + 'message is of type "%s".' % + (extension_handle.full_name, + extension_handle.containing_type.full_name, + message.DESCRIPTOR.full_name)) + + +# TODO(robinson): Unify error handling of "unknown extension" crap. +# TODO(robinson): Support iteritems()-style iteration over all +# extensions with the "has" bits turned on? +class _ExtensionDict(object): + + """Dict-like container for Extension fields on proto instances. + + Note that in all cases we expect extension handles to be + FieldDescriptors. + """ + + def __init__(self, extended_message): + """ + Args: + extended_message: Message instance for which we are the Extensions dict. + """ + self._extended_message = extended_message + + def __getitem__(self, extension_handle): + """Returns the current value of the given extension handle.""" + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + result = self._extended_message._fields.get(extension_handle) + if result is not None: + return result + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + result = extension_handle._default_constructor(self._extended_message) + elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + message_type = extension_handle.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + self._extended_message._FACTORY.GetPrototype(message_type) + assert getattr(extension_handle.message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (extension_handle.full_name, + extension_handle.message_type.full_name)) + result = extension_handle.message_type._concrete_class() + try: + result._SetListener(self._extended_message._listener_for_children) + except ReferenceError: + pass + else: + # Singular scalar -- just return the default without inserting into the + # dict. + return extension_handle.default_value + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + result = self._extended_message._fields.setdefault( + extension_handle, result) + + return result + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + my_fields = self._extended_message.ListFields() + other_fields = other._extended_message.ListFields() + + # Get rid of non-extension fields. + my_fields = [field for field in my_fields if field.is_extension] + other_fields = [field for field in other_fields if field.is_extension] + + return my_fields == other_fields + + def __ne__(self, other): + return not self == other + + def __len__(self): + fields = self._extended_message.ListFields() + # Get rid of non-extension fields. + extension_fields = [field for field in fields if field[0].is_extension] + return len(extension_fields) + + def __hash__(self): + raise TypeError('unhashable object') + + # Note that this is only meaningful for non-repeated, scalar extension + # fields. Note also that we may have to call _Modified() when we do + # successfully set a field this way, to set any necessary "has" bits in the + # ancestors of the extended message. + def __setitem__(self, extension_handle, value): + """If extension_handle specifies a non-repeated, scalar extension + field, sets the value of that field. + """ + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or + extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): + raise TypeError( + 'Cannot assign to extension "%s" because it is a repeated or ' + 'composite type.' % extension_handle.full_name) + + # It's slightly wasteful to lookup the type checker each time, + # but we expect this to be a vanishingly uncommon case anyway. + type_checker = type_checkers.GetTypeChecker(extension_handle) + # pylint: disable=protected-access + self._extended_message._fields[extension_handle] = ( + type_checker.CheckValue(value)) + self._extended_message._Modified() + + def __delitem__(self, extension_handle): + self._extended_message.ClearExtension(extension_handle) + + def _FindExtensionByName(self, name): + """Tries to find a known extension with the specified name. + + Args: + name: Extension full name. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_name.get(name, None) + + def _FindExtensionByNumber(self, number): + """Tries to find a known extension with the field number. + + Args: + number: Extension field number. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_number.get(number, None) + + def __iter__(self): + # Return a generator over the populated extension fields + return (f[0] for f in self._extended_message.ListFields() + if f[0].is_extension) + + def __contains__(self, extension_handle): + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if extension_handle not in self._extended_message._fields: + return False + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + return bool(self._extended_message._fields.get(extension_handle)) + + if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + value = self._extended_message._fields.get(extension_handle) + # pylint: disable=protected-access + return value is not None and value._is_present_in_parent + + return True diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py new file mode 100644 index 0000000000..0fc255a774 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py @@ -0,0 +1,78 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Defines a listener interface for observing certain +state transitions on Message objects. + +Also defines a null implementation of this interface. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +class MessageListener(object): + + """Listens for modifications made to a message. Meant to be registered via + Message._SetListener(). + + Attributes: + dirty: If True, then calling Modified() would be a no-op. This can be + used to avoid these calls entirely in the common case. + """ + + def Modified(self): + """Called every time the message is modified in such a way that the parent + message may need to be updated. This currently means either: + (a) The message was modified for the first time, so the parent message + should henceforth mark the message as present. + (b) The message's cached byte size became dirty -- i.e. the message was + modified for the first time after a previous call to ByteSize(). + Therefore the parent should also mark its byte size as dirty. + Note that (a) implies (b), since new objects start out with a client cached + size (zero). However, we document (a) explicitly because it is important. + + Modified() will *only* be called in response to one of these two events -- + not every time the sub-message is modified. + + Note that if the listener's |dirty| attribute is true, then calling + Modified at the moment would be a no-op, so it can be skipped. Performance- + sensitive callers should check this attribute directly before calling since + it will be true most of the time. + """ + + raise NotImplementedError + + +class NullMessageListener(object): + + """No-op MessageListener implementation.""" + + def Modified(self): + pass diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py new file mode 100644 index 0000000000..63651a3f19 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/message_set_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5google/protobuf/internal/message_set_extensions.proto\x12\x18google.protobuf.internal\"\x1e\n\x0eTestMessageSet*\x08\x08\x04\x10\xff\xff\xff\xff\x07:\x02\x08\x01\"\xa5\x01\n\x18TestMessageSetExtension1\x12\t\n\x01i\x18\x0f \x01(\x05\x32~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xab\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension1\"\xa7\x01\n\x18TestMessageSetExtension2\x12\x0b\n\x03str\x18\x19 \x01(\t2~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xca\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension2\"(\n\x18TestMessageSetExtension3\x12\x0c\n\x04text\x18# \x01(\t:\x7f\n\x16message_set_extension3\x12(.google.protobuf.internal.TestMessageSet\x18\xdf\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageSet.RegisterExtension(message_set_extension3) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension']) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension']) + + DESCRIPTOR._options = None + _TESTMESSAGESET._options = None + _TESTMESSAGESET._serialized_options = b'\010\001' + _TESTMESSAGESET._serialized_start=83 + _TESTMESSAGESET._serialized_end=113 + _TESTMESSAGESETEXTENSION1._serialized_start=116 + _TESTMESSAGESETEXTENSION1._serialized_end=281 + _TESTMESSAGESETEXTENSION2._serialized_start=284 + _TESTMESSAGESETEXTENSION2._serialized_end=451 + _TESTMESSAGESETEXTENSION3._serialized_start=453 + _TESTMESSAGESETEXTENSION3._serialized_end=493 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py new file mode 100644 index 0000000000..5497083197 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/missing_enum_values.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2google/protobuf/internal/missing_enum_values.proto\x12\x1fgoogle.protobuf.python.internal\"\xc1\x02\n\x0eTestEnumValues\x12X\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12X\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12Z\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\x02\x10\x01\"\x1f\n\nNestedEnum\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\"\xd3\x02\n\x15TestMissingEnumValues\x12_\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12_\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12\x61\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\x02\x10\x01\"\x15\n\nNestedEnum\x12\x07\n\x03TWO\x10\x02\"\x1b\n\nJustString\x12\r\n\x05\x64ummy\x18\x01 \x02(\t') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTENUMVALUES._serialized_start=88 + _TESTENUMVALUES._serialized_end=409 + _TESTENUMVALUES_NESTEDENUM._serialized_start=378 + _TESTENUMVALUES_NESTEDENUM._serialized_end=409 + _TESTMISSINGENUMVALUES._serialized_start=412 + _TESTMISSINGENUMVALUES._serialized_end=751 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751 + _JUSTSTRING._serialized_start=753 + _JUSTSTRING._serialized_end=780 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py new file mode 100644 index 0000000000..0953706bac --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions_dynamic.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6google/protobuf/internal/more_extensions_dynamic.proto\x12\x18google.protobuf.internal\x1a.google/protobuf/internal/more_extensions.proto\"\x1f\n\x12\x44ynamicMessageType\x12\t\n\x01\x61\x18\x01 \x01(\x05:J\n\x17\x64ynamic_int32_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x64 \x01(\x05:z\n\x19\x64ynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x65 \x01(\x0b\x32,.google.protobuf.internal.DynamicMessageType:\x83\x01\n\"repeated_dynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x66 \x03(\x0b\x32,.google.protobuf.internal.DynamicMessageType') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension) + + DESCRIPTOR._options = None + _DYNAMICMESSAGETYPE._serialized_start=132 + _DYNAMICMESSAGETYPE._serialized_end=163 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py new file mode 100644 index 0000000000..1cfa1b7c8b --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.google/protobuf/internal/more_extensions.proto\x12\x18google.protobuf.internal\"\x99\x01\n\x0fTopLevelMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\x12\x43\n\x0enested_message\x18\x02 \x01(\x0b\x32\'.google.protobuf.internal.NestedMessageB\x02(\x01\"R\n\rNestedMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\"K\n\x0f\x45xtendedMessage\x12\x17\n\x0eoptional_int32\x18\xe9\x07 \x01(\x05\x12\x18\n\x0frepeated_string\x18\xea\x07 \x03(\t*\x05\x08\x01\x10\xe8\x07\"-\n\x0e\x46oreignMessage\x12\x1b\n\x13\x66oreign_message_int\x18\x01 \x01(\x05:I\n\x16optional_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x01 \x01(\x05:w\n\x1aoptional_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x02 \x01(\x0b\x32(.google.protobuf.internal.ForeignMessage:I\n\x16repeated_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x03 \x03(\x05:w\n\x1arepeated_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x04 \x03(\x0b\x32(.google.protobuf.internal.ForeignMessage') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + ExtendedMessage.RegisterExtension(optional_int_extension) + ExtendedMessage.RegisterExtension(optional_message_extension) + ExtendedMessage.RegisterExtension(repeated_int_extension) + ExtendedMessage.RegisterExtension(repeated_message_extension) + + DESCRIPTOR._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None + _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\001' + _NESTEDMESSAGE.fields_by_name['submessage']._options = None + _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE._serialized_start=77 + _TOPLEVELMESSAGE._serialized_end=230 + _NESTEDMESSAGE._serialized_start=232 + _NESTEDMESSAGE._serialized_end=314 + _EXTENDEDMESSAGE._serialized_start=316 + _EXTENDEDMESSAGE._serialized_end=391 + _FOREIGNMESSAGE._serialized_start=393 + _FOREIGNMESSAGE._serialized_end=438 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py new file mode 100644 index 0000000000..d7f7115609 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py @@ -0,0 +1,556 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_messages.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,google/protobuf/internal/more_messages.proto\x12\x18google.protobuf.internal\"h\n\x10OutOfOrderFields\x12\x17\n\x0foptional_sint32\x18\x05 \x01(\x11\x12\x17\n\x0foptional_uint32\x18\x03 \x01(\r\x12\x16\n\x0eoptional_int32\x18\x01 \x01(\x05*\x04\x08\x04\x10\x05*\x04\x08\x02\x10\x03\"\xcd\x02\n\x05\x63lass\x12\x1b\n\tint_field\x18\x01 \x01(\x05R\x08json_int\x12\n\n\x02if\x18\x02 \x01(\x05\x12(\n\x02\x61s\x18\x03 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12\x30\n\nenum_field\x18\x04 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12>\n\x11nested_enum_field\x18\x05 \x01(\x0e\x32#.google.protobuf.internal.class.for\x12;\n\x0enested_message\x18\x06 \x01(\x0b\x32#.google.protobuf.internal.class.try\x1a\x1c\n\x03try\x12\r\n\x05\x66ield\x18\x01 \x01(\x05*\x06\x08\xe7\x07\x10\x90N\"\x1c\n\x03\x66or\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04True\x10\x01*\x06\x08\xe7\x07\x10\x90N\"?\n\x0b\x45xtendClass20\n\x06return\x12\x1f.google.protobuf.internal.class\x18\xea\x07 \x01(\x05\"~\n\x0fTestFullKeyword\x12:\n\x06\x66ield1\x18\x01 \x01(\x0b\x32*.google.protobuf.internal.OutOfOrderFields\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.google.protobuf.internal.class\"\xa5\x0f\n\x11LotsNestedMessage\x1a\x04\n\x02\x42\x30\x1a\x04\n\x02\x42\x31\x1a\x04\n\x02\x42\x32\x1a\x04\n\x02\x42\x33\x1a\x04\n\x02\x42\x34\x1a\x04\n\x02\x42\x35\x1a\x04\n\x02\x42\x36\x1a\x04\n\x02\x42\x37\x1a\x04\n\x02\x42\x38\x1a\x04\n\x02\x42\x39\x1a\x05\n\x03\x42\x31\x30\x1a\x05\n\x03\x42\x31\x31\x1a\x05\n\x03\x42\x31\x32\x1a\x05\n\x03\x42\x31\x33\x1a\x05\n\x03\x42\x31\x34\x1a\x05\n\x03\x42\x31\x35\x1a\x05\n\x03\x42\x31\x36\x1a\x05\n\x03\x42\x31\x37\x1a\x05\n\x03\x42\x31\x38\x1a\x05\n\x03\x42\x31\x39\x1a\x05\n\x03\x42\x32\x30\x1a\x05\n\x03\x42\x32\x31\x1a\x05\n\x03\x42\x32\x32\x1a\x05\n\x03\x42\x32\x33\x1a\x05\n\x03\x42\x32\x34\x1a\x05\n\x03\x42\x32\x35\x1a\x05\n\x03\x42\x32\x36\x1a\x05\n\x03\x42\x32\x37\x1a\x05\n\x03\x42\x32\x38\x1a\x05\n\x03\x42\x32\x39\x1a\x05\n\x03\x42\x33\x30\x1a\x05\n\x03\x42\x33\x31\x1a\x05\n\x03\x42\x33\x32\x1a\x05\n\x03\x42\x33\x33\x1a\x05\n\x03\x42\x33\x34\x1a\x05\n\x03\x42\x33\x35\x1a\x05\n\x03\x42\x33\x36\x1a\x05\n\x03\x42\x33\x37\x1a\x05\n\x03\x42\x33\x38\x1a\x05\n\x03\x42\x33\x39\x1a\x05\n\x03\x42\x34\x30\x1a\x05\n\x03\x42\x34\x31\x1a\x05\n\x03\x42\x34\x32\x1a\x05\n\x03\x42\x34\x33\x1a\x05\n\x03\x42\x34\x34\x1a\x05\n\x03\x42\x34\x35\x1a\x05\n\x03\x42\x34\x36\x1a\x05\n\x03\x42\x34\x37\x1a\x05\n\x03\x42\x34\x38\x1a\x05\n\x03\x42\x34\x39\x1a\x05\n\x03\x42\x35\x30\x1a\x05\n\x03\x42\x35\x31\x1a\x05\n\x03\x42\x35\x32\x1a\x05\n\x03\x42\x35\x33\x1a\x05\n\x03\x42\x35\x34\x1a\x05\n\x03\x42\x35\x35\x1a\x05\n\x03\x42\x35\x36\x1a\x05\n\x03\x42\x35\x37\x1a\x05\n\x03\x42\x35\x38\x1a\x05\n\x03\x42\x35\x39\x1a\x05\n\x03\x42\x36\x30\x1a\x05\n\x03\x42\x36\x31\x1a\x05\n\x03\x42\x36\x32\x1a\x05\n\x03\x42\x36\x33\x1a\x05\n\x03\x42\x36\x34\x1a\x05\n\x03\x42\x36\x35\x1a\x05\n\x03\x42\x36\x36\x1a\x05\n\x03\x42\x36\x37\x1a\x05\n\x03\x42\x36\x38\x1a\x05\n\x03\x42\x36\x39\x1a\x05\n\x03\x42\x37\x30\x1a\x05\n\x03\x42\x37\x31\x1a\x05\n\x03\x42\x37\x32\x1a\x05\n\x03\x42\x37\x33\x1a\x05\n\x03\x42\x37\x34\x1a\x05\n\x03\x42\x37\x35\x1a\x05\n\x03\x42\x37\x36\x1a\x05\n\x03\x42\x37\x37\x1a\x05\n\x03\x42\x37\x38\x1a\x05\n\x03\x42\x37\x39\x1a\x05\n\x03\x42\x38\x30\x1a\x05\n\x03\x42\x38\x31\x1a\x05\n\x03\x42\x38\x32\x1a\x05\n\x03\x42\x38\x33\x1a\x05\n\x03\x42\x38\x34\x1a\x05\n\x03\x42\x38\x35\x1a\x05\n\x03\x42\x38\x36\x1a\x05\n\x03\x42\x38\x37\x1a\x05\n\x03\x42\x38\x38\x1a\x05\n\x03\x42\x38\x39\x1a\x05\n\x03\x42\x39\x30\x1a\x05\n\x03\x42\x39\x31\x1a\x05\n\x03\x42\x39\x32\x1a\x05\n\x03\x42\x39\x33\x1a\x05\n\x03\x42\x39\x34\x1a\x05\n\x03\x42\x39\x35\x1a\x05\n\x03\x42\x39\x36\x1a\x05\n\x03\x42\x39\x37\x1a\x05\n\x03\x42\x39\x38\x1a\x05\n\x03\x42\x39\x39\x1a\x06\n\x04\x42\x31\x30\x30\x1a\x06\n\x04\x42\x31\x30\x31\x1a\x06\n\x04\x42\x31\x30\x32\x1a\x06\n\x04\x42\x31\x30\x33\x1a\x06\n\x04\x42\x31\x30\x34\x1a\x06\n\x04\x42\x31\x30\x35\x1a\x06\n\x04\x42\x31\x30\x36\x1a\x06\n\x04\x42\x31\x30\x37\x1a\x06\n\x04\x42\x31\x30\x38\x1a\x06\n\x04\x42\x31\x30\x39\x1a\x06\n\x04\x42\x31\x31\x30\x1a\x06\n\x04\x42\x31\x31\x31\x1a\x06\n\x04\x42\x31\x31\x32\x1a\x06\n\x04\x42\x31\x31\x33\x1a\x06\n\x04\x42\x31\x31\x34\x1a\x06\n\x04\x42\x31\x31\x35\x1a\x06\n\x04\x42\x31\x31\x36\x1a\x06\n\x04\x42\x31\x31\x37\x1a\x06\n\x04\x42\x31\x31\x38\x1a\x06\n\x04\x42\x31\x31\x39\x1a\x06\n\x04\x42\x31\x32\x30\x1a\x06\n\x04\x42\x31\x32\x31\x1a\x06\n\x04\x42\x31\x32\x32\x1a\x06\n\x04\x42\x31\x32\x33\x1a\x06\n\x04\x42\x31\x32\x34\x1a\x06\n\x04\x42\x31\x32\x35\x1a\x06\n\x04\x42\x31\x32\x36\x1a\x06\n\x04\x42\x31\x32\x37\x1a\x06\n\x04\x42\x31\x32\x38\x1a\x06\n\x04\x42\x31\x32\x39\x1a\x06\n\x04\x42\x31\x33\x30\x1a\x06\n\x04\x42\x31\x33\x31\x1a\x06\n\x04\x42\x31\x33\x32\x1a\x06\n\x04\x42\x31\x33\x33\x1a\x06\n\x04\x42\x31\x33\x34\x1a\x06\n\x04\x42\x31\x33\x35\x1a\x06\n\x04\x42\x31\x33\x36\x1a\x06\n\x04\x42\x31\x33\x37\x1a\x06\n\x04\x42\x31\x33\x38\x1a\x06\n\x04\x42\x31\x33\x39\x1a\x06\n\x04\x42\x31\x34\x30\x1a\x06\n\x04\x42\x31\x34\x31\x1a\x06\n\x04\x42\x31\x34\x32\x1a\x06\n\x04\x42\x31\x34\x33\x1a\x06\n\x04\x42\x31\x34\x34\x1a\x06\n\x04\x42\x31\x34\x35\x1a\x06\n\x04\x42\x31\x34\x36\x1a\x06\n\x04\x42\x31\x34\x37\x1a\x06\n\x04\x42\x31\x34\x38\x1a\x06\n\x04\x42\x31\x34\x39\x1a\x06\n\x04\x42\x31\x35\x30\x1a\x06\n\x04\x42\x31\x35\x31\x1a\x06\n\x04\x42\x31\x35\x32\x1a\x06\n\x04\x42\x31\x35\x33\x1a\x06\n\x04\x42\x31\x35\x34\x1a\x06\n\x04\x42\x31\x35\x35\x1a\x06\n\x04\x42\x31\x35\x36\x1a\x06\n\x04\x42\x31\x35\x37\x1a\x06\n\x04\x42\x31\x35\x38\x1a\x06\n\x04\x42\x31\x35\x39\x1a\x06\n\x04\x42\x31\x36\x30\x1a\x06\n\x04\x42\x31\x36\x31\x1a\x06\n\x04\x42\x31\x36\x32\x1a\x06\n\x04\x42\x31\x36\x33\x1a\x06\n\x04\x42\x31\x36\x34\x1a\x06\n\x04\x42\x31\x36\x35\x1a\x06\n\x04\x42\x31\x36\x36\x1a\x06\n\x04\x42\x31\x36\x37\x1a\x06\n\x04\x42\x31\x36\x38\x1a\x06\n\x04\x42\x31\x36\x39\x1a\x06\n\x04\x42\x31\x37\x30\x1a\x06\n\x04\x42\x31\x37\x31\x1a\x06\n\x04\x42\x31\x37\x32\x1a\x06\n\x04\x42\x31\x37\x33\x1a\x06\n\x04\x42\x31\x37\x34\x1a\x06\n\x04\x42\x31\x37\x35\x1a\x06\n\x04\x42\x31\x37\x36\x1a\x06\n\x04\x42\x31\x37\x37\x1a\x06\n\x04\x42\x31\x37\x38\x1a\x06\n\x04\x42\x31\x37\x39\x1a\x06\n\x04\x42\x31\x38\x30\x1a\x06\n\x04\x42\x31\x38\x31\x1a\x06\n\x04\x42\x31\x38\x32\x1a\x06\n\x04\x42\x31\x38\x33\x1a\x06\n\x04\x42\x31\x38\x34\x1a\x06\n\x04\x42\x31\x38\x35\x1a\x06\n\x04\x42\x31\x38\x36\x1a\x06\n\x04\x42\x31\x38\x37\x1a\x06\n\x04\x42\x31\x38\x38\x1a\x06\n\x04\x42\x31\x38\x39\x1a\x06\n\x04\x42\x31\x39\x30\x1a\x06\n\x04\x42\x31\x39\x31\x1a\x06\n\x04\x42\x31\x39\x32\x1a\x06\n\x04\x42\x31\x39\x33\x1a\x06\n\x04\x42\x31\x39\x34\x1a\x06\n\x04\x42\x31\x39\x35\x1a\x06\n\x04\x42\x31\x39\x36\x1a\x06\n\x04\x42\x31\x39\x37\x1a\x06\n\x04\x42\x31\x39\x38\x1a\x06\n\x04\x42\x31\x39\x39\x1a\x06\n\x04\x42\x32\x30\x30\x1a\x06\n\x04\x42\x32\x30\x31\x1a\x06\n\x04\x42\x32\x30\x32\x1a\x06\n\x04\x42\x32\x30\x33\x1a\x06\n\x04\x42\x32\x30\x34\x1a\x06\n\x04\x42\x32\x30\x35\x1a\x06\n\x04\x42\x32\x30\x36\x1a\x06\n\x04\x42\x32\x30\x37\x1a\x06\n\x04\x42\x32\x30\x38\x1a\x06\n\x04\x42\x32\x30\x39\x1a\x06\n\x04\x42\x32\x31\x30\x1a\x06\n\x04\x42\x32\x31\x31\x1a\x06\n\x04\x42\x32\x31\x32\x1a\x06\n\x04\x42\x32\x31\x33\x1a\x06\n\x04\x42\x32\x31\x34\x1a\x06\n\x04\x42\x32\x31\x35\x1a\x06\n\x04\x42\x32\x31\x36\x1a\x06\n\x04\x42\x32\x31\x37\x1a\x06\n\x04\x42\x32\x31\x38\x1a\x06\n\x04\x42\x32\x31\x39\x1a\x06\n\x04\x42\x32\x32\x30\x1a\x06\n\x04\x42\x32\x32\x31\x1a\x06\n\x04\x42\x32\x32\x32\x1a\x06\n\x04\x42\x32\x32\x33\x1a\x06\n\x04\x42\x32\x32\x34\x1a\x06\n\x04\x42\x32\x32\x35\x1a\x06\n\x04\x42\x32\x32\x36\x1a\x06\n\x04\x42\x32\x32\x37\x1a\x06\n\x04\x42\x32\x32\x38\x1a\x06\n\x04\x42\x32\x32\x39\x1a\x06\n\x04\x42\x32\x33\x30\x1a\x06\n\x04\x42\x32\x33\x31\x1a\x06\n\x04\x42\x32\x33\x32\x1a\x06\n\x04\x42\x32\x33\x33\x1a\x06\n\x04\x42\x32\x33\x34\x1a\x06\n\x04\x42\x32\x33\x35\x1a\x06\n\x04\x42\x32\x33\x36\x1a\x06\n\x04\x42\x32\x33\x37\x1a\x06\n\x04\x42\x32\x33\x38\x1a\x06\n\x04\x42\x32\x33\x39\x1a\x06\n\x04\x42\x32\x34\x30\x1a\x06\n\x04\x42\x32\x34\x31\x1a\x06\n\x04\x42\x32\x34\x32\x1a\x06\n\x04\x42\x32\x34\x33\x1a\x06\n\x04\x42\x32\x34\x34\x1a\x06\n\x04\x42\x32\x34\x35\x1a\x06\n\x04\x42\x32\x34\x36\x1a\x06\n\x04\x42\x32\x34\x37\x1a\x06\n\x04\x42\x32\x34\x38\x1a\x06\n\x04\x42\x32\x34\x39\x1a\x06\n\x04\x42\x32\x35\x30\x1a\x06\n\x04\x42\x32\x35\x31\x1a\x06\n\x04\x42\x32\x35\x32\x1a\x06\n\x04\x42\x32\x35\x33\x1a\x06\n\x04\x42\x32\x35\x34\x1a\x06\n\x04\x42\x32\x35\x35*\x1b\n\x02is\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04\x65lse\x10\x01:C\n\x0foptional_uint64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x04 \x01(\x04:B\n\x0eoptional_int64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x02 \x01(\x03:2\n\x08\x63ontinue\x12\x1f.google.protobuf.internal.class\x18\xe9\x07 \x01(\x05:2\n\x04with\x12#.google.protobuf.internal.class.try\x18\xe9\x07 \x01(\x05') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + OutOfOrderFields.RegisterExtension(optional_uint64) + OutOfOrderFields.RegisterExtension(optional_int64) + globals()['class'].RegisterExtension(globals()['continue']) + getattr(globals()['class'], 'try').RegisterExtension(globals()['with']) + globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return']) + + DESCRIPTOR._options = None + _IS._serialized_start=2669 + _IS._serialized_end=2696 + _OUTOFORDERFIELDS._serialized_start=74 + _OUTOFORDERFIELDS._serialized_end=178 + _CLASS._serialized_start=181 + _CLASS._serialized_end=514 + _CLASS_TRY._serialized_start=448 + _CLASS_TRY._serialized_end=476 + _CLASS_FOR._serialized_start=478 + _CLASS_FOR._serialized_end=506 + _EXTENDCLASS._serialized_start=516 + _EXTENDCLASS._serialized_end=579 + _TESTFULLKEYWORD._serialized_start=581 + _TESTFULLKEYWORD._serialized_end=707 + _LOTSNESTEDMESSAGE._serialized_start=710 + _LOTSNESTEDMESSAGE._serialized_end=2667 + _LOTSNESTEDMESSAGE_B0._serialized_start=731 + _LOTSNESTEDMESSAGE_B0._serialized_end=735 + _LOTSNESTEDMESSAGE_B1._serialized_start=737 + _LOTSNESTEDMESSAGE_B1._serialized_end=741 + _LOTSNESTEDMESSAGE_B2._serialized_start=743 + _LOTSNESTEDMESSAGE_B2._serialized_end=747 + _LOTSNESTEDMESSAGE_B3._serialized_start=749 + _LOTSNESTEDMESSAGE_B3._serialized_end=753 + _LOTSNESTEDMESSAGE_B4._serialized_start=755 + _LOTSNESTEDMESSAGE_B4._serialized_end=759 + _LOTSNESTEDMESSAGE_B5._serialized_start=761 + _LOTSNESTEDMESSAGE_B5._serialized_end=765 + _LOTSNESTEDMESSAGE_B6._serialized_start=767 + _LOTSNESTEDMESSAGE_B6._serialized_end=771 + _LOTSNESTEDMESSAGE_B7._serialized_start=773 + _LOTSNESTEDMESSAGE_B7._serialized_end=777 + _LOTSNESTEDMESSAGE_B8._serialized_start=779 + _LOTSNESTEDMESSAGE_B8._serialized_end=783 + _LOTSNESTEDMESSAGE_B9._serialized_start=785 + _LOTSNESTEDMESSAGE_B9._serialized_end=789 + _LOTSNESTEDMESSAGE_B10._serialized_start=791 + _LOTSNESTEDMESSAGE_B10._serialized_end=796 + _LOTSNESTEDMESSAGE_B11._serialized_start=798 + _LOTSNESTEDMESSAGE_B11._serialized_end=803 + _LOTSNESTEDMESSAGE_B12._serialized_start=805 + _LOTSNESTEDMESSAGE_B12._serialized_end=810 + _LOTSNESTEDMESSAGE_B13._serialized_start=812 + _LOTSNESTEDMESSAGE_B13._serialized_end=817 + _LOTSNESTEDMESSAGE_B14._serialized_start=819 + _LOTSNESTEDMESSAGE_B14._serialized_end=824 + _LOTSNESTEDMESSAGE_B15._serialized_start=826 + _LOTSNESTEDMESSAGE_B15._serialized_end=831 + _LOTSNESTEDMESSAGE_B16._serialized_start=833 + _LOTSNESTEDMESSAGE_B16._serialized_end=838 + _LOTSNESTEDMESSAGE_B17._serialized_start=840 + _LOTSNESTEDMESSAGE_B17._serialized_end=845 + _LOTSNESTEDMESSAGE_B18._serialized_start=847 + _LOTSNESTEDMESSAGE_B18._serialized_end=852 + _LOTSNESTEDMESSAGE_B19._serialized_start=854 + _LOTSNESTEDMESSAGE_B19._serialized_end=859 + _LOTSNESTEDMESSAGE_B20._serialized_start=861 + _LOTSNESTEDMESSAGE_B20._serialized_end=866 + _LOTSNESTEDMESSAGE_B21._serialized_start=868 + _LOTSNESTEDMESSAGE_B21._serialized_end=873 + _LOTSNESTEDMESSAGE_B22._serialized_start=875 + _LOTSNESTEDMESSAGE_B22._serialized_end=880 + _LOTSNESTEDMESSAGE_B23._serialized_start=882 + _LOTSNESTEDMESSAGE_B23._serialized_end=887 + _LOTSNESTEDMESSAGE_B24._serialized_start=889 + _LOTSNESTEDMESSAGE_B24._serialized_end=894 + _LOTSNESTEDMESSAGE_B25._serialized_start=896 + _LOTSNESTEDMESSAGE_B25._serialized_end=901 + _LOTSNESTEDMESSAGE_B26._serialized_start=903 + _LOTSNESTEDMESSAGE_B26._serialized_end=908 + _LOTSNESTEDMESSAGE_B27._serialized_start=910 + _LOTSNESTEDMESSAGE_B27._serialized_end=915 + _LOTSNESTEDMESSAGE_B28._serialized_start=917 + _LOTSNESTEDMESSAGE_B28._serialized_end=922 + _LOTSNESTEDMESSAGE_B29._serialized_start=924 + _LOTSNESTEDMESSAGE_B29._serialized_end=929 + _LOTSNESTEDMESSAGE_B30._serialized_start=931 + _LOTSNESTEDMESSAGE_B30._serialized_end=936 + _LOTSNESTEDMESSAGE_B31._serialized_start=938 + _LOTSNESTEDMESSAGE_B31._serialized_end=943 + _LOTSNESTEDMESSAGE_B32._serialized_start=945 + _LOTSNESTEDMESSAGE_B32._serialized_end=950 + _LOTSNESTEDMESSAGE_B33._serialized_start=952 + _LOTSNESTEDMESSAGE_B33._serialized_end=957 + _LOTSNESTEDMESSAGE_B34._serialized_start=959 + _LOTSNESTEDMESSAGE_B34._serialized_end=964 + _LOTSNESTEDMESSAGE_B35._serialized_start=966 + _LOTSNESTEDMESSAGE_B35._serialized_end=971 + _LOTSNESTEDMESSAGE_B36._serialized_start=973 + _LOTSNESTEDMESSAGE_B36._serialized_end=978 + _LOTSNESTEDMESSAGE_B37._serialized_start=980 + _LOTSNESTEDMESSAGE_B37._serialized_end=985 + _LOTSNESTEDMESSAGE_B38._serialized_start=987 + _LOTSNESTEDMESSAGE_B38._serialized_end=992 + _LOTSNESTEDMESSAGE_B39._serialized_start=994 + _LOTSNESTEDMESSAGE_B39._serialized_end=999 + _LOTSNESTEDMESSAGE_B40._serialized_start=1001 + _LOTSNESTEDMESSAGE_B40._serialized_end=1006 + _LOTSNESTEDMESSAGE_B41._serialized_start=1008 + _LOTSNESTEDMESSAGE_B41._serialized_end=1013 + _LOTSNESTEDMESSAGE_B42._serialized_start=1015 + _LOTSNESTEDMESSAGE_B42._serialized_end=1020 + _LOTSNESTEDMESSAGE_B43._serialized_start=1022 + _LOTSNESTEDMESSAGE_B43._serialized_end=1027 + _LOTSNESTEDMESSAGE_B44._serialized_start=1029 + _LOTSNESTEDMESSAGE_B44._serialized_end=1034 + _LOTSNESTEDMESSAGE_B45._serialized_start=1036 + _LOTSNESTEDMESSAGE_B45._serialized_end=1041 + _LOTSNESTEDMESSAGE_B46._serialized_start=1043 + _LOTSNESTEDMESSAGE_B46._serialized_end=1048 + _LOTSNESTEDMESSAGE_B47._serialized_start=1050 + _LOTSNESTEDMESSAGE_B47._serialized_end=1055 + _LOTSNESTEDMESSAGE_B48._serialized_start=1057 + _LOTSNESTEDMESSAGE_B48._serialized_end=1062 + _LOTSNESTEDMESSAGE_B49._serialized_start=1064 + _LOTSNESTEDMESSAGE_B49._serialized_end=1069 + _LOTSNESTEDMESSAGE_B50._serialized_start=1071 + _LOTSNESTEDMESSAGE_B50._serialized_end=1076 + _LOTSNESTEDMESSAGE_B51._serialized_start=1078 + _LOTSNESTEDMESSAGE_B51._serialized_end=1083 + _LOTSNESTEDMESSAGE_B52._serialized_start=1085 + _LOTSNESTEDMESSAGE_B52._serialized_end=1090 + _LOTSNESTEDMESSAGE_B53._serialized_start=1092 + _LOTSNESTEDMESSAGE_B53._serialized_end=1097 + _LOTSNESTEDMESSAGE_B54._serialized_start=1099 + _LOTSNESTEDMESSAGE_B54._serialized_end=1104 + _LOTSNESTEDMESSAGE_B55._serialized_start=1106 + _LOTSNESTEDMESSAGE_B55._serialized_end=1111 + _LOTSNESTEDMESSAGE_B56._serialized_start=1113 + _LOTSNESTEDMESSAGE_B56._serialized_end=1118 + _LOTSNESTEDMESSAGE_B57._serialized_start=1120 + _LOTSNESTEDMESSAGE_B57._serialized_end=1125 + _LOTSNESTEDMESSAGE_B58._serialized_start=1127 + _LOTSNESTEDMESSAGE_B58._serialized_end=1132 + _LOTSNESTEDMESSAGE_B59._serialized_start=1134 + _LOTSNESTEDMESSAGE_B59._serialized_end=1139 + _LOTSNESTEDMESSAGE_B60._serialized_start=1141 + _LOTSNESTEDMESSAGE_B60._serialized_end=1146 + _LOTSNESTEDMESSAGE_B61._serialized_start=1148 + _LOTSNESTEDMESSAGE_B61._serialized_end=1153 + _LOTSNESTEDMESSAGE_B62._serialized_start=1155 + _LOTSNESTEDMESSAGE_B62._serialized_end=1160 + _LOTSNESTEDMESSAGE_B63._serialized_start=1162 + _LOTSNESTEDMESSAGE_B63._serialized_end=1167 + _LOTSNESTEDMESSAGE_B64._serialized_start=1169 + _LOTSNESTEDMESSAGE_B64._serialized_end=1174 + _LOTSNESTEDMESSAGE_B65._serialized_start=1176 + _LOTSNESTEDMESSAGE_B65._serialized_end=1181 + _LOTSNESTEDMESSAGE_B66._serialized_start=1183 + _LOTSNESTEDMESSAGE_B66._serialized_end=1188 + _LOTSNESTEDMESSAGE_B67._serialized_start=1190 + _LOTSNESTEDMESSAGE_B67._serialized_end=1195 + _LOTSNESTEDMESSAGE_B68._serialized_start=1197 + _LOTSNESTEDMESSAGE_B68._serialized_end=1202 + _LOTSNESTEDMESSAGE_B69._serialized_start=1204 + _LOTSNESTEDMESSAGE_B69._serialized_end=1209 + _LOTSNESTEDMESSAGE_B70._serialized_start=1211 + _LOTSNESTEDMESSAGE_B70._serialized_end=1216 + _LOTSNESTEDMESSAGE_B71._serialized_start=1218 + _LOTSNESTEDMESSAGE_B71._serialized_end=1223 + _LOTSNESTEDMESSAGE_B72._serialized_start=1225 + _LOTSNESTEDMESSAGE_B72._serialized_end=1230 + _LOTSNESTEDMESSAGE_B73._serialized_start=1232 + _LOTSNESTEDMESSAGE_B73._serialized_end=1237 + _LOTSNESTEDMESSAGE_B74._serialized_start=1239 + _LOTSNESTEDMESSAGE_B74._serialized_end=1244 + _LOTSNESTEDMESSAGE_B75._serialized_start=1246 + _LOTSNESTEDMESSAGE_B75._serialized_end=1251 + _LOTSNESTEDMESSAGE_B76._serialized_start=1253 + _LOTSNESTEDMESSAGE_B76._serialized_end=1258 + _LOTSNESTEDMESSAGE_B77._serialized_start=1260 + _LOTSNESTEDMESSAGE_B77._serialized_end=1265 + _LOTSNESTEDMESSAGE_B78._serialized_start=1267 + _LOTSNESTEDMESSAGE_B78._serialized_end=1272 + _LOTSNESTEDMESSAGE_B79._serialized_start=1274 + _LOTSNESTEDMESSAGE_B79._serialized_end=1279 + _LOTSNESTEDMESSAGE_B80._serialized_start=1281 + _LOTSNESTEDMESSAGE_B80._serialized_end=1286 + _LOTSNESTEDMESSAGE_B81._serialized_start=1288 + _LOTSNESTEDMESSAGE_B81._serialized_end=1293 + _LOTSNESTEDMESSAGE_B82._serialized_start=1295 + _LOTSNESTEDMESSAGE_B82._serialized_end=1300 + _LOTSNESTEDMESSAGE_B83._serialized_start=1302 + _LOTSNESTEDMESSAGE_B83._serialized_end=1307 + _LOTSNESTEDMESSAGE_B84._serialized_start=1309 + _LOTSNESTEDMESSAGE_B84._serialized_end=1314 + _LOTSNESTEDMESSAGE_B85._serialized_start=1316 + _LOTSNESTEDMESSAGE_B85._serialized_end=1321 + _LOTSNESTEDMESSAGE_B86._serialized_start=1323 + _LOTSNESTEDMESSAGE_B86._serialized_end=1328 + _LOTSNESTEDMESSAGE_B87._serialized_start=1330 + _LOTSNESTEDMESSAGE_B87._serialized_end=1335 + _LOTSNESTEDMESSAGE_B88._serialized_start=1337 + _LOTSNESTEDMESSAGE_B88._serialized_end=1342 + _LOTSNESTEDMESSAGE_B89._serialized_start=1344 + _LOTSNESTEDMESSAGE_B89._serialized_end=1349 + _LOTSNESTEDMESSAGE_B90._serialized_start=1351 + _LOTSNESTEDMESSAGE_B90._serialized_end=1356 + _LOTSNESTEDMESSAGE_B91._serialized_start=1358 + _LOTSNESTEDMESSAGE_B91._serialized_end=1363 + _LOTSNESTEDMESSAGE_B92._serialized_start=1365 + _LOTSNESTEDMESSAGE_B92._serialized_end=1370 + _LOTSNESTEDMESSAGE_B93._serialized_start=1372 + _LOTSNESTEDMESSAGE_B93._serialized_end=1377 + _LOTSNESTEDMESSAGE_B94._serialized_start=1379 + _LOTSNESTEDMESSAGE_B94._serialized_end=1384 + _LOTSNESTEDMESSAGE_B95._serialized_start=1386 + _LOTSNESTEDMESSAGE_B95._serialized_end=1391 + _LOTSNESTEDMESSAGE_B96._serialized_start=1393 + _LOTSNESTEDMESSAGE_B96._serialized_end=1398 + _LOTSNESTEDMESSAGE_B97._serialized_start=1400 + _LOTSNESTEDMESSAGE_B97._serialized_end=1405 + _LOTSNESTEDMESSAGE_B98._serialized_start=1407 + _LOTSNESTEDMESSAGE_B98._serialized_end=1412 + _LOTSNESTEDMESSAGE_B99._serialized_start=1414 + _LOTSNESTEDMESSAGE_B99._serialized_end=1419 + _LOTSNESTEDMESSAGE_B100._serialized_start=1421 + _LOTSNESTEDMESSAGE_B100._serialized_end=1427 + _LOTSNESTEDMESSAGE_B101._serialized_start=1429 + _LOTSNESTEDMESSAGE_B101._serialized_end=1435 + _LOTSNESTEDMESSAGE_B102._serialized_start=1437 + _LOTSNESTEDMESSAGE_B102._serialized_end=1443 + _LOTSNESTEDMESSAGE_B103._serialized_start=1445 + _LOTSNESTEDMESSAGE_B103._serialized_end=1451 + _LOTSNESTEDMESSAGE_B104._serialized_start=1453 + _LOTSNESTEDMESSAGE_B104._serialized_end=1459 + _LOTSNESTEDMESSAGE_B105._serialized_start=1461 + _LOTSNESTEDMESSAGE_B105._serialized_end=1467 + _LOTSNESTEDMESSAGE_B106._serialized_start=1469 + _LOTSNESTEDMESSAGE_B106._serialized_end=1475 + _LOTSNESTEDMESSAGE_B107._serialized_start=1477 + _LOTSNESTEDMESSAGE_B107._serialized_end=1483 + _LOTSNESTEDMESSAGE_B108._serialized_start=1485 + _LOTSNESTEDMESSAGE_B108._serialized_end=1491 + _LOTSNESTEDMESSAGE_B109._serialized_start=1493 + _LOTSNESTEDMESSAGE_B109._serialized_end=1499 + _LOTSNESTEDMESSAGE_B110._serialized_start=1501 + _LOTSNESTEDMESSAGE_B110._serialized_end=1507 + _LOTSNESTEDMESSAGE_B111._serialized_start=1509 + _LOTSNESTEDMESSAGE_B111._serialized_end=1515 + _LOTSNESTEDMESSAGE_B112._serialized_start=1517 + _LOTSNESTEDMESSAGE_B112._serialized_end=1523 + _LOTSNESTEDMESSAGE_B113._serialized_start=1525 + _LOTSNESTEDMESSAGE_B113._serialized_end=1531 + _LOTSNESTEDMESSAGE_B114._serialized_start=1533 + _LOTSNESTEDMESSAGE_B114._serialized_end=1539 + _LOTSNESTEDMESSAGE_B115._serialized_start=1541 + _LOTSNESTEDMESSAGE_B115._serialized_end=1547 + _LOTSNESTEDMESSAGE_B116._serialized_start=1549 + _LOTSNESTEDMESSAGE_B116._serialized_end=1555 + _LOTSNESTEDMESSAGE_B117._serialized_start=1557 + _LOTSNESTEDMESSAGE_B117._serialized_end=1563 + _LOTSNESTEDMESSAGE_B118._serialized_start=1565 + _LOTSNESTEDMESSAGE_B118._serialized_end=1571 + _LOTSNESTEDMESSAGE_B119._serialized_start=1573 + _LOTSNESTEDMESSAGE_B119._serialized_end=1579 + _LOTSNESTEDMESSAGE_B120._serialized_start=1581 + _LOTSNESTEDMESSAGE_B120._serialized_end=1587 + _LOTSNESTEDMESSAGE_B121._serialized_start=1589 + _LOTSNESTEDMESSAGE_B121._serialized_end=1595 + _LOTSNESTEDMESSAGE_B122._serialized_start=1597 + _LOTSNESTEDMESSAGE_B122._serialized_end=1603 + _LOTSNESTEDMESSAGE_B123._serialized_start=1605 + _LOTSNESTEDMESSAGE_B123._serialized_end=1611 + _LOTSNESTEDMESSAGE_B124._serialized_start=1613 + _LOTSNESTEDMESSAGE_B124._serialized_end=1619 + _LOTSNESTEDMESSAGE_B125._serialized_start=1621 + _LOTSNESTEDMESSAGE_B125._serialized_end=1627 + _LOTSNESTEDMESSAGE_B126._serialized_start=1629 + _LOTSNESTEDMESSAGE_B126._serialized_end=1635 + _LOTSNESTEDMESSAGE_B127._serialized_start=1637 + _LOTSNESTEDMESSAGE_B127._serialized_end=1643 + _LOTSNESTEDMESSAGE_B128._serialized_start=1645 + _LOTSNESTEDMESSAGE_B128._serialized_end=1651 + _LOTSNESTEDMESSAGE_B129._serialized_start=1653 + _LOTSNESTEDMESSAGE_B129._serialized_end=1659 + _LOTSNESTEDMESSAGE_B130._serialized_start=1661 + _LOTSNESTEDMESSAGE_B130._serialized_end=1667 + _LOTSNESTEDMESSAGE_B131._serialized_start=1669 + _LOTSNESTEDMESSAGE_B131._serialized_end=1675 + _LOTSNESTEDMESSAGE_B132._serialized_start=1677 + _LOTSNESTEDMESSAGE_B132._serialized_end=1683 + _LOTSNESTEDMESSAGE_B133._serialized_start=1685 + _LOTSNESTEDMESSAGE_B133._serialized_end=1691 + _LOTSNESTEDMESSAGE_B134._serialized_start=1693 + _LOTSNESTEDMESSAGE_B134._serialized_end=1699 + _LOTSNESTEDMESSAGE_B135._serialized_start=1701 + _LOTSNESTEDMESSAGE_B135._serialized_end=1707 + _LOTSNESTEDMESSAGE_B136._serialized_start=1709 + _LOTSNESTEDMESSAGE_B136._serialized_end=1715 + _LOTSNESTEDMESSAGE_B137._serialized_start=1717 + _LOTSNESTEDMESSAGE_B137._serialized_end=1723 + _LOTSNESTEDMESSAGE_B138._serialized_start=1725 + _LOTSNESTEDMESSAGE_B138._serialized_end=1731 + _LOTSNESTEDMESSAGE_B139._serialized_start=1733 + _LOTSNESTEDMESSAGE_B139._serialized_end=1739 + _LOTSNESTEDMESSAGE_B140._serialized_start=1741 + _LOTSNESTEDMESSAGE_B140._serialized_end=1747 + _LOTSNESTEDMESSAGE_B141._serialized_start=1749 + _LOTSNESTEDMESSAGE_B141._serialized_end=1755 + _LOTSNESTEDMESSAGE_B142._serialized_start=1757 + _LOTSNESTEDMESSAGE_B142._serialized_end=1763 + _LOTSNESTEDMESSAGE_B143._serialized_start=1765 + _LOTSNESTEDMESSAGE_B143._serialized_end=1771 + _LOTSNESTEDMESSAGE_B144._serialized_start=1773 + _LOTSNESTEDMESSAGE_B144._serialized_end=1779 + _LOTSNESTEDMESSAGE_B145._serialized_start=1781 + _LOTSNESTEDMESSAGE_B145._serialized_end=1787 + _LOTSNESTEDMESSAGE_B146._serialized_start=1789 + _LOTSNESTEDMESSAGE_B146._serialized_end=1795 + _LOTSNESTEDMESSAGE_B147._serialized_start=1797 + _LOTSNESTEDMESSAGE_B147._serialized_end=1803 + _LOTSNESTEDMESSAGE_B148._serialized_start=1805 + _LOTSNESTEDMESSAGE_B148._serialized_end=1811 + _LOTSNESTEDMESSAGE_B149._serialized_start=1813 + _LOTSNESTEDMESSAGE_B149._serialized_end=1819 + _LOTSNESTEDMESSAGE_B150._serialized_start=1821 + _LOTSNESTEDMESSAGE_B150._serialized_end=1827 + _LOTSNESTEDMESSAGE_B151._serialized_start=1829 + _LOTSNESTEDMESSAGE_B151._serialized_end=1835 + _LOTSNESTEDMESSAGE_B152._serialized_start=1837 + _LOTSNESTEDMESSAGE_B152._serialized_end=1843 + _LOTSNESTEDMESSAGE_B153._serialized_start=1845 + _LOTSNESTEDMESSAGE_B153._serialized_end=1851 + _LOTSNESTEDMESSAGE_B154._serialized_start=1853 + _LOTSNESTEDMESSAGE_B154._serialized_end=1859 + _LOTSNESTEDMESSAGE_B155._serialized_start=1861 + _LOTSNESTEDMESSAGE_B155._serialized_end=1867 + _LOTSNESTEDMESSAGE_B156._serialized_start=1869 + _LOTSNESTEDMESSAGE_B156._serialized_end=1875 + _LOTSNESTEDMESSAGE_B157._serialized_start=1877 + _LOTSNESTEDMESSAGE_B157._serialized_end=1883 + _LOTSNESTEDMESSAGE_B158._serialized_start=1885 + _LOTSNESTEDMESSAGE_B158._serialized_end=1891 + _LOTSNESTEDMESSAGE_B159._serialized_start=1893 + _LOTSNESTEDMESSAGE_B159._serialized_end=1899 + _LOTSNESTEDMESSAGE_B160._serialized_start=1901 + _LOTSNESTEDMESSAGE_B160._serialized_end=1907 + _LOTSNESTEDMESSAGE_B161._serialized_start=1909 + _LOTSNESTEDMESSAGE_B161._serialized_end=1915 + _LOTSNESTEDMESSAGE_B162._serialized_start=1917 + _LOTSNESTEDMESSAGE_B162._serialized_end=1923 + _LOTSNESTEDMESSAGE_B163._serialized_start=1925 + _LOTSNESTEDMESSAGE_B163._serialized_end=1931 + _LOTSNESTEDMESSAGE_B164._serialized_start=1933 + _LOTSNESTEDMESSAGE_B164._serialized_end=1939 + _LOTSNESTEDMESSAGE_B165._serialized_start=1941 + _LOTSNESTEDMESSAGE_B165._serialized_end=1947 + _LOTSNESTEDMESSAGE_B166._serialized_start=1949 + _LOTSNESTEDMESSAGE_B166._serialized_end=1955 + _LOTSNESTEDMESSAGE_B167._serialized_start=1957 + _LOTSNESTEDMESSAGE_B167._serialized_end=1963 + _LOTSNESTEDMESSAGE_B168._serialized_start=1965 + _LOTSNESTEDMESSAGE_B168._serialized_end=1971 + _LOTSNESTEDMESSAGE_B169._serialized_start=1973 + _LOTSNESTEDMESSAGE_B169._serialized_end=1979 + _LOTSNESTEDMESSAGE_B170._serialized_start=1981 + _LOTSNESTEDMESSAGE_B170._serialized_end=1987 + _LOTSNESTEDMESSAGE_B171._serialized_start=1989 + _LOTSNESTEDMESSAGE_B171._serialized_end=1995 + _LOTSNESTEDMESSAGE_B172._serialized_start=1997 + _LOTSNESTEDMESSAGE_B172._serialized_end=2003 + _LOTSNESTEDMESSAGE_B173._serialized_start=2005 + _LOTSNESTEDMESSAGE_B173._serialized_end=2011 + _LOTSNESTEDMESSAGE_B174._serialized_start=2013 + _LOTSNESTEDMESSAGE_B174._serialized_end=2019 + _LOTSNESTEDMESSAGE_B175._serialized_start=2021 + _LOTSNESTEDMESSAGE_B175._serialized_end=2027 + _LOTSNESTEDMESSAGE_B176._serialized_start=2029 + _LOTSNESTEDMESSAGE_B176._serialized_end=2035 + _LOTSNESTEDMESSAGE_B177._serialized_start=2037 + _LOTSNESTEDMESSAGE_B177._serialized_end=2043 + _LOTSNESTEDMESSAGE_B178._serialized_start=2045 + _LOTSNESTEDMESSAGE_B178._serialized_end=2051 + _LOTSNESTEDMESSAGE_B179._serialized_start=2053 + _LOTSNESTEDMESSAGE_B179._serialized_end=2059 + _LOTSNESTEDMESSAGE_B180._serialized_start=2061 + _LOTSNESTEDMESSAGE_B180._serialized_end=2067 + _LOTSNESTEDMESSAGE_B181._serialized_start=2069 + _LOTSNESTEDMESSAGE_B181._serialized_end=2075 + _LOTSNESTEDMESSAGE_B182._serialized_start=2077 + _LOTSNESTEDMESSAGE_B182._serialized_end=2083 + _LOTSNESTEDMESSAGE_B183._serialized_start=2085 + _LOTSNESTEDMESSAGE_B183._serialized_end=2091 + _LOTSNESTEDMESSAGE_B184._serialized_start=2093 + _LOTSNESTEDMESSAGE_B184._serialized_end=2099 + _LOTSNESTEDMESSAGE_B185._serialized_start=2101 + _LOTSNESTEDMESSAGE_B185._serialized_end=2107 + _LOTSNESTEDMESSAGE_B186._serialized_start=2109 + _LOTSNESTEDMESSAGE_B186._serialized_end=2115 + _LOTSNESTEDMESSAGE_B187._serialized_start=2117 + _LOTSNESTEDMESSAGE_B187._serialized_end=2123 + _LOTSNESTEDMESSAGE_B188._serialized_start=2125 + _LOTSNESTEDMESSAGE_B188._serialized_end=2131 + _LOTSNESTEDMESSAGE_B189._serialized_start=2133 + _LOTSNESTEDMESSAGE_B189._serialized_end=2139 + _LOTSNESTEDMESSAGE_B190._serialized_start=2141 + _LOTSNESTEDMESSAGE_B190._serialized_end=2147 + _LOTSNESTEDMESSAGE_B191._serialized_start=2149 + _LOTSNESTEDMESSAGE_B191._serialized_end=2155 + _LOTSNESTEDMESSAGE_B192._serialized_start=2157 + _LOTSNESTEDMESSAGE_B192._serialized_end=2163 + _LOTSNESTEDMESSAGE_B193._serialized_start=2165 + _LOTSNESTEDMESSAGE_B193._serialized_end=2171 + _LOTSNESTEDMESSAGE_B194._serialized_start=2173 + _LOTSNESTEDMESSAGE_B194._serialized_end=2179 + _LOTSNESTEDMESSAGE_B195._serialized_start=2181 + _LOTSNESTEDMESSAGE_B195._serialized_end=2187 + _LOTSNESTEDMESSAGE_B196._serialized_start=2189 + _LOTSNESTEDMESSAGE_B196._serialized_end=2195 + _LOTSNESTEDMESSAGE_B197._serialized_start=2197 + _LOTSNESTEDMESSAGE_B197._serialized_end=2203 + _LOTSNESTEDMESSAGE_B198._serialized_start=2205 + _LOTSNESTEDMESSAGE_B198._serialized_end=2211 + _LOTSNESTEDMESSAGE_B199._serialized_start=2213 + _LOTSNESTEDMESSAGE_B199._serialized_end=2219 + _LOTSNESTEDMESSAGE_B200._serialized_start=2221 + _LOTSNESTEDMESSAGE_B200._serialized_end=2227 + _LOTSNESTEDMESSAGE_B201._serialized_start=2229 + _LOTSNESTEDMESSAGE_B201._serialized_end=2235 + _LOTSNESTEDMESSAGE_B202._serialized_start=2237 + _LOTSNESTEDMESSAGE_B202._serialized_end=2243 + _LOTSNESTEDMESSAGE_B203._serialized_start=2245 + _LOTSNESTEDMESSAGE_B203._serialized_end=2251 + _LOTSNESTEDMESSAGE_B204._serialized_start=2253 + _LOTSNESTEDMESSAGE_B204._serialized_end=2259 + _LOTSNESTEDMESSAGE_B205._serialized_start=2261 + _LOTSNESTEDMESSAGE_B205._serialized_end=2267 + _LOTSNESTEDMESSAGE_B206._serialized_start=2269 + _LOTSNESTEDMESSAGE_B206._serialized_end=2275 + _LOTSNESTEDMESSAGE_B207._serialized_start=2277 + _LOTSNESTEDMESSAGE_B207._serialized_end=2283 + _LOTSNESTEDMESSAGE_B208._serialized_start=2285 + _LOTSNESTEDMESSAGE_B208._serialized_end=2291 + _LOTSNESTEDMESSAGE_B209._serialized_start=2293 + _LOTSNESTEDMESSAGE_B209._serialized_end=2299 + _LOTSNESTEDMESSAGE_B210._serialized_start=2301 + _LOTSNESTEDMESSAGE_B210._serialized_end=2307 + _LOTSNESTEDMESSAGE_B211._serialized_start=2309 + _LOTSNESTEDMESSAGE_B211._serialized_end=2315 + _LOTSNESTEDMESSAGE_B212._serialized_start=2317 + _LOTSNESTEDMESSAGE_B212._serialized_end=2323 + _LOTSNESTEDMESSAGE_B213._serialized_start=2325 + _LOTSNESTEDMESSAGE_B213._serialized_end=2331 + _LOTSNESTEDMESSAGE_B214._serialized_start=2333 + _LOTSNESTEDMESSAGE_B214._serialized_end=2339 + _LOTSNESTEDMESSAGE_B215._serialized_start=2341 + _LOTSNESTEDMESSAGE_B215._serialized_end=2347 + _LOTSNESTEDMESSAGE_B216._serialized_start=2349 + _LOTSNESTEDMESSAGE_B216._serialized_end=2355 + _LOTSNESTEDMESSAGE_B217._serialized_start=2357 + _LOTSNESTEDMESSAGE_B217._serialized_end=2363 + _LOTSNESTEDMESSAGE_B218._serialized_start=2365 + _LOTSNESTEDMESSAGE_B218._serialized_end=2371 + _LOTSNESTEDMESSAGE_B219._serialized_start=2373 + _LOTSNESTEDMESSAGE_B219._serialized_end=2379 + _LOTSNESTEDMESSAGE_B220._serialized_start=2381 + _LOTSNESTEDMESSAGE_B220._serialized_end=2387 + _LOTSNESTEDMESSAGE_B221._serialized_start=2389 + _LOTSNESTEDMESSAGE_B221._serialized_end=2395 + _LOTSNESTEDMESSAGE_B222._serialized_start=2397 + _LOTSNESTEDMESSAGE_B222._serialized_end=2403 + _LOTSNESTEDMESSAGE_B223._serialized_start=2405 + _LOTSNESTEDMESSAGE_B223._serialized_end=2411 + _LOTSNESTEDMESSAGE_B224._serialized_start=2413 + _LOTSNESTEDMESSAGE_B224._serialized_end=2419 + _LOTSNESTEDMESSAGE_B225._serialized_start=2421 + _LOTSNESTEDMESSAGE_B225._serialized_end=2427 + _LOTSNESTEDMESSAGE_B226._serialized_start=2429 + _LOTSNESTEDMESSAGE_B226._serialized_end=2435 + _LOTSNESTEDMESSAGE_B227._serialized_start=2437 + _LOTSNESTEDMESSAGE_B227._serialized_end=2443 + _LOTSNESTEDMESSAGE_B228._serialized_start=2445 + _LOTSNESTEDMESSAGE_B228._serialized_end=2451 + _LOTSNESTEDMESSAGE_B229._serialized_start=2453 + _LOTSNESTEDMESSAGE_B229._serialized_end=2459 + _LOTSNESTEDMESSAGE_B230._serialized_start=2461 + _LOTSNESTEDMESSAGE_B230._serialized_end=2467 + _LOTSNESTEDMESSAGE_B231._serialized_start=2469 + _LOTSNESTEDMESSAGE_B231._serialized_end=2475 + _LOTSNESTEDMESSAGE_B232._serialized_start=2477 + _LOTSNESTEDMESSAGE_B232._serialized_end=2483 + _LOTSNESTEDMESSAGE_B233._serialized_start=2485 + _LOTSNESTEDMESSAGE_B233._serialized_end=2491 + _LOTSNESTEDMESSAGE_B234._serialized_start=2493 + _LOTSNESTEDMESSAGE_B234._serialized_end=2499 + _LOTSNESTEDMESSAGE_B235._serialized_start=2501 + _LOTSNESTEDMESSAGE_B235._serialized_end=2507 + _LOTSNESTEDMESSAGE_B236._serialized_start=2509 + _LOTSNESTEDMESSAGE_B236._serialized_end=2515 + _LOTSNESTEDMESSAGE_B237._serialized_start=2517 + _LOTSNESTEDMESSAGE_B237._serialized_end=2523 + _LOTSNESTEDMESSAGE_B238._serialized_start=2525 + _LOTSNESTEDMESSAGE_B238._serialized_end=2531 + _LOTSNESTEDMESSAGE_B239._serialized_start=2533 + _LOTSNESTEDMESSAGE_B239._serialized_end=2539 + _LOTSNESTEDMESSAGE_B240._serialized_start=2541 + _LOTSNESTEDMESSAGE_B240._serialized_end=2547 + _LOTSNESTEDMESSAGE_B241._serialized_start=2549 + _LOTSNESTEDMESSAGE_B241._serialized_end=2555 + _LOTSNESTEDMESSAGE_B242._serialized_start=2557 + _LOTSNESTEDMESSAGE_B242._serialized_end=2563 + _LOTSNESTEDMESSAGE_B243._serialized_start=2565 + _LOTSNESTEDMESSAGE_B243._serialized_end=2571 + _LOTSNESTEDMESSAGE_B244._serialized_start=2573 + _LOTSNESTEDMESSAGE_B244._serialized_end=2579 + _LOTSNESTEDMESSAGE_B245._serialized_start=2581 + _LOTSNESTEDMESSAGE_B245._serialized_end=2587 + _LOTSNESTEDMESSAGE_B246._serialized_start=2589 + _LOTSNESTEDMESSAGE_B246._serialized_end=2595 + _LOTSNESTEDMESSAGE_B247._serialized_start=2597 + _LOTSNESTEDMESSAGE_B247._serialized_end=2603 + _LOTSNESTEDMESSAGE_B248._serialized_start=2605 + _LOTSNESTEDMESSAGE_B248._serialized_end=2611 + _LOTSNESTEDMESSAGE_B249._serialized_start=2613 + _LOTSNESTEDMESSAGE_B249._serialized_end=2619 + _LOTSNESTEDMESSAGE_B250._serialized_start=2621 + _LOTSNESTEDMESSAGE_B250._serialized_end=2627 + _LOTSNESTEDMESSAGE_B251._serialized_start=2629 + _LOTSNESTEDMESSAGE_B251._serialized_end=2635 + _LOTSNESTEDMESSAGE_B252._serialized_start=2637 + _LOTSNESTEDMESSAGE_B252._serialized_end=2643 + _LOTSNESTEDMESSAGE_B253._serialized_start=2645 + _LOTSNESTEDMESSAGE_B253._serialized_end=2651 + _LOTSNESTEDMESSAGE_B254._serialized_start=2653 + _LOTSNESTEDMESSAGE_B254._serialized_end=2659 + _LOTSNESTEDMESSAGE_B255._serialized_start=2661 + _LOTSNESTEDMESSAGE_B255._serialized_end=2667 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py new file mode 100644 index 0000000000..d46dee080a --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/no_package.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)google/protobuf/internal/no_package.proto\";\n\x10NoPackageMessage\x12\'\n\x0fno_package_enum\x18\x01 \x01(\x0e\x32\x0e.NoPackageEnum*?\n\rNoPackageEnum\x12\x16\n\x12NO_PACKAGE_VALUE_0\x10\x00\x12\x16\n\x12NO_PACKAGE_VALUE_1\x10\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _NOPACKAGEENUM._serialized_start=106 + _NOPACKAGEENUM._serialized_end=169 + _NOPACKAGEMESSAGE._serialized_start=45 + _NOPACKAGEMESSAGE._serialized_end=104 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py new file mode 100644 index 0000000000..2921d5cb6e --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py @@ -0,0 +1,1539 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. +# +# TODO(robinson): Helpers for verbose, common checks like seeing if a +# descriptor's cpp_type is CPPTYPE_MESSAGE. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +from io import BytesIO +import struct +import sys +import weakref + +# We use "as" to avoid name collisions with variables. +from google.protobuf.internal import api_implementation +from google.protobuf.internal import containers +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import extension_dict +from google.protobuf.internal import message_listener as message_listener_mod +from google.protobuf.internal import type_checkers +from google.protobuf.internal import well_known_types +from google.protobuf.internal import wire_format +from google.protobuf import descriptor as descriptor_mod +from google.protobuf import message as message_mod +from google.protobuf import text_format + +_FieldDescriptor = descriptor_mod.FieldDescriptor +_AnyFullTypeName = 'google.protobuf.Any' +_ExtensionDict = extension_dict._ExtensionDict + +class GeneratedProtocolMessageType(type): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + We add implementations for all methods described in the Message class. We + also create properties to allow getting/setting all fields in the protocol + message. Finally, we create slots to prevent users from accidentally + "setting" nonexistent fields in the protocol message, which then wouldn't get + serialized / deserialized properly. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __new__(cls, name, bases, dictionary): + """Custom allocation for runtime-generated class types. + + We override __new__ because this is apparently the only place + where we can meaningfully set __slots__ on the class we're creating(?). + (The interplay between metaclasses and slots is not very well-documented). + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + + Returns: + Newly-allocated class. + + Raises: + RuntimeError: Generated code only work with python cpp extension. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + if isinstance(descriptor, str): + raise RuntimeError('The generated code only work with python cpp ' + 'extension, but it is using pure python runtime.') + + # If a concrete class already exists for this descriptor, don't try to + # create another. Doing so will break any messages that already exist with + # the existing class. + # + # The C++ implementation appears to have its own internal `PyMessageFactory` + # to achieve similar results. + # + # This most commonly happens in `text_format.py` when using descriptors from + # a custom pool; it calls symbol_database.Global().getPrototype() on a + # descriptor which already has an existing concrete class. + new_class = getattr(descriptor, '_concrete_class', None) + if new_class: + return new_class + + if descriptor.full_name in well_known_types.WKTBASES: + bases += (well_known_types.WKTBASES[descriptor.full_name],) + _AddClassAttributesForNestedExtensions(descriptor, dictionary) + _AddSlots(descriptor, dictionary) + + superclass = super(GeneratedProtocolMessageType, cls) + new_class = superclass.__new__(cls, name, bases, dictionary) + return new_class + + def __init__(cls, name, bases, dictionary): + """Here we perform the majority of our work on the class. + We add enum getters, an __init__ method, implementations + of all Message methods, and properties for all fields + in the protocol type. + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + # If this is an _existing_ class looked up via `_concrete_class` in the + # __new__ method above, then we don't need to re-initialize anything. + existing_class = getattr(descriptor, '_concrete_class', None) + if existing_class: + assert existing_class is cls, ( + 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' + % (descriptor.full_name)) + return + + cls._decoders_by_tag = {} + if (descriptor.has_options and + descriptor.GetOptions().message_set_wire_format): + cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( + decoder.MessageSetItemDecoder(descriptor), None) + + # Attach stuff to each FieldDescriptor for quick lookup later on. + for field in descriptor.fields: + _AttachFieldHelpers(cls, field) + + descriptor._concrete_class = cls # pylint: disable=protected-access + _AddEnumValues(descriptor, cls) + _AddInitMethod(descriptor, cls) + _AddPropertiesForFields(descriptor, cls) + _AddPropertiesForExtensions(descriptor, cls) + _AddStaticMethods(cls) + _AddMessageMethods(descriptor, cls) + _AddPrivateHelperMethods(descriptor, cls) + + superclass = super(GeneratedProtocolMessageType, cls) + superclass.__init__(name, bases, dictionary) + + +# Stateless helpers for GeneratedProtocolMessageType below. +# Outside clients should not access these directly. +# +# I opted not to make any of these methods on the metaclass, to make it more +# clear that I'm not really using any state there and to keep clients from +# thinking that they have direct access to these construction helpers. + + +def _PropertyName(proto_field_name): + """Returns the name of the public property attribute which + clients can use to get and (in some cases) set the value + of a protocol message field. + + Args: + proto_field_name: The protocol message field name, exactly + as it appears (or would appear) in a .proto file. + """ + # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. + # nnorwitz makes my day by writing: + # """ + # FYI. See the keyword module in the stdlib. This could be as simple as: + # + # if keyword.iskeyword(proto_field_name): + # return proto_field_name + "_" + # return proto_field_name + # """ + # Kenton says: The above is a BAD IDEA. People rely on being able to use + # getattr() and setattr() to reflectively manipulate field values. If we + # rename the properties, then every such user has to also make sure to apply + # the same transformation. Note that currently if you name a field "yield", + # you can still access it just fine using getattr/setattr -- it's not even + # that cumbersome to do so. + # TODO(kenton): Remove this method entirely if/when everyone agrees with my + # position. + return proto_field_name + + +def _AddSlots(message_descriptor, dictionary): + """Adds a __slots__ entry to dictionary, containing the names of all valid + attributes for this message type. + + Args: + message_descriptor: A Descriptor instance describing this message type. + dictionary: Class dictionary to which we'll add a '__slots__' entry. + """ + dictionary['__slots__'] = ['_cached_byte_size', + '_cached_byte_size_dirty', + '_fields', + '_unknown_fields', + '_unknown_field_set', + '_is_present_in_parent', + '_listener', + '_listener_for_children', + '__weakref__', + '_oneofs'] + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == _FieldDescriptor.TYPE_MESSAGE and + field.label == _FieldDescriptor.LABEL_OPTIONAL) + + +def _IsMapField(field): + return (field.type == _FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def _IsMessageMapField(field): + value_type = field.message_type.fields_by_name['value'] + return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE + + +def _AttachFieldHelpers(cls, field_descriptor): + is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) + is_packable = (is_repeated and + wire_format.IsTypePackable(field_descriptor.type)) + is_proto3 = field_descriptor.containing_type.syntax == 'proto3' + if not is_packable: + is_packed = False + elif field_descriptor.containing_type.syntax == 'proto2': + is_packed = (field_descriptor.has_options and + field_descriptor.GetOptions().packed) + else: + has_packed_false = (field_descriptor.has_options and + field_descriptor.GetOptions().HasField('packed') and + field_descriptor.GetOptions().packed == False) + is_packed = not has_packed_false + is_map_entry = _IsMapField(field_descriptor) + + if is_map_entry: + field_encoder = encoder.MapEncoder(field_descriptor) + sizer = encoder.MapSizer(field_descriptor, + _IsMessageMapField(field_descriptor)) + elif _IsMessageSetExtension(field_descriptor): + field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) + sizer = encoder.MessageSetItemSizer(field_descriptor.number) + else: + field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + + field_descriptor._encoder = field_encoder + field_descriptor._sizer = sizer + field_descriptor._default_constructor = _DefaultValueConstructorForField( + field_descriptor) + + def AddDecoder(wiretype, is_packed): + tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) + decode_type = field_descriptor.type + if (decode_type == _FieldDescriptor.TYPE_ENUM and + type_checkers.SupportsOpenEnums(field_descriptor)): + decode_type = _FieldDescriptor.TYPE_INT32 + + oneof_descriptor = None + clear_if_default = False + if field_descriptor.containing_oneof is not None: + oneof_descriptor = field_descriptor + elif (is_proto3 and not is_repeated and + field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE): + clear_if_default = True + + if is_map_entry: + is_message_map = _IsMessageMapField(field_descriptor) + + field_decoder = decoder.MapDecoder( + field_descriptor, _GetInitializeDefaultForMap(field_descriptor), + is_message_map) + elif decode_type == _FieldDescriptor.TYPE_STRING: + field_decoder = decoder.StringDecoder( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor) + else: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + # pylint: disable=protected-access + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + + cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) + + AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], + False) + + if is_repeated and wire_format.IsTypePackable(field_descriptor.type): + # To support wire compatibility of adding packed = true, add a decoder for + # packed values regardless of the field's options. + AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) + + +def _AddClassAttributesForNestedExtensions(descriptor, dictionary): + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + assert extension_name not in dictionary + dictionary[extension_name] = extension_field + + +def _AddEnumValues(descriptor, cls): + """Sets class-level attributes for all enum fields defined in this message. + + Also exporting a class-level object that can name enum values. + + Args: + descriptor: Descriptor object for this message type. + cls: Class we're constructing for this message type. + """ + for enum_type in descriptor.enum_types: + setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) + for enum_value in enum_type.values: + setattr(cls, enum_value.name, enum_value.number) + + +def _GetInitializeDefaultForMap(field): + if field.label != _FieldDescriptor.LABEL_REPEATED: + raise ValueError('map_entry set on non-repeated field %s' % ( + field.name)) + fields_by_name = field.message_type.fields_by_name + key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) + + value_field = fields_by_name['value'] + if _IsMessageMapField(field): + def MakeMessageMapDefault(message): + return containers.MessageMap( + message._listener_for_children, value_field.message_type, key_checker, + field.message_type) + return MakeMessageMapDefault + else: + value_checker = type_checkers.GetTypeChecker(value_field) + def MakePrimitiveMapDefault(message): + return containers.ScalarMap( + message._listener_for_children, key_checker, value_checker, + field.message_type) + return MakePrimitiveMapDefault + +def _DefaultValueConstructorForField(field): + """Returns a function which returns a default value for a field. + + Args: + field: FieldDescriptor object for this field. + + The returned function has one argument: + message: Message instance containing this field, or a weakref proxy + of same. + + That function in turn returns a default value for this field. The default + value may refer back to |message| via a weak reference. + """ + + if _IsMapField(field): + return _GetInitializeDefaultForMap(field) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + if field.has_default_value and field.default_value != []: + raise ValueError('Repeated field default value not empty list: %s' % ( + field.default_value)) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # We can't look at _concrete_class yet since it might not have + # been set. (Depends on order in which we initialize the classes). + message_type = field.message_type + def MakeRepeatedMessageDefault(message): + return containers.RepeatedCompositeFieldContainer( + message._listener_for_children, field.message_type) + return MakeRepeatedMessageDefault + else: + type_checker = type_checkers.GetTypeChecker(field) + def MakeRepeatedScalarDefault(message): + return containers.RepeatedScalarFieldContainer( + message._listener_for_children, type_checker) + return MakeRepeatedScalarDefault + + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # _concrete_class may not yet be initialized. + message_type = field.message_type + def MakeSubMessageDefault(message): + assert getattr(message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (field.full_name, message_type.full_name)) + result = message_type._concrete_class() + result._SetListener( + _OneofListener(message, field) + if field.containing_oneof is not None + else message._listener_for_children) + return result + return MakeSubMessageDefault + + def MakeScalarDefault(message): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return field.default_value + return MakeScalarDefault + + +def _ReraiseTypeErrorWithFieldName(message_name, field_name): + """Re-raise the currently-handled TypeError with the field name added.""" + exc = sys.exc_info()[1] + if len(exc.args) == 1 and type(exc) is TypeError: + # simple TypeError; add field name to exception message + exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) + + # re-raise possibly-amended exception with original traceback: + raise exc.with_traceback(sys.exc_info()[2]) + + +def _AddInitMethod(message_descriptor, cls): + """Adds an __init__ method to cls.""" + + def _GetIntegerEnumValue(enum_type, value): + """Convert a string or integer enum value to an integer. + + If the value is a string, it is converted to the enum value in + enum_type with the same name. If the value is not a string, it's + returned as-is. (No conversion or bounds-checking is done.) + """ + if isinstance(value, str): + try: + return enum_type.values_by_name[value].number + except KeyError: + raise ValueError('Enum type %s: unknown label "%s"' % ( + enum_type.full_name, value)) + return value + + def init(self, **kwargs): + self._cached_byte_size = 0 + self._cached_byte_size_dirty = len(kwargs) > 0 + self._fields = {} + # Contains a mapping from oneof field descriptors to the descriptor + # of the currently set field in that oneof field. + self._oneofs = {} + + # _unknown_fields is () when empty for efficiency, and will be turned into + # a list if fields are added. + self._unknown_fields = () + # _unknown_field_set is None when empty for efficiency, and will be + # turned into UnknownFieldSet struct if fields are added. + self._unknown_field_set = None # pylint: disable=protected-access + self._is_present_in_parent = False + self._listener = message_listener_mod.NullMessageListener() + self._listener_for_children = _Listener(self) + for field_name, field_value in kwargs.items(): + field = _GetFieldByName(message_descriptor, field_name) + if field is None: + raise TypeError('%s() got an unexpected keyword argument "%s"' % + (message_descriptor.name, field_name)) + if field_value is None: + # field=None is the same as no field at all. + continue + if field.label == _FieldDescriptor.LABEL_REPEATED: + copy = field._default_constructor(self) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite + if _IsMapField(field): + if _IsMessageMapField(field): + for key in field_value: + copy[key].MergeFrom(field_value[key]) + else: + copy.update(field_value) + else: + for val in field_value: + if isinstance(val, dict): + copy.add(**val) + else: + copy.add().MergeFrom(val) + else: # Scalar + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = [_GetIntegerEnumValue(field.enum_type, val) + for val in field_value] + copy.extend(field_value) + self._fields[field] = copy + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + copy = field._default_constructor(self) + new_val = field_value + if isinstance(field_value, dict): + new_val = field.message_type._concrete_class(**field_value) + try: + copy.MergeFrom(new_val) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + self._fields[field] = copy + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = _GetIntegerEnumValue(field.enum_type, field_value) + try: + setattr(self, field_name, field_value) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + + init.__module__ = None + init.__doc__ = None + cls.__init__ = init + + +def _GetFieldByName(message_descriptor, field_name): + """Returns a field descriptor by field name. + + Args: + message_descriptor: A Descriptor describing all fields in message. + field_name: The name of the field to retrieve. + Returns: + The field descriptor associated with the field name. + """ + try: + return message_descriptor.fields_by_name[field_name] + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + +def _AddPropertiesForFields(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + for field in descriptor.fields: + _AddPropertiesForField(field, cls) + + if descriptor.is_extendable: + # _ExtensionDict is just an adaptor with no state so we allocate a new one + # every time it is accessed. + cls.Extensions = property(lambda self: _ExtensionDict(self)) + + +def _AddPropertiesForField(field, cls): + """Adds a public property for a protocol message field. + Clients can use this property to get and (in the case + of non-repeated scalar fields) directly set the value + of a protocol message field. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # Catch it if we add other types that we should + # handle specially here. + assert _FieldDescriptor.MAX_CPPTYPE == 10 + + constant_name = field.name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, field.number) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + _AddPropertiesForRepeatedField(field, cls) + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + _AddPropertiesForNonRepeatedCompositeField(field, cls) + else: + _AddPropertiesForNonRepeatedScalarField(field, cls) + + +class _FieldProperty(property): + __slots__ = ('DESCRIPTOR',) + + def __init__(self, descriptor, getter, setter, doc): + property.__init__(self, getter, setter, doc=doc) + self.DESCRIPTOR = descriptor + + +def _AddPropertiesForRepeatedField(field, cls): + """Adds a public property for a "repeated" protocol message field. Clients + can use this property to get the value of the field, which will be either a + RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see + below). + + Note that when clients add values to these containers, we perform + type-checking in the case of repeated scalar fields, and we also set any + necessary "has" bits as a side-effect. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to repeated field ' + '"%s" in protocol message object.' % proto_field_name) + + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedScalarField(field, cls): + """Adds a public property for a nonrepeated, scalar protocol message field. + Clients can use this property to get and directly set the value of the field. + Note that when the client sets the value of a field by using this property, + all necessary "has" bits are set as a side-effect, and we also perform + type-checking. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + type_checker = type_checkers.GetTypeChecker(field) + default_value = field.default_value + is_proto3 = field.containing_type.syntax == 'proto3' + + def getter(self): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return self._fields.get(field, default_value) + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + clear_when_set_to_default = is_proto3 and not field.containing_oneof + + def field_setter(self, new_value): + # pylint: disable=protected-access + # Testing the value for truthiness captures all of the proto3 defaults + # (0, 0.0, enum 0, and False). + try: + new_value = type_checker.CheckValue(new_value) + except TypeError as e: + raise TypeError( + 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) + if clear_when_set_to_default and not new_value: + self._fields.pop(field, None) + else: + self._fields[field] = new_value + # Check _cached_byte_size_dirty inline to improve performance, since scalar + # setters are called frequently. + if not self._cached_byte_size_dirty: + self._Modified() + + if field.containing_oneof: + def setter(self, new_value): + field_setter(self, new_value) + self._UpdateOneofState(field) + else: + setter = field_setter + + setter.__module__ = None + setter.__doc__ = 'Setter for %s.' % proto_field_name + + # Add a property to encapsulate the getter/setter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedCompositeField(field, cls): + """Adds a public property for a nonrepeated, composite protocol message field. + A composite field is a "group" or "message" field. + + Clients can use this property to get the value of the field, but cannot + assign to the property directly. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # TODO(robinson): Remove duplication with similar method + # for non-repeated scalars. + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to composite field ' + '"%s" in protocol message object.' % proto_field_name) + + # Add a property to encapsulate the getter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForExtensions(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + constant_name = extension_name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, extension_field.number) + + # TODO(amauryfa): Migrate all users of these attributes to functions like + # pool.FindExtensionByNumber(descriptor). + if descriptor.file is not None: + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + pool = descriptor.file.pool + cls._extensions_by_number = pool._extensions_by_number[descriptor] + cls._extensions_by_name = pool._extensions_by_name[descriptor] + +def _AddStaticMethods(cls): + # TODO(robinson): This probably needs to be thread-safe(?) + def RegisterExtension(extension_handle): + extension_handle.containing_type = cls.DESCRIPTOR + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + # pylint: disable=protected-access + cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle) + _AttachFieldHelpers(cls, extension_handle) + cls.RegisterExtension = staticmethod(RegisterExtension) + + def FromString(s): + message = cls() + message.MergeFromString(s) + return message + cls.FromString = staticmethod(FromString) + + +def _IsPresent(item): + """Given a (FieldDescriptor, value) tuple from _fields, return true if the + value should be included in the list returned by ListFields().""" + + if item[0].label == _FieldDescriptor.LABEL_REPEATED: + return bool(item[1]) + elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + return item[1]._is_present_in_parent + else: + return True + + +def _AddListFieldsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ListFields(self): + all_fields = [item for item in self._fields.items() if _IsPresent(item)] + all_fields.sort(key = lambda item: item[0].number) + return all_fields + + cls.ListFields = ListFields + +_PROTO3_ERROR_TEMPLATE = \ + ('Protocol message %s has no non-repeated submessage field "%s" ' + 'nor marked as optional') +_PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field "%s"' + +def _AddHasFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + is_proto3 = (message_descriptor.syntax == "proto3") + error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE + + hassable_fields = {} + for field in message_descriptor.fields: + if field.label == _FieldDescriptor.LABEL_REPEATED: + continue + # For proto3, only submessages and fields inside a oneof have presence. + if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and + not field.containing_oneof): + continue + hassable_fields[field.name] = field + + # Has methods are supported for oneof descriptors. + for oneof in message_descriptor.oneofs: + hassable_fields[oneof.name] = oneof + + def HasField(self, field_name): + try: + field = hassable_fields[field_name] + except KeyError: + raise ValueError(error_msg % (message_descriptor.full_name, field_name)) + + if isinstance(field, descriptor_mod.OneofDescriptor): + try: + return HasField(self, self._oneofs[field].name) + except KeyError: + return False + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(field) + return value is not None and value._is_present_in_parent + else: + return field in self._fields + + cls.HasField = HasField + + +def _AddClearFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def ClearField(self, field_name): + try: + field = message_descriptor.fields_by_name[field_name] + except KeyError: + try: + field = message_descriptor.oneofs_by_name[field_name] + if field in self._oneofs: + field = self._oneofs[field] + else: + return + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + if field in self._fields: + # To match the C++ implementation, we need to invalidate iterators + # for map fields when ClearField() happens. + if hasattr(self._fields[field], 'InvalidateIterators'): + self._fields[field].InvalidateIterators() + + # Note: If the field is a sub-message, its listener will still point + # at us. That's fine, because the worst than can happen is that it + # will call _Modified() and invalidate our byte size. Big deal. + del self._fields[field] + + if self._oneofs.get(field.containing_oneof, None) is field: + del self._oneofs[field.containing_oneof] + + # Always call _Modified() -- even if nothing was changed, this is + # a mutating method, and thus calling it should cause the field to become + # present in the parent message. + self._Modified() + + cls.ClearField = ClearField + + +def _AddClearExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def ClearExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + + # Similar to ClearField(), above. + if extension_handle in self._fields: + del self._fields[extension_handle] + self._Modified() + cls.ClearExtension = ClearExtension + + +def _AddHasExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def HasExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + if extension_handle.label == _FieldDescriptor.LABEL_REPEATED: + raise KeyError('"%s" is repeated.' % extension_handle.full_name) + + if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(extension_handle) + return value is not None and value._is_present_in_parent + else: + return extension_handle in self._fields + cls.HasExtension = HasExtension + +def _InternalUnpackAny(msg): + """Unpacks Any message and returns the unpacked message. + + This internal method is different from public Any Unpack method which takes + the target message as argument. _InternalUnpackAny method does not have + target message type and need to find the message type in descriptor pool. + + Args: + msg: An Any message to be unpacked. + + Returns: + The unpacked message. + """ + # TODO(amauryfa): Don't use the factory of generated messages. + # To make Any work with custom factories, use the message factory of the + # parent message. + # pylint: disable=g-import-not-at-top + from google.protobuf import symbol_database + factory = symbol_database.Default() + + type_url = msg.type_url + + if not type_url: + return None + + # TODO(haberman): For now we just strip the hostname. Better logic will be + # required. + type_name = type_url.split('/')[-1] + descriptor = factory.pool.FindMessageTypeByName(type_name) + + if descriptor is None: + return None + + message_class = factory.GetPrototype(descriptor) + message = message_class() + + message.ParseFromString(msg.value) + return message + + +def _AddEqualsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __eq__(self, other): + if (not isinstance(other, message_mod.Message) or + other.DESCRIPTOR != self.DESCRIPTOR): + return False + + if self is other: + return True + + if self.DESCRIPTOR.full_name == _AnyFullTypeName: + any_a = _InternalUnpackAny(self) + any_b = _InternalUnpackAny(other) + if any_a and any_b: + return any_a == any_b + + if not self.ListFields() == other.ListFields(): + return False + + # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, + # then use it for the comparison. + unknown_fields = list(self._unknown_fields) + unknown_fields.sort() + other_unknown_fields = list(other._unknown_fields) + other_unknown_fields.sort() + return unknown_fields == other_unknown_fields + + cls.__eq__ = __eq__ + + +def _AddStrMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __str__(self): + return text_format.MessageToString(self) + cls.__str__ = __str__ + + +def _AddReprMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __repr__(self): + return text_format.MessageToString(self) + cls.__repr__ = __repr__ + + +def _AddUnicodeMethod(unused_message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def __unicode__(self): + return text_format.MessageToString(self, as_utf8=True).decode('utf-8') + cls.__unicode__ = __unicode__ + + +def _BytesForNonRepeatedElement(value, field_number, field_type): + """Returns the number of bytes needed to serialize a non-repeated element. + The returned byte count includes space for tag information and any + other additional space associated with serializing value. + + Args: + value: Value we're serializing. + field_number: Field number of this value. (Since the field number + is stored as part of a varint-encoded tag, this has an impact + on the total bytes required to serialize the value). + field_type: The type of the field. One of the TYPE_* constants + within FieldDescriptor. + """ + try: + fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] + return fn(field_number, value) + except KeyError: + raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) + + +def _AddByteSizeMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ByteSize(self): + if not self._cached_byte_size_dirty: + return self._cached_byte_size + + size = 0 + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + size = descriptor.fields_by_name['key']._sizer(self.key) + size += descriptor.fields_by_name['value']._sizer(self.value) + else: + for field_descriptor, field_value in self.ListFields(): + size += field_descriptor._sizer(field_value) + for tag_bytes, value_bytes in self._unknown_fields: + size += len(tag_bytes) + len(value_bytes) + + self._cached_byte_size = size + self._cached_byte_size_dirty = False + self._listener_for_children.dirty = False + return size + + cls.ByteSize = ByteSize + + +def _AddSerializeToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializeToString(self, **kwargs): + # Check if the message has all of its required fields set. + if not self.IsInitialized(): + raise message_mod.EncodeError( + 'Message %s is missing required fields: %s' % ( + self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) + return self.SerializePartialToString(**kwargs) + cls.SerializeToString = SerializeToString + + +def _AddSerializePartialToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializePartialToString(self, **kwargs): + out = BytesIO() + self._InternalSerialize(out.write, **kwargs) + return out.getvalue() + cls.SerializePartialToString = SerializePartialToString + + def InternalSerialize(self, write_bytes, deterministic=None): + if deterministic is None: + deterministic = ( + api_implementation.IsPythonDefaultSerializationDeterministic()) + else: + deterministic = bool(deterministic) + + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + descriptor.fields_by_name['key']._encoder( + write_bytes, self.key, deterministic) + descriptor.fields_by_name['value']._encoder( + write_bytes, self.value, deterministic) + else: + for field_descriptor, field_value in self.ListFields(): + field_descriptor._encoder(write_bytes, field_value, deterministic) + for tag_bytes, value_bytes in self._unknown_fields: + write_bytes(tag_bytes) + write_bytes(value_bytes) + cls._InternalSerialize = InternalSerialize + + +def _AddMergeFromStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def MergeFromString(self, serialized): + serialized = memoryview(serialized) + length = len(serialized) + try: + if self._InternalParse(serialized, 0, length) != length: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise message_mod.DecodeError('Unexpected end-group tag.') + except (IndexError, TypeError): + # Now ord(buf[p:p+1]) == ord('') gets TypeError. + raise message_mod.DecodeError('Truncated message.') + except struct.error as e: + raise message_mod.DecodeError(e) + return length # Return this for legacy reasons. + cls.MergeFromString = MergeFromString + + local_ReadTag = decoder.ReadTag + local_SkipField = decoder.SkipField + decoders_by_tag = cls._decoders_by_tag + + def InternalParse(self, buffer, pos, end): + """Create a message from serialized bytes. + + Args: + self: Message, instance of the proto message object. + buffer: memoryview of the serialized data. + pos: int, position to start in the serialized data. + end: int, end position of the serialized data. + + Returns: + Message object. + """ + # Guard against internal misuse, since this function is called internally + # quite extensively, and its easy to accidentally pass bytes. + assert isinstance(buffer, memoryview) + self._Modified() + field_dict = self._fields + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + while pos != end: + (tag_bytes, new_pos) = local_ReadTag(buffer, pos) + field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) + if field_decoder is None: + if not self._unknown_fields: # pylint: disable=protected-access + self._unknown_fields = [] # pylint: disable=protected-access + if unknown_field_set is None: + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + # pylint: disable=protected-access + (tag, _) = decoder._DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if field_number == 0: + raise message_mod.DecodeError('Field number 0 is illegal.') + # TODO(jieluo): remove old_pos. + old_pos = new_pos + (data, new_pos) = decoder._DecodeUnknownField( + buffer, new_pos, wire_type) # pylint: disable=protected-access + if new_pos == -1: + return pos + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + # TODO(jieluo): remove _unknown_fields. + new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) + if new_pos == -1: + return pos + self._unknown_fields.append( + (tag_bytes, buffer[old_pos:new_pos].tobytes())) + pos = new_pos + else: + pos = field_decoder(buffer, new_pos, end, self, field_dict) + if field_desc: + self._UpdateOneofState(field_desc) + return pos + cls._InternalParse = InternalParse + + +def _AddIsInitializedMethod(message_descriptor, cls): + """Adds the IsInitialized and FindInitializationError methods to the + protocol message class.""" + + required_fields = [field for field in message_descriptor.fields + if field.label == _FieldDescriptor.LABEL_REQUIRED] + + def IsInitialized(self, errors=None): + """Checks if all required fields of a message are set. + + Args: + errors: A list which, if provided, will be populated with the field + paths of all missing required fields. + + Returns: + True iff the specified message has all required fields set. + """ + + # Performance is critical so we avoid HasField() and ListFields(). + + for field in required_fields: + if (field not in self._fields or + (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and + not self._fields[field]._is_present_in_parent)): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + for field, value in list(self._fields.items()): # dict can change size! + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.label == _FieldDescriptor.LABEL_REPEATED: + if (field.message_type.has_options and + field.message_type.GetOptions().map_entry): + continue + for element in value: + if not element.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + elif value._is_present_in_parent and not value.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + return True + + cls.IsInitialized = IsInitialized + + def FindInitializationErrors(self): + """Finds required fields which are not initialized. + + Returns: + A list of strings. Each string is a path to an uninitialized field from + the top-level message, e.g. "foo.bar[5].baz". + """ + + errors = [] # simplify things + + for field in required_fields: + if not self.HasField(field.name): + errors.append(field.name) + + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + name = '(%s)' % field.full_name + else: + name = field.name + + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + element = value[key] + prefix = '%s[%s].' % (name, key) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + # ScalarMaps can't have any initialization errors. + pass + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for i in range(len(value)): + element = value[i] + prefix = '%s[%d].' % (name, i) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + prefix = name + '.' + sub_errors = value.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + + return errors + + cls.FindInitializationErrors = FindInitializationErrors + + +def _FullyQualifiedClassName(klass): + module = klass.__module__ + name = getattr(klass, '__qualname__', klass.__name__) + if module in (None, 'builtins', '__builtin__'): + return name + return module + '.' + name + + +def _AddMergeFromMethod(cls): + LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED + CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE + + def MergeFrom(self, msg): + if not isinstance(msg, cls): + raise TypeError( + 'Parameter to MergeFrom() must be instance of same class: ' + 'expected %s got %s.' % (_FullyQualifiedClassName(cls), + _FullyQualifiedClassName(msg.__class__))) + + assert msg is not self + self._Modified() + + fields = self._fields + + for field, value in msg._fields.items(): + if field.label == LABEL_REPEATED: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + elif field.cpp_type == CPPTYPE_MESSAGE: + if value._is_present_in_parent: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + else: + self._fields[field] = value + if field.containing_oneof: + self._UpdateOneofState(field) + + if msg._unknown_fields: + if not self._unknown_fields: + self._unknown_fields = [] + self._unknown_fields.extend(msg._unknown_fields) + # pylint: disable=protected-access + if self._unknown_field_set is None: + self._unknown_field_set = containers.UnknownFieldSet() + self._unknown_field_set._extend(msg._unknown_field_set) + + cls.MergeFrom = MergeFrom + + +def _AddWhichOneofMethod(message_descriptor, cls): + def WhichOneof(self, oneof_name): + """Returns the name of the currently set field inside a oneof, or None.""" + try: + field = message_descriptor.oneofs_by_name[oneof_name] + except KeyError: + raise ValueError( + 'Protocol message has no oneof "%s" field.' % oneof_name) + + nested_field = self._oneofs.get(field, None) + if nested_field is not None and self.HasField(nested_field.name): + return nested_field.name + else: + return None + + cls.WhichOneof = WhichOneof + + +def _Clear(self): + # Clear fields. + self._fields = {} + self._unknown_fields = () + # pylint: disable=protected-access + if self._unknown_field_set is not None: + self._unknown_field_set._clear() + self._unknown_field_set = None + + self._oneofs = {} + self._Modified() + + +def _UnknownFields(self): + if self._unknown_field_set is None: # pylint: disable=protected-access + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + return self._unknown_field_set # pylint: disable=protected-access + + +def _DiscardUnknownFields(self): + self._unknown_fields = [] + self._unknown_field_set = None # pylint: disable=protected-access + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + value[key].DiscardUnknownFields() + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for sub_message in value: + sub_message.DiscardUnknownFields() + else: + value.DiscardUnknownFields() + + +def _SetListener(self, listener): + if listener is None: + self._listener = message_listener_mod.NullMessageListener() + else: + self._listener = listener + + +def _AddMessageMethods(message_descriptor, cls): + """Adds implementations of all Message methods to cls.""" + _AddListFieldsMethod(message_descriptor, cls) + _AddHasFieldMethod(message_descriptor, cls) + _AddClearFieldMethod(message_descriptor, cls) + if message_descriptor.is_extendable: + _AddClearExtensionMethod(cls) + _AddHasExtensionMethod(cls) + _AddEqualsMethod(message_descriptor, cls) + _AddStrMethod(message_descriptor, cls) + _AddReprMethod(message_descriptor, cls) + _AddUnicodeMethod(message_descriptor, cls) + _AddByteSizeMethod(message_descriptor, cls) + _AddSerializeToStringMethod(message_descriptor, cls) + _AddSerializePartialToStringMethod(message_descriptor, cls) + _AddMergeFromStringMethod(message_descriptor, cls) + _AddIsInitializedMethod(message_descriptor, cls) + _AddMergeFromMethod(cls) + _AddWhichOneofMethod(message_descriptor, cls) + # Adds methods which do not depend on cls. + cls.Clear = _Clear + cls.UnknownFields = _UnknownFields + cls.DiscardUnknownFields = _DiscardUnknownFields + cls._SetListener = _SetListener + + +def _AddPrivateHelperMethods(message_descriptor, cls): + """Adds implementation of private helper methods to cls.""" + + def Modified(self): + """Sets the _cached_byte_size_dirty bit to true, + and propagates this to our listener iff this was a state change. + """ + + # Note: Some callers check _cached_byte_size_dirty before calling + # _Modified() as an extra optimization. So, if this method is ever + # changed such that it does stuff even when _cached_byte_size_dirty is + # already true, the callers need to be updated. + if not self._cached_byte_size_dirty: + self._cached_byte_size_dirty = True + self._listener_for_children.dirty = True + self._is_present_in_parent = True + self._listener.Modified() + + def _UpdateOneofState(self, field): + """Sets field as the active field in its containing oneof. + + Will also delete currently active field in the oneof, if it is different + from the argument. Does not mark the message as modified. + """ + other_field = self._oneofs.setdefault(field.containing_oneof, field) + if other_field is not field: + del self._fields[other_field] + self._oneofs[field.containing_oneof] = field + + cls._Modified = Modified + cls.SetInParent = Modified + cls._UpdateOneofState = _UpdateOneofState + + +class _Listener(object): + + """MessageListener implementation that a parent message registers with its + child message. + + In order to support semantics like: + + foo.bar.baz.qux = 23 + assert foo.HasField('bar') + + ...child objects must have back references to their parents. + This helper class is at the heart of this support. + """ + + def __init__(self, parent_message): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + """ + # This listener establishes a back reference from a child (contained) object + # to its parent (containing) object. We make this a weak reference to avoid + # creating cyclic garbage when the client finishes with the 'parent' object + # in the tree. + if isinstance(parent_message, weakref.ProxyType): + self._parent_message_weakref = parent_message + else: + self._parent_message_weakref = weakref.proxy(parent_message) + + # As an optimization, we also indicate directly on the listener whether + # or not the parent message is dirty. This way we can avoid traversing + # up the tree in the common case. + self.dirty = False + + def Modified(self): + if self.dirty: + return + try: + # Propagate the signal to our parents iff this is the first field set. + self._parent_message_weakref._Modified() + except ReferenceError: + # We can get here if a client has kept a reference to a child object, + # and is now setting a field on it, but the child's parent has been + # garbage-collected. This is not an error. + pass + + +class _OneofListener(_Listener): + """Special listener implementation for setting composite oneof fields.""" + + def __init__(self, parent_message, field): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + field: The descriptor of the field being set in the parent message. + """ + super(_OneofListener, self).__init__(parent_message) + self._field = field + + def Modified(self): + """Also updates the state of the containing oneof in the parent message.""" + try: + self._parent_message_weakref._UpdateOneofState(self._field) + super(_OneofListener, self).Modified() + except ReferenceError: + pass diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py new file mode 100644 index 0000000000..a53e71fe8e --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py @@ -0,0 +1,435 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides type checking routines. + +This module defines type checking utilities in the forms of dictionaries: + +VALUE_CHECKERS: A dictionary of field types and a value validation object. +TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing + function. +TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization + function. +FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their + corresponding wire types. +TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization + function. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import ctypes +import numbers + +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import descriptor + +_FieldDescriptor = descriptor.FieldDescriptor + + +def TruncateToFourByteFloat(original): + return ctypes.c_float(original).value + + +def ToShortestFloat(original): + """Returns the shortest float that has same value in wire.""" + # All 4 byte floats have between 6 and 9 significant digits, so we + # start with 6 as the lower bound. + # It has to be iterative because use '.9g' directly can not get rid + # of the noises for most values. For example if set a float_field=0.9 + # use '.9g' will print 0.899999976. + precision = 6 + rounded = float('{0:.{1}g}'.format(original, precision)) + while TruncateToFourByteFloat(rounded) != original: + precision += 1 + rounded = float('{0:.{1}g}'.format(original, precision)) + return rounded + + +def SupportsOpenEnums(field_descriptor): + return field_descriptor.containing_type.syntax == 'proto3' + + +def GetTypeChecker(field): + """Returns a type checker for a message field of the specified types. + + Args: + field: FieldDescriptor object for this field. + + Returns: + An instance of TypeChecker which can be used to verify the types + of values assigned to a field of the specified type. + """ + if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and + field.type == _FieldDescriptor.TYPE_STRING): + return UnicodeValueChecker() + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + if SupportsOpenEnums(field): + # When open enums are supported, any int32 can be assigned. + return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] + else: + return EnumValueChecker(field.enum_type) + return _VALUE_CHECKERS[field.cpp_type] + + +# None of the typecheckers below make any attempt to guard against people +# subclassing builtin types and doing weird things. We're not trying to +# protect against malicious clients here, just people accidentally shooting +# themselves in the foot in obvious ways. +class TypeChecker(object): + + """Type checker used to catch type errors as early as possible + when the client is setting scalar fields in protocol messages. + """ + + def __init__(self, *acceptable_types): + self._acceptable_types = acceptable_types + + def CheckValue(self, proposed_value): + """Type check the provided value and return it. + + The returned value might have been normalized to another type. + """ + if not isinstance(proposed_value, self._acceptable_types): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), self._acceptable_types)) + raise TypeError(message) + return proposed_value + + +class TypeCheckerWithDefault(TypeChecker): + + def __init__(self, default_value, *acceptable_types): + TypeChecker.__init__(self, *acceptable_types) + self._default_value = default_value + + def DefaultValue(self): + return self._default_value + + +class BoolValueChecker(object): + """Type checker used for bool fields.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bool, int))) + raise TypeError(message) + return bool(proposed_value) + + def DefaultValue(self): + return False + + +# IntValueChecker and its subclasses perform integer type-checks +# and bounds-checks. +class IntValueChecker(object): + + """Checker used for integer fields. Performs type-check and range check.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + + if not self._MIN <= int(proposed_value) <= self._MAX: + raise ValueError('Value out of range: %d' % proposed_value) + # We force all values to int to make alternate implementations where the + # distinction is more significant (e.g. the C++ implementation) simpler. + proposed_value = int(proposed_value) + return proposed_value + + def DefaultValue(self): + return 0 + + +class EnumValueChecker(object): + + """Checker used for enum fields. Performs type-check and range check.""" + + def __init__(self, enum_type): + self._enum_type = enum_type + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, numbers.Integral): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + if int(proposed_value) not in self._enum_type.values_by_number: + raise ValueError('Unknown enum value: %d' % proposed_value) + return proposed_value + + def DefaultValue(self): + return self._enum_type.values[0].number + + +class UnicodeValueChecker(object): + + """Checker used for string fields. + + Always returns a unicode value, even if the input is of type str. + """ + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, (bytes, str)): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bytes, str))) + raise TypeError(message) + + # If the value is of type 'bytes' make sure that it is valid UTF-8 data. + if isinstance(proposed_value, bytes): + try: + proposed_value = proposed_value.decode('utf-8') + except UnicodeDecodeError: + raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' + 'encoding. Non-UTF-8 strings must be converted to ' + 'unicode objects before being added.' % + (proposed_value)) + else: + try: + proposed_value.encode('utf8') + except UnicodeEncodeError: + raise ValueError('%.1024r isn\'t a valid unicode string and ' + 'can\'t be encoded in UTF-8.'% + (proposed_value)) + + return proposed_value + + def DefaultValue(self): + return u"" + + +class Int32ValueChecker(IntValueChecker): + # We're sure to use ints instead of longs here since comparison may be more + # efficient. + _MIN = -2147483648 + _MAX = 2147483647 + + +class Uint32ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 32) - 1 + + +class Int64ValueChecker(IntValueChecker): + _MIN = -(1 << 63) + _MAX = (1 << 63) - 1 + + +class Uint64ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 64) - 1 + + +# The max 4 bytes float is about 3.4028234663852886e+38 +_FLOAT_MAX = float.fromhex('0x1.fffffep+127') +_FLOAT_MIN = -_FLOAT_MAX +_INF = float('inf') +_NEG_INF = float('-inf') + + +class DoubleValueChecker(object): + """Checker used for double fields. + + Performs type-check and range check. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + if (not hasattr(proposed_value, '__float__') and + not hasattr(proposed_value, '__index__')) or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: int, float' % + (proposed_value, type(proposed_value))) + raise TypeError(message) + return float(proposed_value) + + def DefaultValue(self): + return 0.0 + + +class FloatValueChecker(DoubleValueChecker): + """Checker used for float fields. + + Performs type-check and range check. + + Values exceeding a 32-bit float will be converted to inf/-inf. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + converted_value = super().CheckValue(proposed_value) + # This inf rounding matches the C++ proto SafeDoubleToFloat logic. + if converted_value > _FLOAT_MAX: + return _INF + if converted_value < _FLOAT_MIN: + return _NEG_INF + + return TruncateToFourByteFloat(converted_value) + +# Type-checkers for all scalar CPPTYPEs. +_VALUE_CHECKERS = { + _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), + _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), + _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), + _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), + _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), + _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), +} + + +# Map from field type to a function F, such that F(field_num, value) +# gives the total byte size for a value of the given type. This +# byte size includes tag information and any other additional space +# associated with serializing "value". +TYPE_TO_BYTE_SIZE_FN = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, + _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, + _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, + _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, + _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, + _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, + _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, + _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, + _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, + _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, + _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, + _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, + _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, + _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, + _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, + _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, + _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, + _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize + } + + +# Maps from field types to encoder constructors. +TYPE_TO_ENCODER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, + _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, + _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, + _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, + _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, + _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, + _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, + _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, + } + + +# Maps from field types to sizer constructors. +TYPE_TO_SIZER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, + _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, + _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, + _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, + _FieldDescriptor.TYPE_STRING: encoder.StringSizer, + _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, + _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, + _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, + } + + +# Maps from field type to a decoder constructor. +TYPE_TO_DECODER = { + _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, + _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, + _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, + _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, + _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, + _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, + _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, + _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, + _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, + _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, + _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, + _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, + _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, + _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, + _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, + _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, + _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, + _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, + } + +# Maps from field type to expected wiretype. +FIELD_TYPE_TO_WIRE_TYPE = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_STRING: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, + _FieldDescriptor.TYPE_MESSAGE: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_BYTES: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, + } diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py new file mode 100644 index 0000000000..b581ab750a --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py @@ -0,0 +1,878 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains well known classes. + +This files defines well known classes which need extra maintenance including: + - Any + - Duration + - FieldMask + - Struct + - Timestamp +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +import calendar +import collections.abc +import datetime + +from google.protobuf.descriptor import FieldDescriptor + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_NANOS_PER_SECOND = 1000000000 +_NANOS_PER_MILLISECOND = 1000000 +_NANOS_PER_MICROSECOND = 1000 +_MILLIS_PER_SECOND = 1000 +_MICROS_PER_SECOND = 1000000 +_SECONDS_PER_DAY = 24 * 3600 +_DURATION_SECONDS_MAX = 315576000000 + + +class Any(object): + """Class for Any Message type.""" + + __slots__ = () + + def Pack(self, msg, type_url_prefix='type.googleapis.com/', + deterministic=None): + """Packs the specified message into current Any message.""" + if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': + self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + else: + self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + self.value = msg.SerializeToString(deterministic=deterministic) + + def Unpack(self, msg): + """Unpacks the current Any message into specified message.""" + descriptor = msg.DESCRIPTOR + if not self.Is(descriptor): + return False + msg.ParseFromString(self.value) + return True + + def TypeName(self): + """Returns the protobuf type name of the inner message.""" + # Only last part is to be used: b/25630112 + return self.type_url.split('/')[-1] + + def Is(self, descriptor): + """Checks if this Any represents the given protobuf type.""" + return '/' in self.type_url and self.TypeName() == descriptor.full_name + + +_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) +_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( + 0, tz=datetime.timezone.utc) + + +class Timestamp(object): + """Class for Timestamp message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Timestamp to RFC 3339 date string format. + + Returns: + A string converted from timestamp. The string is always Z-normalized + and uses 3, 6 or 9 fractional digits as required to represent the + exact time. Example of the return format: '1972-01-01T10:00:20.021Z' + """ + nanos = self.nanos % _NANOS_PER_SECOND + total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND + seconds = total_sec % _SECONDS_PER_DAY + days = (total_sec - seconds) // _SECONDS_PER_DAY + dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) + + result = dt.isoformat() + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 'Z' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03dZ' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06dZ' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09dZ' % nanos + + def FromJsonString(self, value): + """Parse a RFC 3339 date string format to Timestamp. + + Args: + value: A date string. Any fractional digits (or none) and any offset are + accepted as long as they fit into nano-seconds precision. + Example of accepted format: '1972-01-01T10:00:20.021-05:00' + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) + timezone_offset = value.find('Z') + if timezone_offset == -1: + timezone_offset = value.find('+') + if timezone_offset == -1: + timezone_offset = value.rfind('-') + if timezone_offset == -1: + raise ValueError( + 'Failed to parse timestamp: missing valid timezone offset.') + time_value = value[0:timezone_offset] + # Parse datetime and nanos. + point_position = time_value.find('.') + if point_position == -1: + second_value = time_value + nano_value = '' + else: + second_value = time_value[:point_position] + nano_value = time_value[point_position + 1:] + if 't' in second_value: + raise ValueError( + 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' + 'lowercase \'t\' is not accepted'.format(second_value)) + date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) + td = date_object - datetime.datetime(1970, 1, 1) + seconds = td.seconds + td.days * _SECONDS_PER_DAY + if len(nano_value) > 9: + raise ValueError( + 'Failed to parse Timestamp: nanos {0} more than ' + '9 fractional digits.'.format(nano_value)) + if nano_value: + nanos = round(float('0.' + nano_value) * 1e9) + else: + nanos = 0 + # Parse timezone offsets. + if value[timezone_offset] == 'Z': + if len(value) != timezone_offset + 1: + raise ValueError('Failed to parse timestamp: invalid trailing' + ' data {0}.'.format(value)) + else: + timezone = value[timezone_offset:] + pos = timezone.find(':') + if pos == -1: + raise ValueError( + 'Invalid timezone offset value: {0}.'.format(timezone)) + if timezone[0] == '+': + seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + else: + seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + # Set seconds and nanos + self.seconds = int(seconds) + self.nanos = int(nanos) + + def GetCurrentTime(self): + """Get the current UTC into Timestamp.""" + self.FromDatetime(datetime.datetime.utcnow()) + + def ToNanoseconds(self): + """Converts Timestamp to nanoseconds since epoch.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts Timestamp to microseconds since epoch.""" + return (self.seconds * _MICROS_PER_SECOND + + self.nanos // _NANOS_PER_MICROSECOND) + + def ToMilliseconds(self): + """Converts Timestamp to milliseconds since epoch.""" + return (self.seconds * _MILLIS_PER_SECOND + + self.nanos // _NANOS_PER_MILLISECOND) + + def ToSeconds(self): + """Converts Timestamp to seconds since epoch.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds since epoch to Timestamp.""" + self.seconds = nanos // _NANOS_PER_SECOND + self.nanos = nanos % _NANOS_PER_SECOND + + def FromMicroseconds(self, micros): + """Converts microseconds since epoch to Timestamp.""" + self.seconds = micros // _MICROS_PER_SECOND + self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + + def FromMilliseconds(self, millis): + """Converts milliseconds since epoch to Timestamp.""" + self.seconds = millis // _MILLIS_PER_SECOND + self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + + def FromSeconds(self, seconds): + """Converts seconds since epoch to Timestamp.""" + self.seconds = seconds + self.nanos = 0 + + def ToDatetime(self, tzinfo=None): + """Converts Timestamp to a datetime. + + Args: + tzinfo: A datetime.tzinfo subclass; defaults to None. + + Returns: + If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone + information, i.e. not aware that it's UTC). + + Otherwise, returns a timezone-aware datetime in the input timezone. + """ + delta = datetime.timedelta( + seconds=self.seconds, + microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) + if tzinfo is None: + return _EPOCH_DATETIME_NAIVE + delta + else: + return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta + + def FromDatetime(self, dt): + """Converts datetime to Timestamp. + + Args: + dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. + """ + # Using this guide: http://wiki.python.org/moin/WorkingWithTime + # And this conversion guide: http://docs.python.org/library/time.html + + # Turn the date parameter into a tuple (struct_time) that can then be + # manipulated into a long value of seconds. During the conversion from + # struct_time to long, the source date in UTC, and so it follows that the + # correct transformation is calendar.timegm() + self.seconds = calendar.timegm(dt.utctimetuple()) + self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND + + +class Duration(object): + """Class for Duration message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Duration to string format. + + Returns: + A string converted from self. The string format will contains + 3, 6, or 9 fractional digits depending on the precision required to + represent the exact Duration value. For example: "1s", "1.010s", + "1.000000100s", "-3.100s" + """ + _CheckDurationValid(self.seconds, self.nanos) + if self.seconds < 0 or self.nanos < 0: + result = '-' + seconds = - self.seconds + int((0 - self.nanos) // 1e9) + nanos = (0 - self.nanos) % 1e9 + else: + result = '' + seconds = self.seconds + int(self.nanos // 1e9) + nanos = self.nanos % 1e9 + result += '%d' % seconds + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 's' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03ds' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06ds' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09ds' % nanos + + def FromJsonString(self, value): + """Converts a string to Duration. + + Args: + value: A string to be converted. The string must end with 's'. Any + fractional digits (or none) are accepted as long as they fit into + precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Duration JSON value not a string: {!r}'.format(value)) + if len(value) < 1 or value[-1] != 's': + raise ValueError( + 'Duration must end with letter "s": {0}.'.format(value)) + try: + pos = value.find('.') + if pos == -1: + seconds = int(value[:-1]) + nanos = 0 + else: + seconds = int(value[:pos]) + if value[0] == '-': + nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) + else: + nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) + _CheckDurationValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + except ValueError as e: + raise ValueError( + 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) + + def ToNanoseconds(self): + """Converts a Duration to nanoseconds.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts a Duration to microseconds.""" + micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) + return self.seconds * _MICROS_PER_SECOND + micros + + def ToMilliseconds(self): + """Converts a Duration to milliseconds.""" + millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) + return self.seconds * _MILLIS_PER_SECOND + millis + + def ToSeconds(self): + """Converts a Duration to seconds.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds to Duration.""" + self._NormalizeDuration(nanos // _NANOS_PER_SECOND, + nanos % _NANOS_PER_SECOND) + + def FromMicroseconds(self, micros): + """Converts microseconds to Duration.""" + self._NormalizeDuration( + micros // _MICROS_PER_SECOND, + (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) + + def FromMilliseconds(self, millis): + """Converts milliseconds to Duration.""" + self._NormalizeDuration( + millis // _MILLIS_PER_SECOND, + (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) + + def FromSeconds(self, seconds): + """Converts seconds to Duration.""" + self.seconds = seconds + self.nanos = 0 + + def ToTimedelta(self): + """Converts Duration to timedelta.""" + return datetime.timedelta( + seconds=self.seconds, microseconds=_RoundTowardZero( + self.nanos, _NANOS_PER_MICROSECOND)) + + def FromTimedelta(self, td): + """Converts timedelta to Duration.""" + self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, + td.microseconds * _NANOS_PER_MICROSECOND) + + def _NormalizeDuration(self, seconds, nanos): + """Set Duration by seconds and nanos.""" + # Force nanos to be negative if the duration is negative. + if seconds < 0 and nanos > 0: + seconds += 1 + nanos -= _NANOS_PER_SECOND + self.seconds = seconds + self.nanos = nanos + + +def _CheckDurationValid(seconds, nanos): + if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: + raise ValueError( + 'Duration is not valid: Seconds {0} must be in range ' + '[-315576000000, 315576000000].'.format(seconds)) + if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Duration is not valid: Nanos {0} must be in range ' + '[-999999999, 999999999].'.format(nanos)) + if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): + raise ValueError( + 'Duration is not valid: Sign mismatch.') + + +def _RoundTowardZero(value, divider): + """Truncates the remainder part after division.""" + # For some languages, the sign of the remainder is implementation + # dependent if any of the operands is negative. Here we enforce + # "rounded toward zero" semantics. For example, for (-5) / 2 an + # implementation may give -3 as the result with the remainder being + # 1. This function ensures we always return -2 (closer to zero). + result = value // divider + remainder = value % divider + if result < 0 and remainder > 0: + return result + 1 + else: + return result + + +class FieldMask(object): + """Class for FieldMask message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts FieldMask to string according to proto3 JSON spec.""" + camelcase_paths = [] + for path in self.paths: + camelcase_paths.append(_SnakeCaseToCamelCase(path)) + return ','.join(camelcase_paths) + + def FromJsonString(self, value): + """Converts string to FieldMask according to proto3 JSON spec.""" + if not isinstance(value, str): + raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) + self.Clear() + if value: + for path in value.split(','): + self.paths.append(_CamelCaseToSnakeCase(path)) + + def IsValidForDescriptor(self, message_descriptor): + """Checks whether the FieldMask is valid for Message Descriptor.""" + for path in self.paths: + if not _IsValidPath(message_descriptor, path): + return False + return True + + def AllFieldsFromDescriptor(self, message_descriptor): + """Gets all direct fields of Message Descriptor to FieldMask.""" + self.Clear() + for field in message_descriptor.fields: + self.paths.append(field.name) + + def CanonicalFormFromMask(self, mask): + """Converts a FieldMask to the canonical form. + + Removes paths that are covered by another path. For example, + "foo.bar" is covered by "foo" and will be removed if "foo" + is also in the FieldMask. Then sorts all paths in alphabetical order. + + Args: + mask: The original FieldMask to be converted. + """ + tree = _FieldMaskTree(mask) + tree.ToFieldMask(self) + + def Union(self, mask1, mask2): + """Merges mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + tree.MergeFromFieldMask(mask2) + tree.ToFieldMask(self) + + def Intersect(self, mask1, mask2): + """Intersects mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + intersection = _FieldMaskTree() + for path in mask2.paths: + tree.IntersectPath(path, intersection) + intersection.ToFieldMask(self) + + def MergeMessage( + self, source, destination, + replace_message_field=False, replace_repeated_field=False): + """Merges fields specified in FieldMask from source to destination. + + Args: + source: Source message. + destination: The destination message to be merged into. + replace_message_field: Replace message field if True. Merge message + field if False. + replace_repeated_field: Replace repeated field if True. Append + elements of repeated field if False. + """ + tree = _FieldMaskTree(self) + tree.MergeMessage( + source, destination, replace_message_field, replace_repeated_field) + + +def _IsValidPath(message_descriptor, path): + """Checks whether the path is valid for Message Descriptor.""" + parts = path.split('.') + last = parts.pop() + for name in parts: + field = message_descriptor.fields_by_name.get(name) + if (field is None or + field.label == FieldDescriptor.LABEL_REPEATED or + field.type != FieldDescriptor.TYPE_MESSAGE): + return False + message_descriptor = field.message_type + return last in message_descriptor.fields_by_name + + +def _CheckFieldMaskMessage(message): + """Raises ValueError if message is not a FieldMask.""" + message_descriptor = message.DESCRIPTOR + if (message_descriptor.name != 'FieldMask' or + message_descriptor.file.name != 'google/protobuf/field_mask.proto'): + raise ValueError('Message {0} is not a FieldMask.'.format( + message_descriptor.full_name)) + + +def _SnakeCaseToCamelCase(path_name): + """Converts a path name from snake_case to camelCase.""" + result = [] + after_underscore = False + for c in path_name: + if c.isupper(): + raise ValueError( + 'Fail to print FieldMask to Json string: Path name ' + '{0} must not contain uppercase letters.'.format(path_name)) + if after_underscore: + if c.islower(): + result.append(c.upper()) + after_underscore = False + else: + raise ValueError( + 'Fail to print FieldMask to Json string: The ' + 'character after a "_" must be a lowercase letter ' + 'in path name {0}.'.format(path_name)) + elif c == '_': + after_underscore = True + else: + result += c + + if after_underscore: + raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' + 'in path name {0}.'.format(path_name)) + return ''.join(result) + + +def _CamelCaseToSnakeCase(path_name): + """Converts a field name from camelCase to snake_case.""" + result = [] + for c in path_name: + if c == '_': + raise ValueError('Fail to parse FieldMask: Path name ' + '{0} must not contain "_"s.'.format(path_name)) + if c.isupper(): + result += '_' + result += c.lower() + else: + result += c + return ''.join(result) + + +class _FieldMaskTree(object): + """Represents a FieldMask in a tree structure. + + For example, given a FieldMask "foo.bar,foo.baz,bar.baz", + the FieldMaskTree will be: + [_root] -+- foo -+- bar + | | + | +- baz + | + +- bar --- baz + In the tree, each leaf node represents a field path. + """ + + __slots__ = ('_root',) + + def __init__(self, field_mask=None): + """Initializes the tree by FieldMask.""" + self._root = {} + if field_mask: + self.MergeFromFieldMask(field_mask) + + def MergeFromFieldMask(self, field_mask): + """Merges a FieldMask to the tree.""" + for path in field_mask.paths: + self.AddPath(path) + + def AddPath(self, path): + """Adds a field path into the tree. + + If the field path to add is a sub-path of an existing field path + in the tree (i.e., a leaf node), it means the tree already matches + the given path so nothing will be added to the tree. If the path + matches an existing non-leaf node in the tree, that non-leaf node + will be turned into a leaf node with all its children removed because + the path matches all the node's children. Otherwise, a new path will + be added. + + Args: + path: The field path to add. + """ + node = self._root + for name in path.split('.'): + if name not in node: + node[name] = {} + elif not node[name]: + # Pre-existing empty node implies we already have this entire tree. + return + node = node[name] + # Remove any sub-trees we might have had. + node.clear() + + def ToFieldMask(self, field_mask): + """Converts the tree to a FieldMask.""" + field_mask.Clear() + _AddFieldPaths(self._root, '', field_mask) + + def IntersectPath(self, path, intersection): + """Calculates the intersection part of a field path with this tree. + + Args: + path: The field path to calculates. + intersection: The out tree to record the intersection part. + """ + node = self._root + for name in path.split('.'): + if name not in node: + return + elif not node[name]: + intersection.AddPath(path) + return + node = node[name] + intersection.AddLeafNodes(path, node) + + def AddLeafNodes(self, prefix, node): + """Adds leaf nodes begin with prefix to this tree.""" + if not node: + self.AddPath(prefix) + for name in node: + child_path = prefix + '.' + name + self.AddLeafNodes(child_path, node[name]) + + def MergeMessage( + self, source, destination, + replace_message, replace_repeated): + """Merge all fields specified by this tree from source to destination.""" + _MergeMessage( + self._root, source, destination, replace_message, replace_repeated) + + +def _StrConvert(value): + """Converts value to str if it is not.""" + # This file is imported by c extension and some methods like ClearField + # requires string for the field name. py2/py3 has different text + # type and may use unicode. + if not isinstance(value, str): + return value.encode('utf-8') + return value + + +def _MergeMessage( + node, source, destination, replace_message, replace_repeated): + """Merge all fields specified by a sub-tree from source to destination.""" + source_descriptor = source.DESCRIPTOR + for name in node: + child = node[name] + field = source_descriptor.fields_by_name[name] + if field is None: + raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( + name, source_descriptor.full_name)) + if child: + # Sub-paths are only allowed for singular message fields. + if (field.label == FieldDescriptor.LABEL_REPEATED or + field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): + raise ValueError('Error: Field {0} in message {1} is not a singular ' + 'message field and cannot have sub-fields.'.format( + name, source_descriptor.full_name)) + if source.HasField(name): + _MergeMessage( + child, getattr(source, name), getattr(destination, name), + replace_message, replace_repeated) + continue + if field.label == FieldDescriptor.LABEL_REPEATED: + if replace_repeated: + destination.ClearField(_StrConvert(name)) + repeated_source = getattr(source, name) + repeated_destination = getattr(destination, name) + repeated_destination.MergeFrom(repeated_source) + else: + if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + if replace_message: + destination.ClearField(_StrConvert(name)) + if source.HasField(name): + getattr(destination, name).MergeFrom(getattr(source, name)) + else: + setattr(destination, name, getattr(source, name)) + + +def _AddFieldPaths(node, prefix, field_mask): + """Adds the field paths descended from node to field_mask.""" + if not node and prefix: + field_mask.paths.append(prefix) + return + for name in sorted(node): + if prefix: + child_path = prefix + '.' + name + else: + child_path = name + _AddFieldPaths(node[name], child_path, field_mask) + + +def _SetStructValue(struct_value, value): + if value is None: + struct_value.null_value = 0 + elif isinstance(value, bool): + # Note: this check must come before the number check because in Python + # True and False are also considered numbers. + struct_value.bool_value = value + elif isinstance(value, str): + struct_value.string_value = value + elif isinstance(value, (int, float)): + struct_value.number_value = value + elif isinstance(value, (dict, Struct)): + struct_value.struct_value.Clear() + struct_value.struct_value.update(value) + elif isinstance(value, (list, ListValue)): + struct_value.list_value.Clear() + struct_value.list_value.extend(value) + else: + raise ValueError('Unexpected type') + + +def _GetStructValue(struct_value): + which = struct_value.WhichOneof('kind') + if which == 'struct_value': + return struct_value.struct_value + elif which == 'null_value': + return None + elif which == 'number_value': + return struct_value.number_value + elif which == 'string_value': + return struct_value.string_value + elif which == 'bool_value': + return struct_value.bool_value + elif which == 'list_value': + return struct_value.list_value + elif which is None: + raise ValueError('Value not set') + + +class Struct(object): + """Class for Struct message type.""" + + __slots__ = () + + def __getitem__(self, key): + return _GetStructValue(self.fields[key]) + + def __contains__(self, item): + return item in self.fields + + def __setitem__(self, key, value): + _SetStructValue(self.fields[key], value) + + def __delitem__(self, key): + del self.fields[key] + + def __len__(self): + return len(self.fields) + + def __iter__(self): + return iter(self.fields) + + def keys(self): # pylint: disable=invalid-name + return self.fields.keys() + + def values(self): # pylint: disable=invalid-name + return [self[key] for key in self] + + def items(self): # pylint: disable=invalid-name + return [(key, self[key]) for key in self] + + def get_or_create_list(self, key): + """Returns a list for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('list_value'): + # Clear will mark list_value modified which will indeed create a list. + self.fields[key].list_value.Clear() + return self.fields[key].list_value + + def get_or_create_struct(self, key): + """Returns a struct for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('struct_value'): + # Clear will mark struct_value modified which will indeed create a struct. + self.fields[key].struct_value.Clear() + return self.fields[key].struct_value + + def update(self, dictionary): # pylint: disable=invalid-name + for key, value in dictionary.items(): + _SetStructValue(self.fields[key], value) + +collections.abc.MutableMapping.register(Struct) + + +class ListValue(object): + """Class for ListValue message type.""" + + __slots__ = () + + def __len__(self): + return len(self.values) + + def append(self, value): + _SetStructValue(self.values.add(), value) + + def extend(self, elem_seq): + for value in elem_seq: + self.append(value) + + def __getitem__(self, index): + """Retrieves item by the specified index.""" + return _GetStructValue(self.values.__getitem__(index)) + + def __setitem__(self, index, value): + _SetStructValue(self.values.__getitem__(index), value) + + def __delitem__(self, key): + del self.values[key] + + def items(self): + for i in range(len(self)): + yield self[i] + + def add_struct(self): + """Appends and returns a struct value as the next value in the list.""" + struct_value = self.values.add().struct_value + # Clear will mark struct_value modified which will indeed create a struct. + struct_value.Clear() + return struct_value + + def add_list(self): + """Appends and returns a list value as the next value in the list.""" + list_value = self.values.add().list_value + # Clear will mark list_value modified which will indeed create a list. + list_value.Clear() + return list_value + +collections.abc.MutableSequence.register(ListValue) + + +WKTBASES = { + 'google.protobuf.Any': Any, + 'google.protobuf.Duration': Duration, + 'google.protobuf.FieldMask': FieldMask, + 'google.protobuf.ListValue': ListValue, + 'google.protobuf.Struct': Struct, + 'google.protobuf.Timestamp': Timestamp, +} diff --git a/openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py b/openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py new file mode 100644 index 0000000000..883f525585 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py @@ -0,0 +1,268 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Constants and static functions to support protocol buffer wire format.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import struct +from google.protobuf import descriptor +from google.protobuf import message + + +TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. +TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 + +# These numbers identify the wire type of a protocol buffer value. +# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded +# tag-and-type to store one of these WIRETYPE_* constants. +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_VARINT = 0 +WIRETYPE_FIXED64 = 1 +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 +WIRETYPE_END_GROUP = 4 +WIRETYPE_FIXED32 = 5 +_WIRETYPE_MAX = 5 + + +# Bounds for various integer types. +INT32_MAX = int((1 << 31) - 1) +INT32_MIN = int(-(1 << 31)) +UINT32_MAX = (1 << 32) - 1 + +INT64_MAX = (1 << 63) - 1 +INT64_MIN = -(1 << 63) +UINT64_MAX = (1 << 64) - 1 + +# "struct" format strings that will encode/decode the specified formats. +FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) + + +def ZigZagEncode(value): + """ZigZag Transform: Encodes signed integers so that they can be + effectively used with varint encoding. See wire_format.h for + more details. + """ + if value >= 0: + return value << 1 + return (value << 1) ^ (~0) + + +def ZigZagDecode(value): + """Inverse of ZigZagEncode().""" + if not value & 0x1: + return value >> 1 + return (value >> 1) ^ (~0) + + + +# The *ByteSize() functions below return the number of bytes required to +# serialize "field number + type" information and then serialize the value. + + +def Int32ByteSize(field_number, int32): + return Int64ByteSize(field_number, int32) + + +def Int32ByteSizeNoTag(int32): + return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) + + +def Int64ByteSize(field_number, int64): + # Have to convert to uint before calling UInt64ByteSize(). + return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) + + +def UInt32ByteSize(field_number, uint32): + return UInt64ByteSize(field_number, uint32) + + +def UInt64ByteSize(field_number, uint64): + return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) + + +def SInt32ByteSize(field_number, int32): + return UInt32ByteSize(field_number, ZigZagEncode(int32)) + + +def SInt64ByteSize(field_number, int64): + return UInt64ByteSize(field_number, ZigZagEncode(int64)) + + +def Fixed32ByteSize(field_number, fixed32): + return TagByteSize(field_number) + 4 + + +def Fixed64ByteSize(field_number, fixed64): + return TagByteSize(field_number) + 8 + + +def SFixed32ByteSize(field_number, sfixed32): + return TagByteSize(field_number) + 4 + + +def SFixed64ByteSize(field_number, sfixed64): + return TagByteSize(field_number) + 8 + + +def FloatByteSize(field_number, flt): + return TagByteSize(field_number) + 4 + + +def DoubleByteSize(field_number, double): + return TagByteSize(field_number) + 8 + + +def BoolByteSize(field_number, b): + return TagByteSize(field_number) + 1 + + +def EnumByteSize(field_number, enum): + return UInt32ByteSize(field_number, enum) + + +def StringByteSize(field_number, string): + return BytesByteSize(field_number, string.encode('utf-8')) + + +def BytesByteSize(field_number, b): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(len(b)) + + len(b)) + + +def GroupByteSize(field_number, message): + return (2 * TagByteSize(field_number) # START and END group. + + message.ByteSize()) + + +def MessageByteSize(field_number, message): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(message.ByteSize()) + + message.ByteSize()) + + +def MessageSetItemByteSize(field_number, msg): + # First compute the sizes of the tags. + # There are 2 tags for the beginning and ending of the repeated group, that + # is field number 1, one with field number 2 (type_id) and one with field + # number 3 (message). + total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) + + # Add the number of bytes for type_id. + total_size += _VarUInt64ByteSizeNoTag(field_number) + + message_size = msg.ByteSize() + + # The number of bytes for encoding the length of the message. + total_size += _VarUInt64ByteSizeNoTag(message_size) + + # The size of the message. + total_size += message_size + return total_size + + +def TagByteSize(field_number): + """Returns the bytes required to serialize a tag with this field number.""" + # Just pass in type 0, since the type won't affect the tag+type size. + return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) + + +# Private helper function for the *ByteSize() functions above. + +def _VarUInt64ByteSizeNoTag(uint64): + """Returns the number of bytes required to serialize a single varint + using boundary value comparisons. (unrolled loop optimization -WPierce) + uint64 must be unsigned. + """ + if uint64 <= 0x7f: return 1 + if uint64 <= 0x3fff: return 2 + if uint64 <= 0x1fffff: return 3 + if uint64 <= 0xfffffff: return 4 + if uint64 <= 0x7ffffffff: return 5 + if uint64 <= 0x3ffffffffff: return 6 + if uint64 <= 0x1ffffffffffff: return 7 + if uint64 <= 0xffffffffffffff: return 8 + if uint64 <= 0x7fffffffffffffff: return 9 + if uint64 > UINT64_MAX: + raise message.EncodeError('Value out of range: %d' % uint64) + return 10 + + +NON_PACKABLE_TYPES = ( + descriptor.FieldDescriptor.TYPE_STRING, + descriptor.FieldDescriptor.TYPE_GROUP, + descriptor.FieldDescriptor.TYPE_MESSAGE, + descriptor.FieldDescriptor.TYPE_BYTES +) + + +def IsTypePackable(field_type): + """Return true iff packable = true is valid for fields of this type. + + Args: + field_type: a FieldDescriptor::Type value. + + Returns: + True iff fields of this type are packable. + """ + return field_type not in NON_PACKABLE_TYPES diff --git a/openpype/hosts/hiero/vendor/google/protobuf/json_format.py b/openpype/hosts/hiero/vendor/google/protobuf/json_format.py new file mode 100644 index 0000000000..5024ed89d7 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/json_format.py @@ -0,0 +1,912 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in JSON format. + +Simple usage example: + + # Create a proto object and serialize it to a json format string. + message = my_proto_pb2.MyMessage(foo='bar') + json_string = json_format.MessageToJson(message) + + # Parse a json format string to proto object. + message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + + +import base64 +from collections import OrderedDict +import json +import math +from operator import methodcaller +import re +import sys + +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import symbol_database + + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, + descriptor.FieldDescriptor.CPPTYPE_UINT32, + descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, + descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) +_INFINITY = 'Infinity' +_NEG_INFINITY = '-Infinity' +_NAN = 'NaN' + +_UNPAIRED_SURROGATE_PATTERN = re.compile( + u'[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: + raise ParseError('Message too deep. Max recursion depth is {0}'.format( + self.max_recursion_depth)) + message_descriptor = message.DESCRIPTOR + full_name = message_descriptor.full_name + if not path: + path = message_descriptor.name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value, message, path) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) + else: + self._ConvertFieldValuePair(value, message, path) + self.recursion_depth -= 1 + + def _ConvertFieldValuePair(self, js, message, path): + """Convert field value pairs into regular message. + + Args: + js: A JSON object to convert the field value pairs. + message: A regular protocol message to record the data. + path: parent path to log parse error info. + + Raises: + ParseError: In case of problems converting. + """ + names = [] + message_descriptor = message.DESCRIPTOR + fields_by_json_name = dict((f.json_name, f) + for f in message_descriptor.fields) + for name in js: + try: + field = fields_by_json_name.get(name, None) + if not field: + field = message_descriptor.fields_by_name.get(name, None) + if not field and _VALID_EXTENSION_NAME.match(name): + if not message_descriptor.is_extendable: + raise ParseError( + 'Message type {0} does not have extensions at {1}'.format( + message_descriptor.full_name, path)) + identifier = name[1:-1] # strip [] brackets + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + # Try looking for extension by the message type name, dropping the + # field name following the final . separator in full_name. + identifier = '.'.join(identifier.split('.')[:-1]) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + if self.ignore_unknown_fields: + continue + raise ParseError( + ('Message type "{0}" has no field named "{1}" at "{2}".\n' + ' Available Fields(except extensions): "{3}"').format( + message_descriptor.full_name, name, path, + [f.json_name for f in message_descriptor.fields])) + if name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" fields at "{2}".'.format( + message.DESCRIPTOR.full_name, name, path)) + names.append(name) + value = js[name] + # Check no other oneof field is parsed. + if field.containing_oneof is not None and value is not None: + oneof_name = field.containing_oneof.name + if oneof_name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" oneof fields at "{2}".'.format( + message.DESCRIPTOR.full_name, oneof_name, + path)) + names.append(oneof_name) + + if value is None: + if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.message_type.full_name == 'google.protobuf.Value'): + sub_message = getattr(message, field.name) + sub_message.null_value = 0 + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM + and field.enum_type.full_name == 'google.protobuf.NullValue'): + setattr(message, field.name, 0) + else: + message.ClearField(field.name) + continue + + # Parse field value. + if _IsMapEntry(field): + message.ClearField(field.name) + self._ConvertMapFieldValue(value, message, field, + '{0}.{1}'.format(path, name)) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + message.ClearField(field.name) + if not isinstance(value, list): + raise ParseError('repeated field {0} must be in [] which is ' + '{1} at {2}'.format(name, value, path)) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + # Repeated message field. + for index, item in enumerate(value): + sub_message = getattr(message, field.name).add() + # None is a null_value in Value. + if (item is None and + sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + self.ConvertMessage(item, sub_message, + '{0}.{1}[{2}]'.format(path, name, index)) + else: + # Repeated scalar field. + for index, item in enumerate(value): + if item is None: + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + getattr(message, field.name).append( + _ConvertScalarFieldValue( + item, field, '{0}.{1}[{2}]'.format(path, name, index))) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + sub_message = message.Extensions[field] + else: + sub_message = getattr(message, field.name) + sub_message.SetInParent() + self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) + else: + if field.is_extension: + message.Extensions[field] = _ConvertScalarFieldValue( + value, field, '{0}.{1}'.format(path, name)) + else: + setattr( + message, field.name, + _ConvertScalarFieldValue(value, field, + '{0}.{1}'.format(path, name))) + except ParseError as e: + if field and field.containing_oneof is None: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + else: + raise ParseError(str(e)) + except ValueError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + except TypeError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + + def _ConvertAnyMessage(self, value, message, path): + """Convert a JSON representation into Any message.""" + if isinstance(value, dict) and not value: + return + try: + type_url = value['@type'] + except KeyError: + raise ParseError( + '@type is missing when parsing any message at {0}'.format(path)) + + try: + sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) + except TypeError as e: + raise ParseError('{0} at {1}'.format(e, path)) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value['value'], sub_message, + '{0}.value'.format(path)) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, + '{0}.value'.format(path))( + self) + else: + del value['@type'] + self._ConvertFieldValuePair(value, sub_message, path) + value['@type'] = type_url + # Sets Any message + message.value = sub_message.SerializeToString() + message.type_url = type_url + + def _ConvertGenericMessage(self, value, message, path): + """Convert a JSON representation into message with FromJsonString.""" + # Duration, Timestamp, FieldMask have a FromJsonString method to do the + # conversion. Users can also call the method directly. + try: + message.FromJsonString(value) + except ValueError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + def _ConvertValueMessage(self, value, message, path): + """Convert a JSON representation into Value message.""" + if isinstance(value, dict): + self._ConvertStructMessage(value, message.struct_value, path) + elif isinstance(value, list): + self._ConvertListValueMessage(value, message.list_value, path) + elif value is None: + message.null_value = 0 + elif isinstance(value, bool): + message.bool_value = value + elif isinstance(value, str): + message.string_value = value + elif isinstance(value, _INT_OR_FLOAT): + message.number_value = value + else: + raise ParseError('Value {0} has unexpected type {1} at {2}'.format( + value, type(value), path)) + + def _ConvertListValueMessage(self, value, message, path): + """Convert a JSON representation into ListValue message.""" + if not isinstance(value, list): + raise ParseError('ListValue must be in [] which is {0} at {1}'.format( + value, path)) + message.ClearField('values') + for index, item in enumerate(value): + self._ConvertValueMessage(item, message.values.add(), + '{0}[{1}]'.format(path, index)) + + def _ConvertStructMessage(self, value, message, path): + """Convert a JSON representation into Struct message.""" + if not isinstance(value, dict): + raise ParseError('Struct must be in a dict which is {0} at {1}'.format( + value, path)) + # Clear will mark the struct as modified so it will be created even if + # there are no values. + message.Clear() + for key in value: + self._ConvertValueMessage(value[key], message.fields[key], + '{0}.{1}'.format(path, key)) + return + + def _ConvertWrapperMessage(self, value, message, path): + """Convert a JSON representation into Wrapper message.""" + field = message.DESCRIPTOR.fields_by_name['value'] + setattr( + message, 'value', + _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) + + def _ConvertMapFieldValue(self, value, message, field, path): + """Convert map field value for a message map field. + + Args: + value: A JSON object to convert the map field value. + message: A protocol message to record the converted data. + field: The descriptor of the map field to be converted. + path: parent path to log parse error info. + + Raises: + ParseError: In case of convert problems. + """ + if not isinstance(value, dict): + raise ParseError( + 'Map field {0} must be in a dict which is {1} at {2}'.format( + field.name, value, path)) + key_field = field.message_type.fields_by_name['key'] + value_field = field.message_type.fields_by_name['value'] + for key in value: + key_value = _ConvertScalarFieldValue(key, key_field, + '{0}.key'.format(path), True) + if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self.ConvertMessage(value[key], + getattr(message, field.name)[key_value], + '{0}[{1}]'.format(path, key_value)) + else: + getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( + value[key], value_field, path='{0}[{1}]'.format(path, key_value)) + + +def _ConvertScalarFieldValue(value, field, path, require_str=False): + """Convert a single scalar field value. + + Args: + value: A scalar value to convert the scalar field value. + field: The descriptor of the field to convert. + path: parent path to log parse error info. + require_str: If True, the field value must be a str. + + Returns: + The converted scalar field value + + Raises: + ParseError: In case of convert problems. + """ + try: + if field.cpp_type in _INT_TYPES: + return _ConvertInteger(value) + elif field.cpp_type in _FLOAT_TYPES: + return _ConvertFloat(value, field) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + return _ConvertBool(value, require_str) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + if isinstance(value, str): + encoded = value.encode('utf-8') + else: + encoded = value + # Add extra padding '=' + padded_value = encoded + b'=' * (4 - len(encoded) % 4) + return base64.urlsafe_b64decode(padded_value) + else: + # Checking for unpaired surrogates appears to be unreliable, + # depending on the specific Python version, so we check manually. + if _UNPAIRED_SURROGATE_PATTERN.search(value): + raise ParseError('Unpaired surrogate') + return value + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + # Convert an enum value. + enum_value = field.enum_type.values_by_name.get(value, None) + if enum_value is None: + try: + number = int(value) + enum_value = field.enum_type.values_by_number.get(number, None) + except ValueError: + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + if enum_value is None: + if field.file.syntax == 'proto3': + # Proto3 accepts unknown enums. + return number + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + return enum_value.number + except ParseError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + +def _ConvertInteger(value): + """Convert an integer. + + Args: + value: A scalar value to convert. + + Returns: + The integer value. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + if isinstance(value, float) and not value.is_integer(): + raise ParseError('Couldn\'t parse integer: {0}'.format(value)) + + if isinstance(value, str) and value.find(' ') != -1: + raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) + + if isinstance(value, bool): + raise ParseError('Bool value {0} is not acceptable for ' + 'integer field'.format(value)) + + return int(value) + + +def _ConvertFloat(value, field): + """Convert an floating point number.""" + if isinstance(value, float): + if math.isnan(value): + raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') + if math.isinf(value): + if value > 0: + raise ParseError('Couldn\'t parse Infinity or value too large, ' + 'use quoted "Infinity" instead') + else: + raise ParseError('Couldn\'t parse -Infinity or value too small, ' + 'use quoted "-Infinity" instead') + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + # pylint: disable=protected-access + if value > type_checkers._FLOAT_MAX: + raise ParseError('Float value too large') + # pylint: disable=protected-access + if value < type_checkers._FLOAT_MIN: + raise ParseError('Float value too small') + if value == 'nan': + raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') + try: + # Assume Python compatible syntax. + return float(value) + except ValueError: + # Check alternative spellings. + if value == _NEG_INFINITY: + return float('-inf') + elif value == _INFINITY: + return float('inf') + elif value == _NAN: + return float('nan') + else: + raise ParseError('Couldn\'t parse float: {0}'.format(value)) + + +def _ConvertBool(value, require_str): + """Convert a boolean value. + + Args: + value: A scalar value to convert. + require_str: If True, value must be a str. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + if require_str: + if value == 'true': + return True + elif value == 'false': + return False + else: + raise ParseError('Expected "true" or "false", not {0}'.format(value)) + + if not isinstance(value, bool): + raise ParseError('Expected true or false without quotes') + return value + +_WKTJSONMETHODS = { + 'google.protobuf.Any': ['_AnyMessageToJsonObject', + '_ConvertAnyMessage'], + 'google.protobuf.Duration': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', + '_ConvertListValueMessage'], + 'google.protobuf.Struct': ['_StructMessageToJsonObject', + '_ConvertStructMessage'], + 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.Value': ['_ValueMessageToJsonObject', + '_ConvertValueMessage'] +} diff --git a/openpype/hosts/hiero/vendor/google/protobuf/message.py b/openpype/hosts/hiero/vendor/google/protobuf/message.py new file mode 100644 index 0000000000..76c6802f70 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/message.py @@ -0,0 +1,424 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# TODO(robinson): We should just make these methods all "pure-virtual" and move +# all implementation out, into reflection.py for now. + + +"""Contains an abstract base class for protocol messages.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +class Error(Exception): + """Base error type for this module.""" + pass + + +class DecodeError(Error): + """Exception raised when deserializing messages.""" + pass + + +class EncodeError(Error): + """Exception raised when serializing messages.""" + pass + + +class Message(object): + + """Abstract base class for protocol messages. + + Protocol message classes are almost always generated by the protocol + compiler. These generated types subclass Message and implement the methods + shown below. + """ + + # TODO(robinson): Link to an HTML document here. + + # TODO(robinson): Document that instances of this class will also + # have an Extensions attribute with __getitem__ and __setitem__. + # Again, not sure how to best convey this. + + # TODO(robinson): Document that the class must also have a static + # RegisterExtension(extension_field) method. + # Not sure how to best express at this point. + + # TODO(robinson): Document these fields and methods. + + __slots__ = [] + + #: The :class:`google.protobuf.descriptor.Descriptor` for this message type. + DESCRIPTOR = None + + def __deepcopy__(self, memo=None): + clone = type(self)() + clone.MergeFrom(self) + return clone + + def __eq__(self, other_msg): + """Recursively compares two messages by value and structure.""" + raise NotImplementedError + + def __ne__(self, other_msg): + # Can't just say self != other_msg, since that would infinitely recurse. :) + return not self == other_msg + + def __hash__(self): + raise TypeError('unhashable object') + + def __str__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def __unicode__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def MergeFrom(self, other_msg): + """Merges the contents of the specified message into current message. + + This method merges the contents of the specified message into the current + message. Singular fields that are set in the specified message overwrite + the corresponding fields in the current message. Repeated fields are + appended. Singular sub-messages and groups are recursively merged. + + Args: + other_msg (Message): A message to merge into the current message. + """ + raise NotImplementedError + + def CopyFrom(self, other_msg): + """Copies the content of the specified message into the current message. + + The method clears the current message and then merges the specified + message using MergeFrom. + + Args: + other_msg (Message): A message to copy into the current one. + """ + if self is other_msg: + return + self.Clear() + self.MergeFrom(other_msg) + + def Clear(self): + """Clears all data that was set in the message.""" + raise NotImplementedError + + def SetInParent(self): + """Mark this as present in the parent. + + This normally happens automatically when you assign a field of a + sub-message, but sometimes you want to make the sub-message + present while keeping it empty. If you find yourself using this, + you may want to reconsider your design. + """ + raise NotImplementedError + + def IsInitialized(self): + """Checks if the message is initialized. + + Returns: + bool: The method returns True if the message is initialized (i.e. all of + its required fields are set). + """ + raise NotImplementedError + + # TODO(robinson): MergeFromString() should probably return None and be + # implemented in terms of a helper that returns the # of bytes read. Our + # deserialization routines would use the helper when recursively + # deserializing, but the end user would almost always just want the no-return + # MergeFromString(). + + def MergeFromString(self, serialized): + """Merges serialized protocol buffer data into this message. + + When we find a field in `serialized` that is already present + in this message: + + - If it's a "repeated" field, we append to the end of our list. + - Else, if it's a scalar, we overwrite our field. + - Else, (it's a nonrepeated composite), we recursively merge + into the existing composite. + + Args: + serialized (bytes): Any object that allows us to call + ``memoryview(serialized)`` to access a string of bytes using the + buffer interface. + + Returns: + int: The number of bytes read from `serialized`. + For non-group messages, this will always be `len(serialized)`, + but for messages which are actually groups, this will + generally be less than `len(serialized)`, since we must + stop when we reach an ``END_GROUP`` tag. Note that if + we *do* stop because of an ``END_GROUP`` tag, the number + of bytes returned does not include the bytes + for the ``END_GROUP`` tag information. + + Raises: + DecodeError: if the input cannot be parsed. + """ + # TODO(robinson): Document handling of unknown fields. + # TODO(robinson): When we switch to a helper, this will return None. + raise NotImplementedError + + def ParseFromString(self, serialized): + """Parse serialized protocol buffer data into this message. + + Like :func:`MergeFromString()`, except we clear the object first. + + Raises: + message.DecodeError if the input cannot be parsed. + """ + self.Clear() + return self.MergeFromString(serialized) + + def SerializeToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + A binary string representation of the message if all of the required + fields in the message are set (i.e. the message is initialized). + + Raises: + EncodeError: if the message isn't initialized (see :func:`IsInitialized`). + """ + raise NotImplementedError + + def SerializePartialToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + This method is similar to SerializeToString but doesn't check if the + message is initialized. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + bytes: A serialized representation of the partial message. + """ + raise NotImplementedError + + # TODO(robinson): Decide whether we like these better + # than auto-generated has_foo() and clear_foo() methods + # on the instances themselves. This way is less consistent + # with C++, but it makes reflection-type access easier and + # reduces the number of magically autogenerated things. + # + # TODO(robinson): Be sure to document (and test) exactly + # which field names are accepted here. Are we case-sensitive? + # What do we do with fields that share names with Python keywords + # like 'lambda' and 'yield'? + # + # nnorwitz says: + # """ + # Typically (in python), an underscore is appended to names that are + # keywords. So they would become lambda_ or yield_. + # """ + def ListFields(self): + """Returns a list of (FieldDescriptor, value) tuples for present fields. + + A message field is non-empty if HasField() would return true. A singular + primitive field is non-empty if HasField() would return true in proto2 or it + is non zero in proto3. A repeated field is non-empty if it contains at least + one element. The fields are ordered by field number. + + Returns: + list[tuple(FieldDescriptor, value)]: field descriptors and values + for all fields in the message which are not empty. The values vary by + field type. + """ + raise NotImplementedError + + def HasField(self, field_name): + """Checks if a certain field is set for the message. + + For a oneof group, checks if any field inside is set. Note that if the + field_name is not defined in the message descriptor, :exc:`ValueError` will + be raised. + + Args: + field_name (str): The name of the field to check for presence. + + Returns: + bool: Whether a value has been set for the named field. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def ClearField(self, field_name): + """Clears the contents of a given field. + + Inside a oneof group, clears the field set. If the name neither refers to a + defined field or oneof group, :exc:`ValueError` is raised. + + Args: + field_name (str): The name of the field to check for presence. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def WhichOneof(self, oneof_group): + """Returns the name of the field that is set inside a oneof group. + + If no field is set, returns None. + + Args: + oneof_group (str): the name of the oneof group to check. + + Returns: + str or None: The name of the group that is set, or None. + + Raises: + ValueError: no group with the given name exists + """ + raise NotImplementedError + + def HasExtension(self, extension_handle): + """Checks if a certain extension is present for this message. + + Extensions are retrieved using the :attr:`Extensions` mapping (if present). + + Args: + extension_handle: The handle for the extension to check. + + Returns: + bool: Whether the extension is present for this message. + + Raises: + KeyError: if the extension is repeated. Similar to repeated fields, + there is no separate notion of presence: a "not present" repeated + extension is an empty list. + """ + raise NotImplementedError + + def ClearExtension(self, extension_handle): + """Clears the contents of a given extension. + + Args: + extension_handle: The handle for the extension to clear. + """ + raise NotImplementedError + + def UnknownFields(self): + """Returns the UnknownFieldSet. + + Returns: + UnknownFieldSet: The unknown fields stored in this message. + """ + raise NotImplementedError + + def DiscardUnknownFields(self): + """Clears all fields in the :class:`UnknownFieldSet`. + + This operation is recursive for nested message. + """ + raise NotImplementedError + + def ByteSize(self): + """Returns the serialized size of this message. + + Recursively calls ByteSize() on all contained messages. + + Returns: + int: The number of bytes required to serialize this message. + """ + raise NotImplementedError + + @classmethod + def FromString(cls, s): + raise NotImplementedError + + @staticmethod + def RegisterExtension(extension_handle): + raise NotImplementedError + + def _SetListener(self, message_listener): + """Internal method used by the protocol message implementation. + Clients should not call this directly. + + Sets a listener that this message will call on certain state transitions. + + The purpose of this method is to register back-edges from children to + parents at runtime, for the purpose of setting "has" bits and + byte-size-dirty bits in the parent and ancestor objects whenever a child or + descendant object is modified. + + If the client wants to disconnect this Message from the object tree, she + explicitly sets callback to None. + + If message_listener is None, unregisters any existing listener. Otherwise, + message_listener must implement the MessageListener interface in + internal/message_listener.py, and we discard any listener registered + via a previous _SetListener() call. + """ + raise NotImplementedError + + def __getstate__(self): + """Support the pickle protocol.""" + return dict(serialized=self.SerializePartialToString()) + + def __setstate__(self, state): + """Support the pickle protocol.""" + self.__init__() + serialized = state['serialized'] + # On Python 3, using encoding='latin1' is required for unpickling + # protos pickled by Python 2. + if not isinstance(serialized, bytes): + serialized = serialized.encode('latin1') + self.ParseFromString(serialized) + + def __reduce__(self): + message_descriptor = self.DESCRIPTOR + if message_descriptor.containing_type is None: + return type(self), (), self.__getstate__() + # the message type must be nested. + # Python does not pickle nested classes; use the symbol_database on the + # receiving end. + container = message_descriptor + return (_InternalConstructMessage, (container.full_name,), + self.__getstate__()) + + +def _InternalConstructMessage(full_name): + """Constructs a nested message.""" + from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top + + return symbol_database.Default().GetSymbol(full_name)() diff --git a/openpype/hosts/hiero/vendor/google/protobuf/message_factory.py b/openpype/hosts/hiero/vendor/google/protobuf/message_factory.py new file mode 100644 index 0000000000..3656fa6874 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/message_factory.py @@ -0,0 +1,185 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a factory class for generating dynamic messages. + +The easiest way to use this class is if you have access to the FileDescriptor +protos containing the messages you want to create you can just do the following: + +message_classes = message_factory.GetMessages(iterable_of_file_descriptors) +my_proto_instance = message_classes['some.proto.package.MessageName']() +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message + +if api_implementation.Type() == 'cpp': + from google.protobuf.pyext import cpp_message as message_impl +else: + from google.protobuf.internal import python_message as message_impl + + +# The type of all Message classes. +_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType + + +class MessageFactory(object): + """Factory for creating Proto2 messages from descriptors in a pool.""" + + def __init__(self, pool=None): + """Initializes a new factory.""" + self.pool = pool or descriptor_pool.DescriptorPool() + + # local cache of all classes built from protobuf descriptors + self._classes = {} + + def GetPrototype(self, descriptor): + """Obtains a proto2 message class based on the passed in descriptor. + + Passing a descriptor with a fully qualified name matching a previous + invocation will cause the same class to be returned. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + if descriptor not in self._classes: + result_class = self.CreatePrototype(descriptor) + # The assignment to _classes is redundant for the base implementation, but + # might avoid confusion in cases where CreatePrototype gets overridden and + # does not call the base implementation. + self._classes[descriptor] = result_class + return result_class + return self._classes[descriptor] + + def CreatePrototype(self, descriptor): + """Builds a proto2 message class based on the passed in descriptor. + + Don't call this function directly, it always creates a new class. Call + GetPrototype() instead. This method is meant to be overridden in subblasses + to perform additional operations on the newly constructed class. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + descriptor_name = descriptor.name + result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( + descriptor_name, + (message.Message,), + { + 'DESCRIPTOR': descriptor, + # If module not set, it wrongly points to message_factory module. + '__module__': None, + }) + result_class._FACTORY = self # pylint: disable=protected-access + # Assign in _classes before doing recursive calls to avoid infinite + # recursion. + self._classes[descriptor] = result_class + for field in descriptor.fields: + if field.message_type: + self.GetPrototype(field.message_type) + for extension in result_class.DESCRIPTOR.extensions: + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result_class + + def GetMessages(self, files): + """Gets all the messages from a specified file. + + This will find and resolve dependencies, failing if the descriptor + pool cannot satisfy them. + + Args: + files: The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for desc in file_desc.message_types_by_name.values(): + result[desc.full_name] = self.GetPrototype(desc) + + # While the extension FieldDescriptors are created by the descriptor pool, + # the python classes created in the factory need them to be registered + # explicitly, which is done below. + # + # The call to RegisterExtension will specifically check if the + # extension was already registered on the object and either + # ignore the registration if the original was the same, or raise + # an error if they were different. + + for extension in file_desc.extensions_by_name.values(): + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result + + +_FACTORY = MessageFactory() + + +def GetMessages(file_protos): + """Builds a dictionary of all the messages available in a set of files. + + Args: + file_protos: Iterable of FileDescriptorProto to build messages out of. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + # The cpp implementation of the protocol buffer library requires to add the + # message in topological order of the dependency graph. + file_by_name = {file_proto.name: file_proto for file_proto in file_protos} + def _AddFile(file_proto): + for dependency in file_proto.dependency: + if dependency in file_by_name: + # Remove from elements to be visited, in order to cut cycles. + _AddFile(file_by_name.pop(dependency)) + _FACTORY.pool.Add(file_proto) + while file_by_name: + _AddFile(file_by_name.popitem()[1]) + return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos]) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py b/openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py new file mode 100644 index 0000000000..a4667ce63e --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py @@ -0,0 +1,134 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Dynamic Protobuf class creator.""" + +from collections import OrderedDict +import hashlib +import os + +from google.protobuf import descriptor_pb2 +from google.protobuf import descriptor +from google.protobuf import message_factory + + +def _GetMessageFromFactory(factory, full_name): + """Get a proto class from the MessageFactory by name. + + Args: + factory: a MessageFactory instance. + full_name: str, the fully qualified name of the proto type. + Returns: + A class, for the type identified by full_name. + Raises: + KeyError, if the proto is not found in the factory's descriptor pool. + """ + proto_descriptor = factory.pool.FindMessageTypeByName(full_name) + proto_cls = factory.GetPrototype(proto_descriptor) + return proto_cls + + +def MakeSimpleProtoClass(fields, full_name=None, pool=None): + """Create a Protobuf class whose fields are basic types. + + Note: this doesn't validate field names! + + Args: + fields: dict of {name: field_type} mappings for each field in the proto. If + this is an OrderedDict the order will be maintained, otherwise the + fields will be sorted by name. + full_name: optional str, the fully-qualified name of the proto type. + pool: optional DescriptorPool instance. + Returns: + a class, the new protobuf class with a FileDescriptor. + """ + factory = message_factory.MessageFactory(pool=pool) + + if full_name is not None: + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # Get a list of (name, field_type) tuples from the fields dict. If fields was + # an OrderedDict we keep the order, but otherwise we sort the field to ensure + # consistent ordering. + field_items = fields.items() + if not isinstance(fields, OrderedDict): + field_items = sorted(field_items) + + # Use a consistent file name that is unlikely to conflict with any imported + # proto files. + fields_hash = hashlib.sha1() + for f_name, f_type in field_items: + fields_hash.update(f_name.encode('utf-8')) + fields_hash.update(str(f_type).encode('utf-8')) + proto_file_name = fields_hash.hexdigest() + '.proto' + + # If the proto is anonymous, use the same hash to name it. + if full_name is None: + full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + + fields_hash.hexdigest()) + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # This is the first time we see this proto: add a new descriptor to the pool. + factory.pool.Add( + _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) + return _GetMessageFromFactory(factory, full_name) + + +def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): + """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" + package, name = full_name.rsplit('.', 1) + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) + file_proto.package = package + desc_proto = file_proto.message_type.add() + desc_proto.name = name + for f_number, (f_name, f_type) in enumerate(field_items, 1): + field_proto = desc_proto.field.add() + field_proto.name = f_name + # # If the number falls in the reserved range, reassign it to the correct + # # number after the range. + if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: + f_number += ( + descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - + descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) + field_proto.number = f_number + field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + field_proto.type = f_type + return file_proto diff --git a/openpype/hosts/hiero/vendor/google/protobuf/pyext/__init__.py b/openpype/hosts/hiero/vendor/google/protobuf/pyext/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py b/openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py new file mode 100644 index 0000000000..fc8eb32d79 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py @@ -0,0 +1,65 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Protocol message implementation hooks for C++ implementation. + +Contains helper functions used to create protocol message classes from +Descriptor objects at runtime backed by the protocol buffer C++ API. +""" + +__author__ = 'tibell@google.com (Johan Tibell)' + +from google.protobuf.pyext import _message + + +class GeneratedProtocolMessageType(_message.MessageMeta): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + + The above example will not work for nested types. If you wish to include them, + use reflection.MakeClass() instead of manually instantiating the class in + order to create the appropriate class structure. + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' diff --git a/openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py new file mode 100644 index 0000000000..2c6ecf4c98 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/pyext/python.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"google/protobuf/pyext/python.proto\x12\x1fgoogle.protobuf.python.internal\"\xbc\x02\n\x0cTestAllTypes\x12\\\n\x17repeated_nested_message\x18\x01 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\\\n\x17optional_nested_message\x18\x02 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\x16\n\x0eoptional_int32\x18\x03 \x01(\x05\x1aX\n\rNestedMessage\x12\n\n\x02\x62\x62\x18\x01 \x01(\x05\x12;\n\x02\x63\x63\x18\x02 \x01(\x0b\x32/.google.protobuf.python.internal.ForeignMessage\"&\n\x0e\x46oreignMessage\x12\t\n\x01\x63\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x03(\x05\"\x1d\n\x11TestAllExtensions*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x9a\x01\n!optional_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x01 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\x9a\x01\n!repeated_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x02 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\x02H\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestAllExtensions.RegisterExtension(optional_nested_message_extension) + TestAllExtensions.RegisterExtension(repeated_nested_message_extension) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'H\001' + _TESTALLTYPES._serialized_start=72 + _TESTALLTYPES._serialized_end=388 + _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300 + _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388 + _FOREIGNMESSAGE._serialized_start=390 + _FOREIGNMESSAGE._serialized_end=428 + _TESTALLEXTENSIONS._serialized_start=430 + _TESTALLEXTENSIONS._serialized_end=459 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/reflection.py b/openpype/hosts/hiero/vendor/google/protobuf/reflection.py new file mode 100644 index 0000000000..81e18859a8 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/reflection.py @@ -0,0 +1,95 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +from google.protobuf import message_factory +from google.protobuf import symbol_database + +# The type of all Message classes. +# Part of the public interface, but normally only used by message factories. +GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE + +MESSAGE_CLASS_CACHE = {} + + +# Deprecated. Please NEVER use reflection.ParseMessage(). +def ParseMessage(descriptor, byte_str): + """Generate a new Message instance from this Descriptor and a byte string. + + DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). + Please use MessageFactory.GetPrototype() instead. + + Args: + descriptor: Protobuf Descriptor object + byte_str: Serialized protocol buffer byte string + + Returns: + Newly created protobuf Message object. + """ + result_class = MakeClass(descriptor) + new_msg = result_class() + new_msg.ParseFromString(byte_str) + return new_msg + + +# Deprecated. Please NEVER use reflection.MakeClass(). +def MakeClass(descriptor): + """Construct a class object for a protobuf described by descriptor. + + DEPRECATED: use MessageFactory.GetPrototype() instead. + + Args: + descriptor: A descriptor.Descriptor object describing the protobuf. + Returns: + The Message class object described by the descriptor. + """ + # Original implementation leads to duplicate message classes, which won't play + # well with extensions. Message factory info is also missing. + # Redirect to message_factory. + return symbol_database.Default().GetPrototype(descriptor) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/service.py b/openpype/hosts/hiero/vendor/google/protobuf/service.py new file mode 100644 index 0000000000..5625246324 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/service.py @@ -0,0 +1,228 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""DEPRECATED: Declares the RPC service interfaces. + +This module declares the abstract interfaces underlying proto2 RPC +services. These are intended to be independent of any particular RPC +implementation, so that proto2 services can be used on top of a variety +of implementations. Starting with version 2.3.0, RPC implementations should +not try to build on these, but should instead provide code generator plugins +which generate code specific to the particular RPC implementation. This way +the generated code can be more appropriate for the implementation in use +and can avoid unnecessary layers of indirection. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class RpcException(Exception): + """Exception raised on failed blocking RPC method call.""" + pass + + +class Service(object): + + """Abstract base interface for protocol-buffer-based RPC services. + + Services themselves are abstract classes (implemented either by servers or as + stubs), but they subclass this base interface. The methods of this + interface can be used to call the methods of the service without knowing + its exact type at compile time (analogous to the Message interface). + """ + + def GetDescriptor(): + """Retrieves this service's descriptor.""" + raise NotImplementedError + + def CallMethod(self, method_descriptor, rpc_controller, + request, done): + """Calls a method of the service specified by method_descriptor. + + If "done" is None then the call is blocking and the response + message will be returned directly. Otherwise the call is asynchronous + and "done" will later be called with the response value. + + In the blocking case, RpcException will be raised on error. + + Preconditions: + + * method_descriptor.service == GetDescriptor + * request is of the exact same classes as returned by + GetRequestClass(method). + * After the call has started, the request must not be modified. + * "rpc_controller" is of the correct type for the RPC implementation being + used by this Service. For stubs, the "correct type" depends on the + RpcChannel which the stub is using. + + Postconditions: + + * "done" will be called when the method is complete. This may be + before CallMethod() returns or it may be at some point in the future. + * If the RPC failed, the response value passed to "done" will be None. + Further details about the failure can be found by querying the + RpcController. + """ + raise NotImplementedError + + def GetRequestClass(self, method_descriptor): + """Returns the class of the request message for the specified method. + + CallMethod() requires that the request is of a particular subclass of + Message. GetRequestClass() gets the default instance of this required + type. + + Example: + method = service.GetDescriptor().FindMethodByName("Foo") + request = stub.GetRequestClass(method)() + request.ParseFromString(input) + service.CallMethod(method, request, callback) + """ + raise NotImplementedError + + def GetResponseClass(self, method_descriptor): + """Returns the class of the response message for the specified method. + + This method isn't really needed, as the RpcChannel's CallMethod constructs + the response protocol message. It's provided anyway in case it is useful + for the caller to know the response type in advance. + """ + raise NotImplementedError + + +class RpcController(object): + + """An RpcController mediates a single method call. + + The primary purpose of the controller is to provide a way to manipulate + settings specific to the RPC implementation and to find out about RPC-level + errors. The methods provided by the RpcController interface are intended + to be a "least common denominator" set of features which we expect all + implementations to support. Specific implementations may provide more + advanced features (e.g. deadline propagation). + """ + + # Client-side methods below + + def Reset(self): + """Resets the RpcController to its initial state. + + After the RpcController has been reset, it may be reused in + a new call. Must not be called while an RPC is in progress. + """ + raise NotImplementedError + + def Failed(self): + """Returns true if the call failed. + + After a call has finished, returns true if the call failed. The possible + reasons for failure depend on the RPC implementation. Failed() must not + be called before a call has finished. If Failed() returns true, the + contents of the response message are undefined. + """ + raise NotImplementedError + + def ErrorText(self): + """If Failed is true, returns a human-readable description of the error.""" + raise NotImplementedError + + def StartCancel(self): + """Initiate cancellation. + + Advises the RPC system that the caller desires that the RPC call be + canceled. The RPC system may cancel it immediately, may wait awhile and + then cancel it, or may not even cancel the call at all. If the call is + canceled, the "done" callback will still be called and the RpcController + will indicate that the call failed at that time. + """ + raise NotImplementedError + + # Server-side methods below + + def SetFailed(self, reason): + """Sets a failure reason. + + Causes Failed() to return true on the client side. "reason" will be + incorporated into the message returned by ErrorText(). If you find + you need to return machine-readable information about failures, you + should incorporate it into your response protocol buffer and should + NOT call SetFailed(). + """ + raise NotImplementedError + + def IsCanceled(self): + """Checks if the client cancelled the RPC. + + If true, indicates that the client canceled the RPC, so the server may + as well give up on replying to it. The server should still call the + final "done" callback. + """ + raise NotImplementedError + + def NotifyOnCancel(self, callback): + """Sets a callback to invoke on cancel. + + Asks that the given callback be called when the RPC is canceled. The + callback will always be called exactly once. If the RPC completes without + being canceled, the callback will be called after completion. If the RPC + has already been canceled when NotifyOnCancel() is called, the callback + will be called immediately. + + NotifyOnCancel() must be called no more than once per request. + """ + raise NotImplementedError + + +class RpcChannel(object): + + """Abstract interface for an RPC channel. + + An RpcChannel represents a communication line to a service which can be used + to call that service's methods. The service may be running on another + machine. Normally, you should not use an RpcChannel directly, but instead + construct a stub {@link Service} wrapping it. Example: + + Example: + RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") + RpcController controller = rpcImpl.Controller() + MyService service = MyService_Stub(channel) + service.MyMethod(controller, request, callback) + """ + + def CallMethod(self, method_descriptor, rpc_controller, + request, response_class, done): + """Calls the method identified by the descriptor. + + Call the given method of the remote service. The signature of this + procedure looks the same as Service.CallMethod(), but the requirements + are less strict in one important way: the request object doesn't have to + be of any specific class as long as its descriptor is method.input_type. + """ + raise NotImplementedError diff --git a/openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py b/openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py new file mode 100644 index 0000000000..f82ab7145a --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py @@ -0,0 +1,295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains metaclasses used to create protocol service and service stub +classes from ServiceDescriptor objects at runtime. + +The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to +inject all useful functionality into the classes output by the protocol +compiler at compile-time. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class GeneratedServiceType(type): + + """Metaclass for service classes created at runtime from ServiceDescriptors. + + Implementations for all methods described in the Service class are added here + by this class. We also create properties to allow getting/setting all fields + in the protocol message. + + The protocol compiler currently uses this metaclass to create protocol service + classes at runtime. Clients can also manually create their own classes at + runtime, as in this example:: + + mydescriptor = ServiceDescriptor(.....) + class MyProtoService(service.Service): + __metaclass__ = GeneratedServiceType + DESCRIPTOR = mydescriptor + myservice_instance = MyProtoService() + # ... + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service class. + + Args: + name: Name of the class (ignored, but required by the metaclass + protocol). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service class is subclassed. + if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] + service_builder = _ServiceBuilder(descriptor) + service_builder.BuildService(cls) + cls.DESCRIPTOR = descriptor + + +class GeneratedServiceStubType(GeneratedServiceType): + + """Metaclass for service stubs created at runtime from ServiceDescriptors. + + This class has similar responsibilities as GeneratedServiceType, except that + it creates the service stub classes. + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service stub class. + + Args: + name: Name of the class (ignored, here). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service stub is subclassed. + if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] + service_stub_builder = _ServiceStubBuilder(descriptor) + service_stub_builder.BuildServiceStub(cls) + + +class _ServiceBuilder(object): + + """This class constructs a protocol service class using a service descriptor. + + Given a service descriptor, this class constructs a class that represents + the specified service descriptor. One service builder instance constructs + exactly one service class. That means all instances of that class share the + same builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + service class. + """ + self.descriptor = service_descriptor + + def BuildService(builder, cls): + """Constructs the service class. + + Args: + cls: The class that will be constructed. + """ + + # CallMethod needs to operate with an instance of the Service class. This + # internal wrapper function exists only to be able to pass the service + # instance to the method that does the real CallMethod work. + # Making sure to use exact argument names from the abstract interface in + # service.py to match the type signature + def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): + return builder._CallMethod(self, method_descriptor, rpc_controller, + request, done) + + def _WrapGetRequestClass(self, method_descriptor): + return builder._GetRequestClass(method_descriptor) + + def _WrapGetResponseClass(self, method_descriptor): + return builder._GetResponseClass(method_descriptor) + + builder.cls = cls + cls.CallMethod = _WrapCallMethod + cls.GetDescriptor = staticmethod(lambda: builder.descriptor) + cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' + cls.GetRequestClass = _WrapGetRequestClass + cls.GetResponseClass = _WrapGetResponseClass + for method in builder.descriptor.methods: + setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) + + def _CallMethod(self, srvc, method_descriptor, + rpc_controller, request, callback): + """Calls the method described by a given method descriptor. + + Args: + srvc: Instance of the service for which this method is called. + method_descriptor: Descriptor that represent the method to call. + rpc_controller: RPC controller to use for this method's execution. + request: Request protocol message. + callback: A callback to invoke after the method has completed. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'CallMethod() given method descriptor for wrong service type.') + method = getattr(srvc, method_descriptor.name) + return method(rpc_controller, request, callback) + + def _GetRequestClass(self, method_descriptor): + """Returns the class of the request protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + request protocol message class. + + Returns: + A class that represents the input protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetRequestClass() given method descriptor for wrong service type.') + return method_descriptor.input_type._concrete_class + + def _GetResponseClass(self, method_descriptor): + """Returns the class of the response protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + response protocol message class. + + Returns: + A class that represents the output protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetResponseClass() given method descriptor for wrong service type.') + return method_descriptor.output_type._concrete_class + + def _GenerateNonImplementedMethod(self, method): + """Generates and returns a method that can be set for a service methods. + + Args: + method: Descriptor of the service method for which a method is to be + generated. + + Returns: + A method that can be added to the service class. + """ + return lambda inst, rpc_controller, request, callback: ( + self._NonImplementedMethod(method.name, rpc_controller, callback)) + + def _NonImplementedMethod(self, method_name, rpc_controller, callback): + """The body of all methods in the generated service class. + + Args: + method_name: Name of the method being executed. + rpc_controller: RPC controller used to execute this method. + callback: A callback which will be invoked when the method finishes. + """ + rpc_controller.SetFailed('Method %s not implemented.' % method_name) + callback(None) + + +class _ServiceStubBuilder(object): + + """Constructs a protocol service stub class using a service descriptor. + + Given a service descriptor, this class constructs a suitable stub class. + A stub is just a type-safe wrapper around an RpcChannel which emulates a + local implementation of the service. + + One service stub builder instance constructs exactly one class. It means all + instances of that class share the same service stub builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service stub class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + stub class. + """ + self.descriptor = service_descriptor + + def BuildServiceStub(self, cls): + """Constructs the stub class. + + Args: + cls: The class that will be constructed. + """ + + def _ServiceStubInit(stub, rpc_channel): + stub.rpc_channel = rpc_channel + self.cls = cls + cls.__init__ = _ServiceStubInit + for method in self.descriptor.methods: + setattr(cls, method.name, self._GenerateStubMethod(method)) + + def _GenerateStubMethod(self, method): + return (lambda inst, rpc_controller, request, callback=None: + self._StubMethod(inst, method, rpc_controller, request, callback)) + + def _StubMethod(self, stub, method_descriptor, + rpc_controller, request, callback): + """The body of all service methods in the generated stub class. + + Args: + stub: Stub instance. + method_descriptor: Descriptor of the invoked method. + rpc_controller: Rpc controller to execute the method. + request: Request protocol message. + callback: A callback to execute when the method finishes. + Returns: + Response message (in case of blocking call). + """ + return stub.rpc_channel.CallMethod( + method_descriptor, rpc_controller, request, + method_descriptor.output_type._concrete_class, callback) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py new file mode 100644 index 0000000000..30cca2e06e --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/source_context.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\"\"\n\rSourceContext\x12\x11\n\tfile_name\x18\x01 \x01(\tB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SOURCECONTEXT._serialized_start=57 + _SOURCECONTEXT._serialized_end=91 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py new file mode 100644 index 0000000000..149728ca08 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/struct.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x84\x01\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntry\x1a\x45\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xea\x01\n\x05Value\x12\x30\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x12\x16\n\x0cnumber_value\x18\x02 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12/\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00\x42\x06\n\x04kind\"3\n\tListValue\x12&\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.Value*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _STRUCT_FIELDSENTRY._options = None + _STRUCT_FIELDSENTRY._serialized_options = b'8\001' + _NULLVALUE._serialized_start=474 + _NULLVALUE._serialized_end=501 + _STRUCT._serialized_start=50 + _STRUCT._serialized_end=182 + _STRUCT_FIELDSENTRY._serialized_start=113 + _STRUCT_FIELDSENTRY._serialized_end=182 + _VALUE._serialized_start=185 + _VALUE._serialized_end=419 + _LISTVALUE._serialized_start=421 + _LISTVALUE._serialized_end=472 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py b/openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py new file mode 100644 index 0000000000..fdcf8cf06c --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py @@ -0,0 +1,194 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""A database of Python protocol buffer generated symbols. + +SymbolDatabase is the MessageFactory for messages generated at compile time, +and makes it easy to create new instances of a registered type, given only the +type's protocol buffer symbol name. + +Example usage:: + + db = symbol_database.SymbolDatabase() + + # Register symbols of interest, from one or multiple files. + db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) + db.RegisterMessage(my_proto_pb2.MyMessage) + db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) + + # The database can be used as a MessageFactory, to generate types based on + # their name: + types = db.GetMessages(['my_proto.proto']) + my_message_instance = types['MyMessage']() + + # The database's underlying descriptor pool can be queried, so it's not + # necessary to know a type's filename to be able to generate it: + filename = db.pool.FindFileContainingSymbol('MyMessage') + my_message_instance = db.GetMessages([filename])['MyMessage']() + + # This functionality is also provided directly via a convenience method: + my_message_instance = db.GetSymbol('MyMessage')() +""" + + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message_factory + + +class SymbolDatabase(message_factory.MessageFactory): + """A database of Python generated symbols.""" + + def RegisterMessage(self, message): + """Registers the given message type in the local database. + + Calls to GetSymbol() and GetMessages() will return messages registered here. + + Args: + message: A :class:`google.protobuf.message.Message` subclass (or + instance); its descriptor will be registered. + + Returns: + The provided message. + """ + + desc = message.DESCRIPTOR + self._classes[desc] = message + self.RegisterMessageDescriptor(desc) + return message + + def RegisterMessageDescriptor(self, message_descriptor): + """Registers the given message descriptor in the local database. + + Args: + message_descriptor (Descriptor): the message descriptor to add. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddDescriptor(message_descriptor) + + def RegisterEnumDescriptor(self, enum_descriptor): + """Registers the given enum descriptor in the local database. + + Args: + enum_descriptor (EnumDescriptor): The enum descriptor to register. + + Returns: + EnumDescriptor: The provided descriptor. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddEnumDescriptor(enum_descriptor) + return enum_descriptor + + def RegisterServiceDescriptor(self, service_descriptor): + """Registers the given service descriptor in the local database. + + Args: + service_descriptor (ServiceDescriptor): the service descriptor to + register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddServiceDescriptor(service_descriptor) + + def RegisterFileDescriptor(self, file_descriptor): + """Registers the given file descriptor in the local database. + + Args: + file_descriptor (FileDescriptor): The file descriptor to register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._InternalAddFileDescriptor(file_descriptor) + + def GetSymbol(self, symbol): + """Tries to find a symbol in the local database. + + Currently, this method only returns message.Message instances, however, if + may be extended in future to support other symbol types. + + Args: + symbol (str): a protocol buffer symbol. + + Returns: + A Python class corresponding to the symbol. + + Raises: + KeyError: if the symbol could not be found. + """ + + return self._classes[self.pool.FindMessageTypeByName(symbol)] + + def GetMessages(self, files): + # TODO(amauryfa): Fix the differences with MessageFactory. + """Gets all registered messages from a specified file. + + Only messages already created and registered will be returned; (this is the + case for imported _pb2 modules) + But unlike MessageFactory, this version also returns already defined nested + messages, but does not register any message extensions. + + Args: + files (list[str]): The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. + + Raises: + KeyError: if a file could not be found. + """ + + def _GetAllMessages(desc): + """Walk a message Descriptor and recursively yields all message names.""" + yield desc + for msg_desc in desc.nested_types: + for nested_desc in _GetAllMessages(msg_desc): + yield nested_desc + + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for msg_desc in file_desc.message_types_by_name.values(): + for desc in _GetAllMessages(msg_desc): + try: + result[desc.full_name] = self._classes[desc] + except KeyError: + # This descriptor has no registered class, skip it. + pass + return result + + +_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) + + +def Default(): + """Returns the default SymbolDatabase.""" + return _DEFAULT diff --git a/openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py b/openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py new file mode 100644 index 0000000000..759cf11f62 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py @@ -0,0 +1,110 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Encoding related utilities.""" +import re + +_cescape_chr_to_symbol_map = {} +_cescape_chr_to_symbol_map[9] = r'\t' # optional escape +_cescape_chr_to_symbol_map[10] = r'\n' # optional escape +_cescape_chr_to_symbol_map[13] = r'\r' # optional escape +_cescape_chr_to_symbol_map[34] = r'\"' # necessary escape +_cescape_chr_to_symbol_map[39] = r"\'" # optional escape +_cescape_chr_to_symbol_map[92] = r'\\' # necessary escape + +# Lookup table for unicode +_cescape_unicode_to_str = [chr(i) for i in range(0, 256)] +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_unicode_to_str[byte] = string + +# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) +_cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + + [chr(i) for i in range(32, 127)] + + [r'\%03o' % i for i in range(127, 256)]) +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_byte_to_str[byte] = string +del byte, string + + +def CEscape(text, as_utf8): + # type: (...) -> str + """Escape a bytes string for use in an text protocol buffer. + + Args: + text: A byte string to be escaped. + as_utf8: Specifies if result may contain non-ASCII characters. + In Python 3 this allows unescaped non-ASCII Unicode characters. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + Returns: + Escaped string (str). + """ + # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not + # satisfy our needs; they encodes unprintable characters using two-digit hex + # escapes whereas our C++ unescaping function allows hex escapes to be any + # length. So, "\0011".encode('string_escape') ends up being "\\x011", which + # will be decoded in C++ as a single-character string with char code 0x11. + text_is_unicode = isinstance(text, str) + if as_utf8 and text_is_unicode: + # We're already unicode, no processing beyond control char escapes. + return text.translate(_cescape_chr_to_symbol_map) + ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. + if as_utf8: + return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) + return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) + + +_CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') + + +def CUnescape(text): + # type: (str) -> bytes + """Unescape a text string with C-style escape sequences to UTF-8 bytes. + + Args: + text: The data to parse in a str. + Returns: + A byte string. + """ + + def ReplaceHex(m): + # Only replace the match if the number of leading back slashes is odd. i.e. + # the slash itself is not escaped. + if len(m.group(1)) & 1: + return m.group(1) + 'x0' + m.group(2) + return m.group(0) + + # This is required because the 'string_escape' encoding doesn't + # allow single-digit hex escapes (like '\xf'). + result = _CUNESCAPE_HEX.sub(ReplaceHex, text) + + return (result.encode('utf-8') # Make it bytes to allow decode. + .decode('unicode_escape') + # Make it bytes again to return the proper type. + .encode('raw_unicode_escape')) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/text_format.py b/openpype/hosts/hiero/vendor/google/protobuf/text_format.py new file mode 100644 index 0000000000..412385c26f --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/text_format.py @@ -0,0 +1,1795 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in text format. + +Simple usage example:: + + # Create a proto object and serialize it to a text proto string. + message = my_proto_pb2.MyMessage(foo='bar') + text_proto = text_format.MessageToString(message) + + # Parse a text proto string. + message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +# TODO(b/129989314) Import thread contention leads to test failures. +import encodings.raw_unicode_escape # pylint: disable=unused-import +import encodings.unicode_escape # pylint: disable=unused-import +import io +import math +import re + +from google.protobuf.internal import decoder +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import text_encoding + +# pylint: disable=g-import-not-at-top +__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', + 'PrintFieldValue', 'Merge', 'MessageToBytes'] + +_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), + type_checkers.Int32ValueChecker(), + type_checkers.Uint64ValueChecker(), + type_checkers.Int64ValueChecker()) +_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) +_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) +_QUOTES = frozenset(("'", '"')) +_ANY_FULL_TYPE_NAME = 'google.protobuf.Any' + + +class Error(Exception): + """Top-level module error for text_format.""" + + +class ParseError(Error): + """Thrown in case of text parsing or tokenizing error.""" + + def __init__(self, message=None, line=None, column=None): + if message is not None and line is not None: + loc = str(line) + if column is not None: + loc += ':{0}'.format(column) + message = '{0} : {1}'.format(loc, message) + if message is not None: + super(ParseError, self).__init__(message) + else: + super(ParseError, self).__init__() + self._line = line + self._column = column + + def GetLine(self): + return self._line + + def GetColumn(self): + return self._column + + +class TextWriter(object): + + def __init__(self, as_utf8): + self._writer = io.StringIO() + + def write(self, val): + return self._writer.write(val) + + def close(self): + return self._writer.close() + + def getvalue(self): + return self._writer.getvalue() + + +def MessageToString( + message, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + indent=0, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + # type: (...) -> str + """Convert protobuf message to text format. + + Double values can be formatted compactly with 15 digits of + precision (which is the most that IEEE 754 "double" can guarantee) + using double_format='.15g'. To ensure that converting to text and back to a + proto will result in an identical value, double_format='.17g' should be used. + + Args: + message: The protocol buffers message. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, fields of a proto message will be printed using + the order defined in source code instead of the field number, extensions + will be printed at the end of the message and their relative order is + determined by the extension number. By default, use the field number + order. + float_format (str): If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest float + that has same value in wire will be printed. Also affect double field + if double_format is not set but float_format is set. + double_format (str): If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, use ``str()`` + use_field_number: If True, print field numbers instead of names. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + indent (int): The initial indent level, in terms of spaces, for pretty + print. + message_formatter (function(message, indent, as_one_line) -> unicode|None): + Custom formatter for selected sub-messages (usually based on message + type). Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if the + field is a proto message. + + Returns: + str: A string of the text formatted protocol buffer message. + """ + out = TextWriter(as_utf8) + printer = _Printer( + out, + indent, + as_utf8, + as_one_line, + use_short_repeated_primitives, + pointy_brackets, + use_index_order, + float_format, + double_format, + use_field_number, + descriptor_pool, + message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + result = out.getvalue() + out.close() + if as_one_line: + return result.rstrip() + return result + + +def MessageToBytes(message, **kwargs): + # type: (...) -> bytes + """Convert protobuf message to encoded text format. See MessageToString.""" + text = MessageToString(message, **kwargs) + if isinstance(text, bytes): + return text + codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' + return text.encode(codec) + + +def _IsMapEntry(field): + return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def PrintMessage(message, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + printer = _Printer( + out=out, indent=indent, as_utf8=as_utf8, + as_one_line=as_one_line, + use_short_repeated_primitives=use_short_repeated_primitives, + pointy_brackets=pointy_brackets, + use_index_order=use_index_order, + float_format=float_format, + double_format=double_format, + use_field_number=use_field_number, + descriptor_pool=descriptor_pool, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + + +def PrintField(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field name/value pair.""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintField(field, value) + + +def PrintFieldValue(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field value (not including name).""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintFieldValue(field, value) + + +def _BuildMessageFromTypeName(type_name, descriptor_pool): + """Returns a protobuf message instance. + + Args: + type_name: Fully-qualified protobuf message type name string. + descriptor_pool: DescriptorPool instance. + + Returns: + A Message instance of type matching type_name, or None if the a Descriptor + wasn't found matching type_name. + """ + # pylint: disable=g-import-not-at-top + if descriptor_pool is None: + from google.protobuf import descriptor_pool as pool_mod + descriptor_pool = pool_mod.Default() + from google.protobuf import symbol_database + database = symbol_database.Default() + try: + message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) + except KeyError: + return None + message_type = database.GetPrototype(message_descriptor) + return message_type() + + +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 + + +class _Printer(object): + """Text format printer for protocol message.""" + + def __init__( + self, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Initialize the Printer. + + Double values can be formatted compactly with 15 digits of precision + (which is the most that IEEE 754 "double" can guarantee) using + double_format='.15g'. To ensure that converting to text and back to a proto + will result in an identical value, double_format='.17g' should be used. + + Args: + out: To record the text format result. + indent: The initial indent level for pretty print. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, print fields of a proto message using the order + defined in source code instead of the field number. By default, use the + field number order. + float_format: If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest + float that has same value in wire will be printed. Also affect double + field if double_format is not set but float_format is set. + double_format: If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, str() is used. + use_field_number: If True, print field numbers instead of names. + descriptor_pool: A DescriptorPool used to resolve Any types. + message_formatter: A function(message, indent, as_one_line): unicode|None + to custom format selected sub-messages (usually based on message type). + Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if + the field is a proto message. + """ + self.out = out + self.indent = indent + self.as_utf8 = as_utf8 + self.as_one_line = as_one_line + self.use_short_repeated_primitives = use_short_repeated_primitives + self.pointy_brackets = pointy_brackets + self.use_index_order = use_index_order + self.float_format = float_format + if double_format is not None: + self.double_format = double_format + else: + self.double_format = float_format + self.use_field_number = use_field_number + self.descriptor_pool = descriptor_pool + self.message_formatter = message_formatter + self.print_unknown_fields = print_unknown_fields + self.force_colon = force_colon + + def _TryPrintAsAnyMessage(self, message): + """Serializes if message is a google.protobuf.Any field.""" + if '/' not in message.type_url: + return False + packed_message = _BuildMessageFromTypeName(message.TypeName(), + self.descriptor_pool) + if packed_message: + packed_message.MergeFromString(message.value) + colon = ':' if self.force_colon else '' + self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) + self._PrintMessageFieldValue(packed_message) + self.out.write(' ' if self.as_one_line else '\n') + return True + else: + return False + + def _TryCustomFormatMessage(self, message): + formatted = self.message_formatter(message, self.indent, self.as_one_line) + if formatted is None: + return False + + out = self.out + out.write(' ' * self.indent) + out.write(formatted) + out.write(' ' if self.as_one_line else '\n') + return True + + def PrintMessage(self, message): + """Convert protobuf message to text format. + + Args: + message: The protocol buffers message. + """ + if self.message_formatter and self._TryCustomFormatMessage(message): + return + if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and + self._TryPrintAsAnyMessage(message)): + return + fields = message.ListFields() + if self.use_index_order: + fields.sort( + key=lambda x: x[0].number if x[0].is_extension else x[0].index) + for field, value in fields: + if _IsMapEntry(field): + for key in sorted(value): + # This is slow for maps with submessage entries because it copies the + # entire tree. Unfortunately this would take significant refactoring + # of this file to work around. + # + # TODO(haberman): refactor and optimize if this becomes an issue. + entry_submsg = value.GetEntryClass()(key=key, value=value[key]) + self.PrintField(field, entry_submsg) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if (self.use_short_repeated_primitives + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): + self._PrintShortRepeatedPrimitivesValue(field, value) + else: + for element in value: + self.PrintField(field, element) + else: + self.PrintField(field, value) + + if self.print_unknown_fields: + self._PrintUnknownFields(message.UnknownFields()) + + def _PrintUnknownFields(self, unknown_fields): + """Print unknown fields.""" + out = self.out + for field in unknown_fields: + out.write(' ' * self.indent) + out.write(str(field.field_number)) + if field.wire_type == WIRETYPE_START_GROUP: + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(field.data) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: + try: + # If this field is parseable as a Message, it is probably + # an embedded message. + # pylint: disable=protected-access + (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( + memoryview(field.data), 0, len(field.data)) + except Exception: # pylint: disable=broad-except + pos = 0 + + if pos == len(field.data): + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(embedded_unknown_message) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + else: + # A string or bytes field. self.as_utf8 may not work. + out.write(': \"') + out.write(text_encoding.CEscape(field.data, False)) + out.write('\" ' if self.as_one_line else '\"\n') + else: + # varint, fixed32, fixed64 + out.write(': ') + out.write(str(field.data)) + out.write(' ' if self.as_one_line else '\n') + + def _PrintFieldName(self, field): + """Print field name.""" + out = self.out + out.write(' ' * self.indent) + if self.use_field_number: + out.write(str(field.number)) + else: + if field.is_extension: + out.write('[') + if (field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): + out.write(field.message_type.full_name) + else: + out.write(field.full_name) + out.write(']') + elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: + # For groups, use the capitalized name. + out.write(field.message_type.name) + else: + out.write(field.name) + + if (self.force_colon or + field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): + # The colon is optional in this case, but our cross-language golden files + # don't include it. Here, the colon is only included if force_colon is + # set to True + out.write(':') + + def PrintField(self, field, value): + """Print a single field name/value pair.""" + self._PrintFieldName(field) + self.out.write(' ') + self.PrintFieldValue(field, value) + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintShortRepeatedPrimitivesValue(self, field, value): + """"Prints short repeated primitives value.""" + # Note: this is called only when value has at least one element. + self._PrintFieldName(field) + self.out.write(' [') + for i in range(len(value) - 1): + self.PrintFieldValue(field, value[i]) + self.out.write(', ') + self.PrintFieldValue(field, value[-1]) + self.out.write(']') + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintMessageFieldValue(self, value): + if self.pointy_brackets: + openb = '<' + closeb = '>' + else: + openb = '{' + closeb = '}' + + if self.as_one_line: + self.out.write('%s ' % openb) + self.PrintMessage(value) + self.out.write(closeb) + else: + self.out.write('%s\n' % openb) + self.indent += 2 + self.PrintMessage(value) + self.indent -= 2 + self.out.write(' ' * self.indent + closeb) + + def PrintFieldValue(self, field, value): + """Print a single field value (not including name). + + For repeated fields, the value should be a single element. + + Args: + field: The descriptor of the field to be printed. + value: The value of the field. + """ + out = self.out + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self._PrintMessageFieldValue(value) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + enum_value = field.enum_type.values_by_number.get(value, None) + if enum_value is not None: + out.write(enum_value.name) + else: + out.write(str(value)) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + out.write('\"') + if isinstance(value, str) and not self.as_utf8: + out_value = value.encode('utf-8') + else: + out_value = value + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + # We always need to escape all binary data in TYPE_BYTES fields. + out_as_utf8 = False + else: + out_as_utf8 = self.as_utf8 + out.write(text_encoding.CEscape(out_value, out_as_utf8)) + out.write('\"') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + if value: + out.write('true') + else: + out.write('false') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + if self.float_format is not None: + out.write('{1:{0}}'.format(self.float_format, value)) + else: + if math.isnan(value): + out.write(str(value)) + else: + out.write(str(type_checkers.ToShortestFloat(value))) + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and + self.double_format is not None): + out.write('{1:{0}}'.format(self.double_format, value)) + else: + out.write(str(value)) + + +def Parse(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + NOTE: for historical reasons this function does not clear the input + message. This is different from what the binary msg.ParseFrom(...) does. + If text contains a field already set in message, the value is appended if the + field is repeated. Otherwise, an error is raised. + + Example:: + + a = MyProto() + a.repeated_field.append('test') + b = MyProto() + + # Repeated fields are combined + text_format.Parse(repr(a), b) + text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] + + # Non-repeated fields cannot be overwritten + a.singular_field = 1 + b.singular_field = 2 + text_format.Parse(repr(a), b) # ParseError + + # Binary version: + b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" + + Caller is responsible for clearing the message as needed. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def Merge(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + Like Parse(), but allows repeated values for a non-repeated field, and uses + the last one. This means any non-repeated, top-level fields specified in text + replace those in the message. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return MergeLines( + text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def ParseLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Parse() for caveats. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.ParseLines(lines, message) + + +def MergeLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Merge() for more details. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.MergeLines(lines, message) + + +class _Parser(object): + """Text format parser for protocol message.""" + + def __init__(self, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + self.allow_unknown_extension = allow_unknown_extension + self.allow_field_number = allow_field_number + self.descriptor_pool = descriptor_pool + self.allow_unknown_field = allow_unknown_field + + def ParseLines(self, lines, message): + """Parses a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = False + self._ParseOrMerge(lines, message) + return message + + def MergeLines(self, lines, message): + """Merges a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = True + self._ParseOrMerge(lines, message) + return message + + def _ParseOrMerge(self, lines, message): + """Converts a text representation of a protocol message into a message. + + Args: + lines: Lines of a message's text representation. + message: A protocol buffer message to merge into. + + Raises: + ParseError: On text parsing problems. + """ + # Tokenize expects native str lines. + str_lines = ( + line if isinstance(line, str) else line.decode('utf-8') + for line in lines) + tokenizer = Tokenizer(str_lines) + while not tokenizer.AtEnd(): + self._MergeField(tokenizer, message) + + def _MergeField(self, tokenizer, message): + """Merges a single protocol message field into a message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + message: A protocol message to record the data. + + Raises: + ParseError: In case of text parsing problems. + """ + message_descriptor = message.DESCRIPTOR + if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and + tokenizer.TryConsume('[')): + type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) + tokenizer.Consume(']') + tokenizer.TryConsume(':') + if tokenizer.TryConsume('<'): + expanded_any_end_token = '>' + else: + tokenizer.Consume('{') + expanded_any_end_token = '}' + expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, + self.descriptor_pool) + if not expanded_any_sub_message: + raise ParseError('Type %s not found in descriptor pool' % + packed_type_name) + while not tokenizer.TryConsume(expanded_any_end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % + (expanded_any_end_token,)) + self._MergeField(tokenizer, expanded_any_sub_message) + deterministic = False + + message.Pack(expanded_any_sub_message, + type_url_prefix=type_url_prefix, + deterministic=deterministic) + return + + if tokenizer.TryConsume('['): + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + name = '.'.join(name) + + if not message_descriptor.is_extendable: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" does not have extensions.' % + message_descriptor.full_name) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(name) + # pylint: enable=protected-access + + + if not field: + if self.allow_unknown_extension: + field = None + else: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" not registered. ' + 'Did you import the _pb2 module which defines it? ' + 'If you are trying to place the extension in the MessageSet ' + 'field of another message that is in an Any or MessageSet field, ' + 'that message\'s _pb2 module must be imported as well' % name) + elif message_descriptor != field.containing_type: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" does not extend message type "%s".' % + (name, message_descriptor.full_name)) + + tokenizer.Consume(']') + + else: + name = tokenizer.ConsumeIdentifierOrNumber() + if self.allow_field_number and name.isdigit(): + number = ParseInteger(name, True, True) + field = message_descriptor.fields_by_number.get(number, None) + if not field and message_descriptor.is_extendable: + field = message.Extensions._FindExtensionByNumber(number) + else: + field = message_descriptor.fields_by_name.get(name, None) + + # Group names are expected to be capitalized as they appear in the + # .proto file, which actually matches their type names, not their field + # names. + if not field: + field = message_descriptor.fields_by_name.get(name.lower(), None) + if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: + field = None + + if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and + field.message_type.name != name): + field = None + + if not field and not self.allow_unknown_field: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" has no field named "%s".' % + (message_descriptor.full_name, name)) + + if field: + if not self._allow_multiple_scalars and field.containing_oneof: + # Check if there's a different field set in this oneof. + # Note that we ignore the case if the same field was set before, and we + # apply _allow_multiple_scalars to non-scalar fields as well. + which_oneof = message.WhichOneof(field.containing_oneof.name) + if which_oneof is not None and which_oneof != field.name: + raise tokenizer.ParseErrorPreviousToken( + 'Field "%s" is specified along with field "%s", another member ' + 'of oneof "%s" for message type "%s".' % + (field.name, which_oneof, field.containing_oneof.name, + message_descriptor.full_name)) + + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + tokenizer.TryConsume(':') + merger = self._MergeMessageField + else: + tokenizer.Consume(':') + merger = self._MergeScalarField + + if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and + tokenizer.TryConsume('[')): + # Short repeated format, e.g. "foo: [1, 2, 3]" + if not tokenizer.TryConsume(']'): + while True: + merger(tokenizer, message, field) + if tokenizer.TryConsume(']'): + break + tokenizer.Consume(',') + + else: + merger(tokenizer, message, field) + + else: # Proto field is unknown. + assert (self.allow_unknown_extension or self.allow_unknown_field) + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + + def _ConsumeAnyTypeUrl(self, tokenizer): + """Consumes a google.protobuf.Any type URL and returns the type name.""" + # Consume "type.googleapis.com/". + prefix = [tokenizer.ConsumeIdentifier()] + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('/') + # Consume the fully-qualified type name. + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + return '.'.join(prefix), '.'.join(name) + + def _MergeMessageField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: The message of which field is a member. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + """ + is_map_entry = _IsMapEntry(field) + + if tokenizer.TryConsume('<'): + end_token = '>' + else: + tokenizer.Consume('{') + end_token = '}' + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + sub_message = message.Extensions[field].add() + elif is_map_entry: + sub_message = getattr(message, field.name).GetEntryClass()() + else: + sub_message = getattr(message, field.name).add() + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + sub_message = message.Extensions[field] + else: + # Also apply _allow_multiple_scalars to message field. + # TODO(jieluo): Change to _allow_singular_overwrites. + if (not self._allow_multiple_scalars and + message.HasField(field.name)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + sub_message = getattr(message, field.name) + sub_message.SetInParent() + + while not tokenizer.TryConsume(end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) + self._MergeField(tokenizer, sub_message) + + if is_map_entry: + value_cpptype = field.message_type.fields_by_name['value'].cpp_type + if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + value = getattr(message, field.name)[sub_message.key] + value.CopyFrom(sub_message.value) + else: + getattr(message, field.name)[sub_message.key] = sub_message.value + + @staticmethod + def _IsProto3Syntax(message): + message_descriptor = message.DESCRIPTOR + return (hasattr(message_descriptor, 'syntax') and + message_descriptor.syntax == 'proto3') + + def _MergeScalarField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: A protocol message to record the data. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + RuntimeError: On runtime errors. + """ + _ = self.allow_unknown_extension + value = None + + if field.type in (descriptor.FieldDescriptor.TYPE_INT32, + descriptor.FieldDescriptor.TYPE_SINT32, + descriptor.FieldDescriptor.TYPE_SFIXED32): + value = _ConsumeInt32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, + descriptor.FieldDescriptor.TYPE_SINT64, + descriptor.FieldDescriptor.TYPE_SFIXED64): + value = _ConsumeInt64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, + descriptor.FieldDescriptor.TYPE_FIXED32): + value = _ConsumeUint32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, + descriptor.FieldDescriptor.TYPE_FIXED64): + value = _ConsumeUint64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, + descriptor.FieldDescriptor.TYPE_DOUBLE): + value = tokenizer.ConsumeFloat() + elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: + value = tokenizer.ConsumeBool() + elif field.type == descriptor.FieldDescriptor.TYPE_STRING: + value = tokenizer.ConsumeString() + elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: + value = tokenizer.ConsumeByteString() + elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: + value = tokenizer.ConsumeEnum(field) + else: + raise RuntimeError('Unknown field type %d' % field.type) + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + message.Extensions[field].append(value) + else: + getattr(message, field.name).append(value) + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + not self._IsProto3Syntax(message) and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + else: + message.Extensions[field] = value + else: + duplicate_error = False + if not self._allow_multiple_scalars: + if self._IsProto3Syntax(message): + # Proto3 doesn't represent presence so we try best effort to check + # multiple scalars by compare to default values. + duplicate_error = bool(getattr(message, field.name)) + else: + duplicate_error = message.HasField(field.name) + + if duplicate_error: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + else: + setattr(message, field.name, value) + + +def _SkipFieldContents(tokenizer): + """Skips over contents (value or message) of a field. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + # Try to guess the type of this field. + # If this field is not a message, there should be a ":" between the + # field name and the field value and also the field value should not + # start with "{" or "<" which indicates the beginning of a message body. + # If there is no ":" or there is a "{" or "<" after ":", this field has + # to be a message or the input is ill-formed. + if tokenizer.TryConsume(':') and not tokenizer.LookingAt( + '{') and not tokenizer.LookingAt('<'): + _SkipFieldValue(tokenizer) + else: + _SkipFieldMessage(tokenizer) + + +def _SkipField(tokenizer): + """Skips over a complete field (name and value/message). + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + if tokenizer.TryConsume('['): + # Consume extension name. + tokenizer.ConsumeIdentifier() + while tokenizer.TryConsume('.'): + tokenizer.ConsumeIdentifier() + tokenizer.Consume(']') + else: + tokenizer.ConsumeIdentifierOrNumber() + + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + +def _SkipFieldMessage(tokenizer): + """Skips over a field message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + + if tokenizer.TryConsume('<'): + delimiter = '>' + else: + tokenizer.Consume('{') + delimiter = '}' + + while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): + _SkipField(tokenizer) + + tokenizer.Consume(delimiter) + + +def _SkipFieldValue(tokenizer): + """Skips over a field value. + + Args: + tokenizer: A tokenizer to parse the field name and values. + + Raises: + ParseError: In case an invalid field value is found. + """ + # String/bytes tokens can come in multiple adjacent string literals. + # If we can consume one, consume as many as we can. + if tokenizer.TryConsumeByteString(): + while tokenizer.TryConsumeByteString(): + pass + return + + if (not tokenizer.TryConsumeIdentifier() and + not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and + not tokenizer.TryConsumeFloat()): + raise ParseError('Invalid field value: ' + tokenizer.token) + + +class Tokenizer(object): + """Protocol buffer text representation tokenizer. + + This class handles the lower level string parsing by splitting it into + meaningful tokens. + + It was directly ported from the Java protocol buffer API. + """ + + _WHITESPACE = re.compile(r'\s+') + _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) + _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) + _TOKEN = re.compile('|'.join([ + r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier + r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number + ] + [ # quoted str for each quote mark + # Avoid backtracking! https://stackoverflow.com/a/844267 + r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) + for mark in _QUOTES + ])) + + _IDENTIFIER = re.compile(r'[^\d\W]\w*') + _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') + + def __init__(self, lines, skip_comments=True): + self._position = 0 + self._line = -1 + self._column = 0 + self._token_start = None + self.token = '' + self._lines = iter(lines) + self._current_line = '' + self._previous_line = 0 + self._previous_column = 0 + self._more_lines = True + self._skip_comments = skip_comments + self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT + or self._WHITESPACE) + self._SkipWhitespace() + self.NextToken() + + def LookingAt(self, token): + return self.token == token + + def AtEnd(self): + """Checks the end of the text was reached. + + Returns: + True iff the end was reached. + """ + return not self.token + + def _PopLine(self): + while len(self._current_line) <= self._column: + try: + self._current_line = next(self._lines) + except StopIteration: + self._current_line = '' + self._more_lines = False + return + else: + self._line += 1 + self._column = 0 + + def _SkipWhitespace(self): + while True: + self._PopLine() + match = self._whitespace_pattern.match(self._current_line, self._column) + if not match: + break + length = len(match.group(0)) + self._column += length + + def TryConsume(self, token): + """Tries to consume a given piece of text. + + Args: + token: Text to consume. + + Returns: + True iff the text was consumed. + """ + if self.token == token: + self.NextToken() + return True + return False + + def Consume(self, token): + """Consumes a piece of text. + + Args: + token: Text to consume. + + Raises: + ParseError: If the text couldn't be consumed. + """ + if not self.TryConsume(token): + raise self.ParseError('Expected "%s".' % token) + + def ConsumeComment(self): + result = self.token + if not self._COMMENT.match(result): + raise self.ParseError('Expected comment.') + self.NextToken() + return result + + def ConsumeCommentOrTrailingComment(self): + """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" + + # Tokenizer initializes _previous_line and _previous_column to 0. As the + # tokenizer starts, it looks like there is a previous token on the line. + just_started = self._line == 0 and self._column == 0 + + before_parsing = self._previous_line + comment = self.ConsumeComment() + + # A trailing comment is a comment on the same line than the previous token. + trailing = (self._previous_line == before_parsing + and not just_started) + + return trailing, comment + + def TryConsumeIdentifier(self): + try: + self.ConsumeIdentifier() + return True + except ParseError: + return False + + def ConsumeIdentifier(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER.match(result): + raise self.ParseError('Expected identifier.') + self.NextToken() + return result + + def TryConsumeIdentifierOrNumber(self): + try: + self.ConsumeIdentifierOrNumber() + return True + except ParseError: + return False + + def ConsumeIdentifierOrNumber(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER_OR_NUMBER.match(result): + raise self.ParseError('Expected identifier or number, got %s.' % result) + self.NextToken() + return result + + def TryConsumeInteger(self): + try: + self.ConsumeInteger() + return True + except ParseError: + return False + + def ConsumeInteger(self): + """Consumes an integer number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + try: + result = _ParseAbstractInteger(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeFloat(self): + try: + self.ConsumeFloat() + return True + except ParseError: + return False + + def ConsumeFloat(self): + """Consumes an floating point number. + + Returns: + The number parsed. + + Raises: + ParseError: If a floating point number couldn't be consumed. + """ + try: + result = ParseFloat(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeBool(self): + """Consumes a boolean value. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + try: + result = ParseBool(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeByteString(self): + try: + self.ConsumeByteString() + return True + except ParseError: + return False + + def ConsumeString(self): + """Consumes a string value. + + Returns: + The string parsed. + + Raises: + ParseError: If a string value couldn't be consumed. + """ + the_bytes = self.ConsumeByteString() + try: + return str(the_bytes, 'utf-8') + except UnicodeDecodeError as e: + raise self._StringParseError(e) + + def ConsumeByteString(self): + """Consumes a byte array value. + + Returns: + The array parsed (as a string). + + Raises: + ParseError: If a byte array value couldn't be consumed. + """ + the_list = [self._ConsumeSingleByteString()] + while self.token and self.token[0] in _QUOTES: + the_list.append(self._ConsumeSingleByteString()) + return b''.join(the_list) + + def _ConsumeSingleByteString(self): + """Consume one token of a string literal. + + String literals (whether bytes or text) can come in multiple adjacent + tokens which are automatically concatenated, like in C or Python. This + method only consumes one token. + + Returns: + The token parsed. + Raises: + ParseError: When the wrong format data is found. + """ + text = self.token + if len(text) < 1 or text[0] not in _QUOTES: + raise self.ParseError('Expected string but found: %r' % (text,)) + + if len(text) < 2 or text[-1] != text[0]: + raise self.ParseError('String missing ending quote: %r' % (text,)) + + try: + result = text_encoding.CUnescape(text[1:-1]) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeEnum(self, field): + try: + result = ParseEnum(field, self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ParseErrorPreviousToken(self, message): + """Creates and *returns* a ParseError for the previously read token. + + Args: + message: A message to set for the exception. + + Returns: + A ParseError instance. + """ + return ParseError(message, self._previous_line + 1, + self._previous_column + 1) + + def ParseError(self, message): + """Creates and *returns* a ParseError for the current token.""" + return ParseError('\'' + self._current_line + '\': ' + message, + self._line + 1, self._column + 1) + + def _StringParseError(self, e): + return self.ParseError('Couldn\'t parse string: ' + str(e)) + + def NextToken(self): + """Reads the next meaningful token.""" + self._previous_line = self._line + self._previous_column = self._column + + self._column += len(self.token) + self._SkipWhitespace() + + if not self._more_lines: + self.token = '' + return + + match = self._TOKEN.match(self._current_line, self._column) + if not match and not self._skip_comments: + match = self._COMMENT.match(self._current_line, self._column) + if match: + token = match.group(0) + self.token = token + else: + self.token = self._current_line[self._column] + +# Aliased so it can still be accessed by current visibility violators. +# TODO(dbarnett): Migrate violators to textformat_tokenizer. +_Tokenizer = Tokenizer # pylint: disable=invalid-name + + +def _ConsumeInt32(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) + + +def _ConsumeUint32(tokenizer): + """Consumes an unsigned 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) + + +def _TryConsumeInt64(tokenizer): + try: + _ConsumeInt64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeInt64(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) + + +def _TryConsumeUint64(tokenizer): + try: + _ConsumeUint64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeUint64(tokenizer): + """Consumes an unsigned 64bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 64bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) + + +def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): + """Consumes an integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer with given characteristics couldn't be consumed. + """ + try: + result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) + except ValueError as e: + raise tokenizer.ParseError(str(e)) + tokenizer.NextToken() + return result + + +def ParseInteger(text, is_signed=False, is_long=False): + """Parses an integer. + + Args: + text: The text to parse. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + result = _ParseAbstractInteger(text) + + # Check if the integer is sane. Exceptions handled by callers. + checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] + checker.CheckValue(result) + return result + + +def _ParseAbstractInteger(text): + """Parses an integer without checking size/signedness. + + Args: + text: The text to parse. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + orig_text = text + c_octal_match = re.match(r'(-?)0(\d+)$', text) + if c_octal_match: + # Python 3 no longer supports 0755 octal syntax without the 'o', so + # we always use the '0o' prefix for multi-digit numbers starting with 0. + text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) + try: + return int(text, 0) + except ValueError: + raise ValueError('Couldn\'t parse integer: %s' % orig_text) + + +def ParseFloat(text): + """Parse a floating point number. + + Args: + text: Text to parse. + + Returns: + The number parsed. + + Raises: + ValueError: If a floating point number couldn't be parsed. + """ + try: + # Assume Python compatible syntax. + return float(text) + except ValueError: + # Check alternative spellings. + if _FLOAT_INFINITY.match(text): + if text[0] == '-': + return float('-inf') + else: + return float('inf') + elif _FLOAT_NAN.match(text): + return float('nan') + else: + # assume '1.0f' format + try: + return float(text.rstrip('f')) + except ValueError: + raise ValueError('Couldn\'t parse float: %s' % text) + + +def ParseBool(text): + """Parse a boolean value. + + Args: + text: Text to parse. + + Returns: + Boolean values parsed + + Raises: + ValueError: If text is not a valid boolean. + """ + if text in ('true', 't', '1', 'True'): + return True + elif text in ('false', 'f', '0', 'False'): + return False + else: + raise ValueError('Expected "true" or "false".') + + +def ParseEnum(field, value): + """Parse an enum value. + + The value can be specified by a number (the enum value), or by + a string literal (the enum name). + + Args: + field: Enum field descriptor. + value: String value. + + Returns: + Enum value number. + + Raises: + ValueError: If the enum value could not be parsed. + """ + enum_descriptor = field.enum_type + try: + number = int(value, 0) + except ValueError: + # Identifier. + enum_value = enum_descriptor.values_by_name.get(value, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value named %s.' % + (enum_descriptor.full_name, value)) + else: + # Numeric value. + if hasattr(field.file, 'syntax'): + # Attribute is checked for compatibility. + if field.file.syntax == 'proto3': + # Proto3 accept numeric unknown enums. + return number + enum_value = enum_descriptor.values_by_number.get(number, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value with number %d.' % + (enum_descriptor.full_name, number)) + return enum_value.number diff --git a/openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py new file mode 100644 index 0000000000..558d496941 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/timestamp.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\"+\n\tTimestamp\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _TIMESTAMP._serialized_start=52 + _TIMESTAMP._serialized_end=95 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py new file mode 100644 index 0000000000..19903fb6b4 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/type.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xd7\x01\n\x04Type\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Field\x12\x0e\n\x06oneofs\x18\x03 \x03(\t\x12(\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x05\n\x05\x46ield\x12)\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.Kind\x12\x37\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.Cardinality\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08type_url\x18\x06 \x01(\t\x12\x13\n\x0boneof_index\x18\x07 \x01(\x05\x12\x0e\n\x06packed\x18\x08 \x01(\x08\x12(\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.Option\x12\x11\n\tjson_name\x18\n \x01(\t\x12\x15\n\rdefault_value\x18\x0b \x01(\t\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\xce\x01\n\x04\x45num\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValue\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.Syntax\"S\n\tEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\";\n\x06Option\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any*.\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SYNTAX._serialized_start=1413 + _SYNTAX._serialized_end=1459 + _TYPE._serialized_start=113 + _TYPE._serialized_end=328 + _FIELD._serialized_start=331 + _FIELD._serialized_end=1056 + _FIELD_KIND._serialized_start=610 + _FIELD_KIND._serialized_end=938 + _FIELD_CARDINALITY._serialized_start=940 + _FIELD_CARDINALITY._serialized_end=1056 + _ENUM._serialized_start=1059 + _ENUM._serialized_end=1265 + _ENUMVALUE._serialized_start=1267 + _ENUMVALUE._serialized_end=1350 + _OPTION._serialized_start=1352 + _OPTION._serialized_end=1411 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/util/__init__.py b/openpype/hosts/hiero/vendor/google/protobuf/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py new file mode 100644 index 0000000000..66a5836c82 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&google/protobuf/util/json_format.proto\x12\x11protobuf_unittest\"\x89\x01\n\x13TestFlagsAndStrings\x12\t\n\x01\x41\x18\x01 \x02(\x05\x12K\n\rrepeatedgroup\x18\x02 \x03(\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\x1a\x1a\n\rRepeatedGroup\x12\t\n\x01\x66\x18\x03 \x02(\t\"!\n\x14TestBase64ByteArrays\x12\t\n\x01\x61\x18\x01 \x02(\x0c\"G\n\x12TestJavaScriptJSON\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\r\n\x05\x66inal\x18\x02 \x01(\x02\x12\n\n\x02in\x18\x03 \x01(\t\x12\x0b\n\x03Var\x18\x04 \x01(\t\"Q\n\x18TestJavaScriptOrderJSON1\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\"\x89\x01\n\x18TestJavaScriptOrderJSON2\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\x12\x36\n\x01z\x18\x06 \x03(\x0b\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\"$\n\x0cTestLargeInt\x12\t\n\x01\x61\x18\x01 \x02(\x03\x12\t\n\x01\x62\x18\x02 \x02(\x04\"\xa0\x01\n\x0bTestNumbers\x12\x30\n\x01\x61\x18\x01 \x01(\x0e\x32%.protobuf_unittest.TestNumbers.MyType\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x02\x12\t\n\x01\x64\x18\x04 \x01(\x08\x12\t\n\x01\x65\x18\x05 \x01(\x01\x12\t\n\x01\x66\x18\x06 \x01(\r\"(\n\x06MyType\x12\x06\n\x02OK\x10\x00\x12\x0b\n\x07WARNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\"T\n\rTestCamelCase\x12\x14\n\x0cnormal_field\x18\x01 \x01(\t\x12\x15\n\rCAPITAL_FIELD\x18\x02 \x01(\x05\x12\x16\n\x0e\x43\x61melCaseField\x18\x03 \x01(\x05\"|\n\x0bTestBoolMap\x12=\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"O\n\rTestRecursion\x12\r\n\x05value\x18\x01 \x01(\x05\x12/\n\x05\x63hild\x18\x02 \x01(\x0b\x32 .protobuf_unittest.TestRecursion\"\x86\x01\n\rTestStringMap\x12\x43\n\nstring_map\x18\x01 \x03(\x0b\x32/.protobuf_unittest.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x14TestStringSerializer\x12\x15\n\rscalar_string\x18\x01 \x01(\t\x12\x17\n\x0frepeated_string\x18\x02 \x03(\t\x12J\n\nstring_map\x18\x03 \x03(\x0b\x32\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x18TestMessageWithExtension*\x08\x08\x64\x10\x80\x80\x80\x80\x02\"z\n\rTestExtension\x12\r\n\x05value\x18\x01 \x01(\t2Z\n\x03\x65xt\x12+.protobuf_unittest.TestMessageWithExtension\x18\x64 \x01(\x0b\x32 .protobuf_unittest.TestExtension\"Q\n\x14TestDefaultEnumValue\x12\x39\n\nenum_value\x18\x01 \x01(\x0e\x32\x1c.protobuf_unittest.EnumValue:\x07\x44\x45\x46\x41ULT*2\n\tEnumValue\x12\x0c\n\x08PROTOCOL\x10\x00\x12\n\n\x06\x42UFFER\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext']) + + DESCRIPTOR._options = None + _TESTBOOLMAP_BOOLMAPENTRY._options = None + _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\001' + _ENUMVALUE._serialized_start=1607 + _ENUMVALUE._serialized_end=1657 + _TESTFLAGSANDSTRINGS._serialized_start=62 + _TESTFLAGSANDSTRINGS._serialized_end=199 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199 + _TESTBASE64BYTEARRAYS._serialized_start=201 + _TESTBASE64BYTEARRAYS._serialized_end=234 + _TESTJAVASCRIPTJSON._serialized_start=236 + _TESTJAVASCRIPTJSON._serialized_end=307 + _TESTJAVASCRIPTORDERJSON1._serialized_start=309 + _TESTJAVASCRIPTORDERJSON1._serialized_end=390 + _TESTJAVASCRIPTORDERJSON2._serialized_start=393 + _TESTJAVASCRIPTORDERJSON2._serialized_end=530 + _TESTLARGEINT._serialized_start=532 + _TESTLARGEINT._serialized_end=568 + _TESTNUMBERS._serialized_start=571 + _TESTNUMBERS._serialized_end=731 + _TESTNUMBERS_MYTYPE._serialized_start=691 + _TESTNUMBERS_MYTYPE._serialized_end=731 + _TESTCAMELCASE._serialized_start=733 + _TESTCAMELCASE._serialized_end=817 + _TESTBOOLMAP._serialized_start=819 + _TESTBOOLMAP._serialized_end=943 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943 + _TESTRECURSION._serialized_start=945 + _TESTRECURSION._serialized_end=1024 + _TESTSTRINGMAP._serialized_start=1027 + _TESTSTRINGMAP._serialized_end=1161 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161 + _TESTSTRINGSERIALIZER._serialized_start=1164 + _TESTSTRINGSERIALIZER._serialized_end=1360 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161 + _TESTMESSAGEWITHEXTENSION._serialized_start=1362 + _TESTMESSAGEWITHEXTENSION._serialized_end=1398 + _TESTEXTENSION._serialized_start=1400 + _TESTEXTENSION._serialized_end=1522 + _TESTDEFAULTENUMVALUE._serialized_start=1524 + _TESTDEFAULTENUMVALUE._serialized_end=1605 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py new file mode 100644 index 0000000000..5498deafa9 --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format_proto3.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 +from google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-google/protobuf/util/json_format_proto3.proto\x12\x06proto3\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1egoogle/protobuf/unittest.proto\"\x1c\n\x0bMessageType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x94\x05\n\x0bTestMessage\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x13\n\x0bint32_value\x18\x02 \x01(\x05\x12\x13\n\x0bint64_value\x18\x03 \x01(\x03\x12\x14\n\x0cuint32_value\x18\x04 \x01(\r\x12\x14\n\x0cuint64_value\x18\x05 \x01(\x04\x12\x13\n\x0b\x66loat_value\x18\x06 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x07 \x01(\x01\x12\x14\n\x0cstring_value\x18\x08 \x01(\t\x12\x13\n\x0b\x62ytes_value\x18\t \x01(\x0c\x12$\n\nenum_value\x18\n \x01(\x0e\x32\x10.proto3.EnumType\x12*\n\rmessage_value\x18\x0b \x01(\x0b\x32\x13.proto3.MessageType\x12\x1b\n\x13repeated_bool_value\x18\x15 \x03(\x08\x12\x1c\n\x14repeated_int32_value\x18\x16 \x03(\x05\x12\x1c\n\x14repeated_int64_value\x18\x17 \x03(\x03\x12\x1d\n\x15repeated_uint32_value\x18\x18 \x03(\r\x12\x1d\n\x15repeated_uint64_value\x18\x19 \x03(\x04\x12\x1c\n\x14repeated_float_value\x18\x1a \x03(\x02\x12\x1d\n\x15repeated_double_value\x18\x1b \x03(\x01\x12\x1d\n\x15repeated_string_value\x18\x1c \x03(\t\x12\x1c\n\x14repeated_bytes_value\x18\x1d \x03(\x0c\x12-\n\x13repeated_enum_value\x18\x1e \x03(\x0e\x32\x10.proto3.EnumType\x12\x33\n\x16repeated_message_value\x18\x1f \x03(\x0b\x32\x13.proto3.MessageType\"\x8c\x02\n\tTestOneof\x12\x1b\n\x11oneof_int32_value\x18\x01 \x01(\x05H\x00\x12\x1c\n\x12oneof_string_value\x18\x02 \x01(\tH\x00\x12\x1b\n\x11oneof_bytes_value\x18\x03 \x01(\x0cH\x00\x12,\n\x10oneof_enum_value\x18\x04 \x01(\x0e\x32\x10.proto3.EnumTypeH\x00\x12\x32\n\x13oneof_message_value\x18\x05 \x01(\x0b\x32\x13.proto3.MessageTypeH\x00\x12\x36\n\x10oneof_null_value\x18\x06 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\r\n\x0boneof_value\"\xe1\x04\n\x07TestMap\x12.\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\x1c.proto3.TestMap.BoolMapEntry\x12\x30\n\tint32_map\x18\x02 \x03(\x0b\x32\x1d.proto3.TestMap.Int32MapEntry\x12\x30\n\tint64_map\x18\x03 \x03(\x0b\x32\x1d.proto3.TestMap.Int64MapEntry\x12\x32\n\nuint32_map\x18\x04 \x03(\x0b\x32\x1e.proto3.TestMap.Uint32MapEntry\x12\x32\n\nuint64_map\x18\x05 \x03(\x0b\x32\x1e.proto3.TestMap.Uint64MapEntry\x12\x32\n\nstring_map\x18\x06 \x03(\x0b\x32\x1e.proto3.TestMap.StringMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x85\x06\n\rTestNestedMap\x12\x34\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\".proto3.TestNestedMap.BoolMapEntry\x12\x36\n\tint32_map\x18\x02 \x03(\x0b\x32#.proto3.TestNestedMap.Int32MapEntry\x12\x36\n\tint64_map\x18\x03 \x03(\x0b\x32#.proto3.TestNestedMap.Int64MapEntry\x12\x38\n\nuint32_map\x18\x04 \x03(\x0b\x32$.proto3.TestNestedMap.Uint32MapEntry\x12\x38\n\nuint64_map\x18\x05 \x03(\x0b\x32$.proto3.TestNestedMap.Uint64MapEntry\x12\x38\n\nstring_map\x18\x06 \x03(\x0b\x32$.proto3.TestNestedMap.StringMapEntry\x12\x32\n\x07map_map\x18\x07 \x03(\x0b\x32!.proto3.TestNestedMap.MapMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x44\n\x0bMapMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.proto3.TestNestedMap:\x02\x38\x01\"{\n\rTestStringMap\x12\x38\n\nstring_map\x18\x01 \x03(\x0b\x32$.proto3.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0bTestWrapper\x12.\n\nbool_value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0bint32_value\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x30\n\x0bint64_value\x18\x03 \x01(\x0b\x32\x1b.google.protobuf.Int64Value\x12\x32\n\x0cuint32_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x32\n\x0cuint64_value\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x30\n\x0b\x66loat_value\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.FloatValue\x12\x32\n\x0c\x64ouble_value\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x32\n\x0cstring_value\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\x0b\x62ytes_value\x18\t \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x12\x37\n\x13repeated_bool_value\x18\x0b \x03(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\x14repeated_int32_value\x18\x0c \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x39\n\x14repeated_int64_value\x18\r \x03(\x0b\x32\x1b.google.protobuf.Int64Value\x12;\n\x15repeated_uint32_value\x18\x0e \x03(\x0b\x32\x1c.google.protobuf.UInt32Value\x12;\n\x15repeated_uint64_value\x18\x0f \x03(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x39\n\x14repeated_float_value\x18\x10 \x03(\x0b\x32\x1b.google.protobuf.FloatValue\x12;\n\x15repeated_double_value\x18\x11 \x03(\x0b\x32\x1c.google.protobuf.DoubleValue\x12;\n\x15repeated_string_value\x18\x12 \x03(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\x14repeated_bytes_value\x18\x13 \x03(\x0b\x32\x1b.google.protobuf.BytesValue\"n\n\rTestTimestamp\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"k\n\x0cTestDuration\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x19.google.protobuf.Duration\":\n\rTestFieldMask\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\"e\n\nTestStruct\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12/\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"\\\n\x07TestAny\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\x12,\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x14.google.protobuf.Any\"b\n\tTestValue\x12%\n\x05value\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\x12.\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Value\"n\n\rTestListValue\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.ListValue\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.ListValue\"\x89\x01\n\rTestBoolValue\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x34\n\x08\x62ool_map\x18\x02 \x03(\x0b\x32\".proto3.TestBoolValue.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"+\n\x12TestCustomJsonName\x12\x15\n\x05value\x18\x01 \x01(\x05R\x06@value\"J\n\x0eTestExtensions\x12\x38\n\nextensions\x18\x01 \x01(\x0b\x32$.protobuf_unittest.TestAllExtensions\"\x84\x01\n\rTestEnumValue\x12%\n\x0b\x65num_value1\x18\x01 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value2\x18\x02 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value3\x18\x03 \x01(\x0e\x32\x10.proto3.EnumType*\x1c\n\x08\x45numType\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\x42,\n\x18\x63om.google.protobuf.utilB\x10JsonFormatProto3b\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\030com.google.protobuf.utilB\020JsonFormatProto3' + _TESTMAP_BOOLMAPENTRY._options = None + _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT32MAPENTRY._options = None + _TESTMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT64MAPENTRY._options = None + _TESTMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT32MAPENTRY._options = None + _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT64MAPENTRY._options = None + _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_STRINGMAPENTRY._options = None + _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_BOOLMAPENTRY._options = None + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT32MAPENTRY._options = None + _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT64MAPENTRY._options = None + _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT32MAPENTRY._options = None + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT64MAPENTRY._options = None + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_STRINGMAPENTRY._options = None + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_MAPMAPENTRY._options = None + _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTBOOLVALUE_BOOLMAPENTRY._options = None + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\001' + _ENUMTYPE._serialized_start=4849 + _ENUMTYPE._serialized_end=4877 + _MESSAGETYPE._serialized_start=277 + _MESSAGETYPE._serialized_end=305 + _TESTMESSAGE._serialized_start=308 + _TESTMESSAGE._serialized_end=968 + _TESTONEOF._serialized_start=971 + _TESTONEOF._serialized_end=1239 + _TESTMAP._serialized_start=1242 + _TESTMAP._serialized_end=1851 + _TESTMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTMAP_INT32MAPENTRY._serialized_start=1605 + _TESTMAP_INT32MAPENTRY._serialized_end=1652 + _TESTMAP_INT64MAPENTRY._serialized_start=1654 + _TESTMAP_INT64MAPENTRY._serialized_end=1701 + _TESTMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP._serialized_start=1854 + _TESTNESTEDMAP._serialized_end=2627 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627 + _TESTSTRINGMAP._serialized_start=2629 + _TESTSTRINGMAP._serialized_end=2752 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752 + _TESTWRAPPER._serialized_start=2755 + _TESTWRAPPER._serialized_end=3761 + _TESTTIMESTAMP._serialized_start=3763 + _TESTTIMESTAMP._serialized_end=3873 + _TESTDURATION._serialized_start=3875 + _TESTDURATION._serialized_end=3982 + _TESTFIELDMASK._serialized_start=3984 + _TESTFIELDMASK._serialized_end=4042 + _TESTSTRUCT._serialized_start=4044 + _TESTSTRUCT._serialized_end=4145 + _TESTANY._serialized_start=4147 + _TESTANY._serialized_end=4239 + _TESTVALUE._serialized_start=4241 + _TESTVALUE._serialized_end=4339 + _TESTLISTVALUE._serialized_start=4341 + _TESTLISTVALUE._serialized_end=4451 + _TESTBOOLVALUE._serialized_start=4454 + _TESTBOOLVALUE._serialized_end=4591 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603 + _TESTCUSTOMJSONNAME._serialized_start=4593 + _TESTCUSTOMJSONNAME._serialized_end=4636 + _TESTEXTENSIONS._serialized_start=4638 + _TESTEXTENSIONS._serialized_end=4712 + _TESTENUMVALUE._serialized_start=4715 + _TESTENUMVALUE._serialized_end=4847 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py b/openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py new file mode 100644 index 0000000000..e49eb4c15d --- /dev/null +++ b/openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/wrappers.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"\x1c\n\x0b\x44oubleValue\x12\r\n\x05value\x18\x01 \x01(\x01\"\x1b\n\nFloatValue\x12\r\n\x05value\x18\x01 \x01(\x02\"\x1b\n\nInt64Value\x12\r\n\x05value\x18\x01 \x01(\x03\"\x1c\n\x0bUInt64Value\x12\r\n\x05value\x18\x01 \x01(\x04\"\x1b\n\nInt32Value\x12\r\n\x05value\x18\x01 \x01(\x05\"\x1c\n\x0bUInt32Value\x12\r\n\x05value\x18\x01 \x01(\r\"\x1a\n\tBoolValue\x12\r\n\x05value\x18\x01 \x01(\x08\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nBytesValue\x12\r\n\x05value\x18\x01 \x01(\x0c\x42\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DOUBLEVALUE._serialized_start=51 + _DOUBLEVALUE._serialized_end=79 + _FLOATVALUE._serialized_start=81 + _FLOATVALUE._serialized_end=108 + _INT64VALUE._serialized_start=110 + _INT64VALUE._serialized_end=137 + _UINT64VALUE._serialized_start=139 + _UINT64VALUE._serialized_end=167 + _INT32VALUE._serialized_start=169 + _INT32VALUE._serialized_end=196 + _UINT32VALUE._serialized_start=198 + _UINT32VALUE._serialized_end=226 + _BOOLVALUE._serialized_start=228 + _BOOLVALUE._serialized_end=254 + _STRINGVALUE._serialized_start=256 + _STRINGVALUE._serialized_end=284 + _BYTESVALUE._serialized_start=286 + _BYTESVALUE._serialized_end=313 +# @@protoc_insertion_point(module_scope) From 2687bbe2029353f32a05bd70321a9a65da990556 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 11:20:09 +0200 Subject: [PATCH 0952/1030] removed usage of HOST_WORKFILE_EXTENSIONS --- openpype/hosts/hiero/api/workio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/workio.py b/openpype/hosts/hiero/api/workio.py index 394cb5e2ab..762e22804f 100644 --- a/openpype/hosts/hiero/api/workio.py +++ b/openpype/hosts/hiero/api/workio.py @@ -2,13 +2,12 @@ import os import hiero from openpype.api import Logger -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS log = Logger.get_logger(__name__) def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["hiero"] + return [".hrox"] def has_unsaved_changes(): From d0036ac186fc72079bc4522baf9bce22d7e090fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 11:59:27 +0200 Subject: [PATCH 0953/1030] close settings on tray exit to remove settings lock --- openpype/modules/settings_action.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 2b4b51e3ad..1e7eca4dec 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -23,6 +23,11 @@ class SettingsAction(OpenPypeModule, ITrayAction): """Initialization in tray implementation of ITrayAction.""" self.create_settings_window() + def tray_exit(self): + # Close settings UI to remove settings lock + if self.settings_window: + self.settings_window.close() + def on_action_trigger(self): """Implementation for action trigger of ITrayAction.""" self.show_settings_window() From a83f7b5811824daf2327734d8be382580707ed5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 12:13:12 +0200 Subject: [PATCH 0954/1030] fix empty values from info --- openpype/tools/settings/settings/dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dialogs.py b/openpype/tools/settings/settings/dialogs.py index f25374a48c..b1b4daa1a0 100644 --- a/openpype/tools/settings/settings/dialogs.py +++ b/openpype/tools/settings/settings/dialogs.py @@ -39,7 +39,7 @@ class BaseInfoDialog(QtWidgets.QDialog): ): other_information_layout.addRow( label, - QtWidgets.QLabel(value, other_information) + QtWidgets.QLabel(value or "N/A", other_information) ) timestamp_label = QtWidgets.QLabel( From deec5e5c936abd1fca49cc586875e0d4f671e761 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 Aug 2022 12:30:29 +0200 Subject: [PATCH 0955/1030] nuke: fixing setting colorspace --- openpype/hosts/nuke/api/lib.py | 38 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a53d932db1..10ddfca51e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1945,15 +1945,25 @@ class WorkfileSettings(object): if not write_node: return - # write all knobs to node - for knob in nuke_imageio_writes["knobs"]: - value = knob["value"] - if isinstance(value, six.text_type): - value = str(value) - if str(value).startswith("0x"): - value = int(value, 16) + try: + # write all knobs to node + for knob in nuke_imageio_writes["knobs"]: + value = knob["value"] + if isinstance(value, six.text_type): + value = str(value) + if str(value).startswith("0x"): + value = int(value, 16) - write_node[knob["name"]].setValue(value) + log.debug("knob: {}| value: {}".format( + knob["name"], value + )) + write_node[knob["name"]].setValue(value) + except TypeError: + log.warning( + "Legacy workflow didnt work, switching to current") + + set_node_knobs_from_settings( + write_node, nuke_imageio_writes["knobs"]) def set_reads_colorspace(self, read_clrs_inputs): """ Setting colorspace to Read nodes @@ -2010,12 +2020,14 @@ class WorkfileSettings(object): # get imageio nuke_colorspace = get_nuke_imageio_settings() + log.info("Setting colorspace to workfile...") try: self.set_root_colorspace(nuke_colorspace["workfile"]) except AttributeError: msg = "set_colorspace(): missing `workfile` settings in template" nuke.message(msg) + log.info("Setting colorspace to viewers...") try: self.set_viewers_colorspace(nuke_colorspace["viewer"]) except AttributeError: @@ -2023,24 +2035,18 @@ class WorkfileSettings(object): nuke.message(msg) log.error(msg) + log.info("Setting colorspace to write nodes...") try: self.set_writes_colorspace() except AttributeError as _error: nuke.message(_error) log.error(_error) + log.info("Setting colorspace to read nodes...") read_clrs_inputs = nuke_colorspace["regexInputs"].get("inputs", []) if read_clrs_inputs: self.set_reads_colorspace(read_clrs_inputs) - try: - for key in nuke_colorspace: - log.debug("Preset's colorspace key: {}".format(key)) - except TypeError: - msg = "Nuke is not in templates! Contact your supervisor!" - nuke.message(msg) - log.error(msg) - def reset_frame_range_handles(self): """Set frame range to current asset""" From 3ab974f98c96c823953e349fe499b48c5e4a0a7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 Aug 2022 13:46:19 +0200 Subject: [PATCH 0956/1030] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/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 2b9d3bded2fb9b2af2a589c186760f884fdcb752 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:00:43 +0200 Subject: [PATCH 0957/1030] created nuke module --- openpype/hosts/nuke/__init__.py | 47 +++++----------------------- openpype/hosts/nuke/module.py | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 openpype/hosts/nuke/module.py diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index 134a6621c4..718307583e 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -1,41 +1,10 @@ -import os -import platform +from .module import ( + NUKE_ROOT_DIR, + NukeModule, +) -def add_implementation_envs(env, _app): - # Add requirements to NUKE_PATH - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_nuke_paths = [ - os.path.join(pype_root, "openpype", "hosts", "nuke", "startup") - ] - old_nuke_path = env.get("NUKE_PATH") or "" - for path in old_nuke_path.split(os.pathsep): - if not path: - continue - - norm_path = os.path.normpath(path) - if norm_path not in new_nuke_paths: - new_nuke_paths.append(norm_path) - - env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) - env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) - - # Try to add QuickTime to PATH - quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" - if platform.system() == "windows" and os.path.exists(quick_time_path): - path_value = env.get("PATH") or "" - path_paths = [ - path - for path in path_value.split(os.pathsep) - if path - ] - path_paths.append(quick_time_path) - env["PATH"] = os.pathsep.join(path_paths) - - # Set default values if are not already set via settings - defaults = { - "LOGLEVEL": "DEBUG" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value +__all__ = ( + "NUKE_ROOT_DIR", + "NukeModule", +) diff --git a/openpype/hosts/nuke/module.py b/openpype/hosts/nuke/module.py new file mode 100644 index 0000000000..a50444f817 --- /dev/null +++ b/openpype/hosts/nuke/module.py @@ -0,0 +1,54 @@ +import os +import platform +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class NukeModule(OpenPypeModule, IHostModule): + name = "nuke" + host_name = "nuke" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to NUKE_PATH + new_nuke_paths = [ + os.path.join(NUKE_ROOT_DIR, "startup") + ] + old_nuke_path = env.get("NUKE_PATH") or "" + for path in old_nuke_path.split(os.pathsep): + if not path: + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_nuke_paths: + new_nuke_paths.append(norm_path) + + env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) + env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + + # Set default values if are not already set via settings + defaults = { + "LOGLEVEL": "DEBUG" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + # Try to add QuickTime to PATH + quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" + if platform.system() == "windows" and os.path.exists(quick_time_path): + path_value = env.get("PATH") or "" + path_paths = [ + path + for path in path_value.split(os.pathsep) + if path + ] + path_paths.append(quick_time_path) + env["PATH"] = os.pathsep.join(path_paths) + + def get_workfile_extensions(self): + return [".nk"] From 9b4654a1c8b8f2dfdbd840553a69781282a496ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:01:49 +0200 Subject: [PATCH 0958/1030] removed usage of HOST_WORKFILE_EXTENSIONS in nuke --- openpype/hosts/nuke/api/workio.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 68fcb0927f..65b86bf01b 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -2,11 +2,9 @@ import os import nuke -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["nuke"] + return [".nk"] def has_unsaved_changes(): From 013e37b44d3c0937ff748a6bcee5d42eef62219a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:02:04 +0200 Subject: [PATCH 0959/1030] added protobuf into nuke vendor --- openpype/hosts/nuke/module.py | 9 + .../nuke/vendor/google/protobuf/__init__.py | 33 + .../nuke/vendor/google/protobuf/any_pb2.py | 26 + .../nuke/vendor/google/protobuf/api_pb2.py | 32 + .../google/protobuf/compiler/__init__.py | 0 .../google/protobuf/compiler/plugin_pb2.py | 35 + .../nuke/vendor/google/protobuf/descriptor.py | 1224 +++++++++++ .../google/protobuf/descriptor_database.py | 177 ++ .../vendor/google/protobuf/descriptor_pb2.py | 1925 +++++++++++++++++ .../vendor/google/protobuf/descriptor_pool.py | 1295 +++++++++++ .../vendor/google/protobuf/duration_pb2.py | 26 + .../nuke/vendor/google/protobuf/empty_pb2.py | 26 + .../vendor/google/protobuf/field_mask_pb2.py | 26 + .../google/protobuf/internal/__init__.py | 0 .../protobuf/internal/_parameterized.py | 443 ++++ .../protobuf/internal/api_implementation.py | 112 + .../google/protobuf/internal/builder.py | 130 ++ .../google/protobuf/internal/containers.py | 710 ++++++ .../google/protobuf/internal/decoder.py | 1029 +++++++++ .../google/protobuf/internal/encoder.py | 829 +++++++ .../protobuf/internal/enum_type_wrapper.py | 124 ++ .../protobuf/internal/extension_dict.py | 213 ++ .../protobuf/internal/message_listener.py | 78 + .../internal/message_set_extensions_pb2.py | 36 + .../internal/missing_enum_values_pb2.py | 37 + .../internal/more_extensions_dynamic_pb2.py | 29 + .../protobuf/internal/more_extensions_pb2.py | 41 + .../protobuf/internal/more_messages_pb2.py | 556 +++++ .../protobuf/internal/no_package_pb2.py | 27 + .../protobuf/internal/python_message.py | 1539 +++++++++++++ .../google/protobuf/internal/type_checkers.py | 435 ++++ .../protobuf/internal/well_known_types.py | 878 ++++++++ .../google/protobuf/internal/wire_format.py | 268 +++ .../vendor/google/protobuf/json_format.py | 912 ++++++++ .../nuke/vendor/google/protobuf/message.py | 424 ++++ .../vendor/google/protobuf/message_factory.py | 185 ++ .../vendor/google/protobuf/proto_builder.py | 134 ++ .../vendor/google/protobuf/pyext/__init__.py | 0 .../google/protobuf/pyext/cpp_message.py | 65 + .../google/protobuf/pyext/python_pb2.py | 34 + .../nuke/vendor/google/protobuf/reflection.py | 95 + .../nuke/vendor/google/protobuf/service.py | 228 ++ .../google/protobuf/service_reflection.py | 295 +++ .../google/protobuf/source_context_pb2.py | 26 + .../nuke/vendor/google/protobuf/struct_pb2.py | 36 + .../vendor/google/protobuf/symbol_database.py | 194 ++ .../vendor/google/protobuf/text_encoding.py | 110 + .../vendor/google/protobuf/text_format.py | 1795 +++++++++++++++ .../vendor/google/protobuf/timestamp_pb2.py | 26 + .../nuke/vendor/google/protobuf/type_pb2.py | 42 + .../vendor/google/protobuf/util/__init__.py | 0 .../google/protobuf/util/json_format_pb2.py | 72 + .../protobuf/util/json_format_proto3_pb2.py | 129 ++ .../vendor/google/protobuf/wrappers_pb2.py | 42 + 54 files changed, 17192 insertions(+) create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/__init__.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/compiler/__init__.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/descriptor.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/__init__.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/encoder.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/enum_type_wrapper.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/json_format.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/message.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/message_factory.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/pyext/__init__.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/reflection.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/service.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/text_format.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/util/__init__.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py create mode 100644 openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py diff --git a/openpype/hosts/nuke/module.py b/openpype/hosts/nuke/module.py index a50444f817..e4706a36cb 100644 --- a/openpype/hosts/nuke/module.py +++ b/openpype/hosts/nuke/module.py @@ -30,6 +30,15 @@ class NukeModule(OpenPypeModule, IHostModule): env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + # Add vendor to PYTHONPATH + python_path = env["PYTHONPATH"] + python_path_parts = [] + if python_path: + python_path_parts = python_path.split(os.pathsep) + vendor_path = os.path.join(NUKE_ROOT_DIR, "vendor") + python_path_parts.insert(0, vendor_path) + env["PYTHONPATH"] = os.pathsep.join(python_path_parts) + # Set default values if are not already set via settings defaults = { "LOGLEVEL": "DEBUG" diff --git a/openpype/hosts/nuke/vendor/google/protobuf/__init__.py b/openpype/hosts/nuke/vendor/google/protobuf/__init__.py new file mode 100644 index 0000000000..03f3b29ee7 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/__init__.py @@ -0,0 +1,33 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Copyright 2007 Google Inc. All Rights Reserved. + +__version__ = '3.20.1' diff --git a/openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py new file mode 100644 index 0000000000..9121193d11 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/any.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"&\n\x03\x41ny\x12\x10\n\x08type_url\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42v\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _ANY._serialized_start=46 + _ANY._serialized_end=84 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py new file mode 100644 index 0000000000..1721b10a75 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/api.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 +from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\x81\x02\n\x03\x41pi\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Method\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12&\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.Mixin\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x01\n\x06Method\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10request_type_url\x18\x02 \x01(\t\x12\x19\n\x11request_streaming\x18\x03 \x01(\x08\x12\x19\n\x11response_type_url\x18\x04 \x01(\t\x12\x1a\n\x12response_streaming\x18\x05 \x01(\x08\x12(\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.Option\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"#\n\x05Mixin\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04root\x18\x02 \x01(\tBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _API._serialized_start=113 + _API._serialized_end=370 + _METHOD._serialized_start=373 + _METHOD._serialized_end=586 + _MIXIN._serialized_start=588 + _MIXIN._serialized_end=623 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/compiler/__init__.py b/openpype/hosts/nuke/vendor/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py new file mode 100644 index 0000000000..715a891370 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/compiler/plugin.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"F\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\x05\x12\r\n\x05minor\x18\x02 \x01(\x05\x12\r\n\x05patch\x18\x03 \x01(\x05\x12\x0e\n\x06suffix\x18\x04 \x01(\t\"\xba\x01\n\x14\x43odeGeneratorRequest\x12\x18\n\x10\x66ile_to_generate\x18\x01 \x03(\t\x12\x11\n\tparameter\x18\x02 \x01(\t\x12\x38\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\x12;\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.Version\"\xc1\x02\n\x15\x43odeGeneratorResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x1a\n\x12supported_features\x18\x02 \x01(\x04\x12\x42\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.File\x1a\x7f\n\x04\x46ile\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0finsertion_point\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x0f \x01(\t\x12?\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfo\"8\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x42W\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb' + _VERSION._serialized_start=101 + _VERSION._serialized_end=171 + _CODEGENERATORREQUEST._serialized_start=174 + _CODEGENERATORREQUEST._serialized_end=360 + _CODEGENERATORRESPONSE._serialized_start=363 + _CODEGENERATORRESPONSE._serialized_end=684 + _CODEGENERATORRESPONSE_FILE._serialized_start=499 + _CODEGENERATORRESPONSE_FILE._serialized_end=626 + _CODEGENERATORRESPONSE_FEATURE._serialized_start=628 + _CODEGENERATORRESPONSE_FEATURE._serialized_end=684 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/descriptor.py b/openpype/hosts/nuke/vendor/google/protobuf/descriptor.py new file mode 100644 index 0000000000..ad70be9a11 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/descriptor.py @@ -0,0 +1,1224 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Descriptors essentially contain exactly the information found in a .proto +file, in types that make this information accessible in Python. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import threading +import warnings + +from google.protobuf.internal import api_implementation + +_USE_C_DESCRIPTORS = False +if api_implementation.Type() == 'cpp': + # Used by MakeDescriptor in cpp mode + import binascii + import os + from google.protobuf.pyext import _message + _USE_C_DESCRIPTORS = True + + +class Error(Exception): + """Base error for this module.""" + + +class TypeTransformationError(Error): + """Error transforming between python proto type and corresponding C++ type.""" + + +if _USE_C_DESCRIPTORS: + # This metaclass allows to override the behavior of code like + # isinstance(my_descriptor, FieldDescriptor) + # and make it return True when the descriptor is an instance of the extension + # type written in C++. + class DescriptorMetaclass(type): + def __instancecheck__(cls, obj): + if super(DescriptorMetaclass, cls).__instancecheck__(obj): + return True + if isinstance(obj, cls._C_DESCRIPTOR_CLASS): + return True + return False +else: + # The standard metaclass; nothing changes. + DescriptorMetaclass = type + + +class _Lock(object): + """Wrapper class of threading.Lock(), which is allowed by 'with'.""" + + def __new__(cls): + self = object.__new__(cls) + self._lock = threading.Lock() # pylint: disable=protected-access + return self + + def __enter__(self): + self._lock.acquire() + + def __exit__(self, exc_type, exc_value, exc_tb): + self._lock.release() + + +_lock = threading.Lock() + + +def _Deprecated(name): + if _Deprecated.count > 0: + _Deprecated.count -= 1 + warnings.warn( + 'Call to deprecated create function %s(). Note: Create unlinked ' + 'descriptors is going to go away. Please use get/find descriptors from ' + 'generated code or query the descriptor_pool.' + % name, + category=DeprecationWarning, stacklevel=3) + + +# Deprecated warnings will print 100 times at most which should be enough for +# users to notice and do not cause timeout. +_Deprecated.count = 100 + + +_internal_create_key = object() + + +class DescriptorBase(metaclass=DescriptorMetaclass): + + """Descriptors base class. + + This class is the base of all descriptor classes. It provides common options + related functionality. + + Attributes: + has_options: True if the descriptor has non-default options. Usually it + is not necessary to read this -- just call GetOptions() which will + happily return the default instance. However, it's sometimes useful + for efficiency, and also useful inside the protobuf implementation to + avoid some bootstrapping issues. + """ + + if _USE_C_DESCRIPTORS: + # The class, or tuple of classes, that are considered as "virtual + # subclasses" of this descriptor class. + _C_DESCRIPTOR_CLASS = () + + def __init__(self, options, serialized_options, options_class_name): + """Initialize the descriptor given its options message and the name of the + class of the options message. The name of the class is required in case + the options message is None and has to be created. + """ + self._options = options + self._options_class_name = options_class_name + self._serialized_options = serialized_options + + # Does this descriptor have non-default options? + self.has_options = (options is not None) or (serialized_options is not None) + + def _SetOptions(self, options, options_class_name): + """Sets the descriptor's options + + This function is used in generated proto2 files to update descriptor + options. It must not be used outside proto2. + """ + self._options = options + self._options_class_name = options_class_name + + # Does this descriptor have non-default options? + self.has_options = options is not None + + def GetOptions(self): + """Retrieves descriptor options. + + This method returns the options set or creates the default options for the + descriptor. + """ + if self._options: + return self._options + + from google.protobuf import descriptor_pb2 + try: + options_class = getattr(descriptor_pb2, + self._options_class_name) + except AttributeError: + raise RuntimeError('Unknown options class name %s!' % + (self._options_class_name)) + + with _lock: + if self._serialized_options is None: + self._options = options_class() + else: + self._options = _ParseOptions(options_class(), + self._serialized_options) + + return self._options + + +class _NestedDescriptorBase(DescriptorBase): + """Common class for descriptors that can be nested.""" + + def __init__(self, options, options_class_name, name, full_name, + file, containing_type, serialized_start=None, + serialized_end=None, serialized_options=None): + """Constructor. + + Args: + options: Protocol message options or None + to use default message options. + options_class_name (str): The class name of the above options. + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + file (FileDescriptor): Reference to file info. + containing_type: if provided, this is a nested descriptor, with this + descriptor as parent, otherwise None. + serialized_start: The start index (inclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_end: The end index (exclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_options: Protocol message serialized options or None. + """ + super(_NestedDescriptorBase, self).__init__( + options, serialized_options, options_class_name) + + self.name = name + # TODO(falk): Add function to calculate full_name instead of having it in + # memory? + self.full_name = full_name + self.file = file + self.containing_type = containing_type + + self._serialized_start = serialized_start + self._serialized_end = serialized_end + + def CopyToProto(self, proto): + """Copies this to the matching proto in descriptor_pb2. + + Args: + proto: An empty proto instance from descriptor_pb2. + + Raises: + Error: If self couldn't be serialized, due to to few constructor + arguments. + """ + if (self.file is not None and + self._serialized_start is not None and + self._serialized_end is not None): + proto.ParseFromString(self.file.serialized_pb[ + self._serialized_start:self._serialized_end]) + else: + raise Error('Descriptor does not contain serialization.') + + +class Descriptor(_NestedDescriptorBase): + + """Descriptor for a protocol message type. + + Attributes: + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + containing_type (Descriptor): Reference to the descriptor of the type + containing us, or None if this is top-level. + fields (list[FieldDescriptor]): Field descriptors for all fields in + this type. + fields_by_number (dict(int, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed + by "number" attribute in each FieldDescriptor. + fields_by_name (dict(str, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by + "name" attribute in each :class:`FieldDescriptor`. + nested_types (list[Descriptor]): Descriptor references + for all protocol message types nested within this one. + nested_types_by_name (dict(str, Descriptor)): Same Descriptor + objects as in :attr:`nested_types`, but indexed by "name" attribute + in each Descriptor. + enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references + for all enums contained within this type. + enum_types_by_name (dict(str, EnumDescriptor)): Same + :class:`EnumDescriptor` objects as in :attr:`enum_types`, but + indexed by "name" attribute in each EnumDescriptor. + enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping + from enum value name to :class:`EnumValueDescriptor` for that value. + extensions (list[FieldDescriptor]): All extensions defined directly + within this message type (NOT within a nested type). + extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor + objects as :attr:`extensions`, but indexed by "name" attribute of each + FieldDescriptor. + is_extendable (bool): Does this type define any extension ranges? + oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields + in this message. + oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in + :attr:`oneofs`, but indexed by "name" attribute. + file (FileDescriptor): Reference to file descriptor. + + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.Descriptor + + def __new__( + cls, + name=None, + full_name=None, + filename=None, + containing_type=None, + fields=None, + nested_types=None, + enum_types=None, + extensions=None, + options=None, + serialized_options=None, + is_extendable=True, + extension_ranges=None, + oneofs=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + syntax=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindMessageTypeByName(full_name) + + # NOTE(tmarek): The file argument redefining a builtin is nothing we can + # fix right now since we don't know how many clients already rely on the + # name of the argument. + def __init__(self, name, full_name, filename, containing_type, fields, + nested_types, enum_types, extensions, options=None, + serialized_options=None, + is_extendable=True, extension_ranges=None, oneofs=None, + file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin + syntax=None, create_key=None): + """Arguments to __init__() are as described in the description + of Descriptor fields above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('Descriptor') + + super(Descriptor, self).__init__( + options, 'MessageOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + # We have fields in addition to fields_by_name and fields_by_number, + # so that: + # 1. Clients can index fields by "order in which they're listed." + # 2. Clients can easily iterate over all fields with the terse + # syntax: for f in descriptor.fields: ... + self.fields = fields + for field in self.fields: + field.containing_type = self + self.fields_by_number = dict((f.number, f) for f in fields) + self.fields_by_name = dict((f.name, f) for f in fields) + self._fields_by_camelcase_name = None + + self.nested_types = nested_types + for nested_type in nested_types: + nested_type.containing_type = self + self.nested_types_by_name = dict((t.name, t) for t in nested_types) + + self.enum_types = enum_types + for enum_type in self.enum_types: + enum_type.containing_type = self + self.enum_types_by_name = dict((t.name, t) for t in enum_types) + self.enum_values_by_name = dict( + (v.name, v) for t in enum_types for v in t.values) + + self.extensions = extensions + for extension in self.extensions: + extension.extension_scope = self + self.extensions_by_name = dict((f.name, f) for f in extensions) + self.is_extendable = is_extendable + self.extension_ranges = extension_ranges + self.oneofs = oneofs if oneofs is not None else [] + self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) + for oneof in self.oneofs: + oneof.containing_type = self + self.syntax = syntax or "proto2" + + @property + def fields_by_camelcase_name(self): + """Same FieldDescriptor objects as in :attr:`fields`, but indexed by + :attr:`FieldDescriptor.camelcase_name`. + """ + if self._fields_by_camelcase_name is None: + self._fields_by_camelcase_name = dict( + (f.camelcase_name, f) for f in self.fields) + return self._fields_by_camelcase_name + + def EnumValueName(self, enum, value): + """Returns the string name of an enum value. + + This is just a small helper method to simplify a common operation. + + Args: + enum: string name of the Enum. + value: int, value of the enum. + + Returns: + string name of the enum value. + + Raises: + KeyError if either the Enum doesn't exist or the value is not a valid + value for the enum. + """ + return self.enum_types_by_name[enum].values_by_number[value].name + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.DescriptorProto. + + Args: + proto: An empty descriptor_pb2.DescriptorProto. + """ + # This function is overridden to give a better doc comment. + super(Descriptor, self).CopyToProto(proto) + + +# TODO(robinson): We should have aggressive checking here, +# for example: +# * If you specify a repeated field, you should not be allowed +# to specify a default value. +# * [Other examples here as needed]. +# +# TODO(robinson): for this and other *Descriptor classes, we +# might also want to lock things down aggressively (e.g., +# prevent clients from setting the attributes). Having +# stronger invariants here in general will reduce the number +# of runtime checks we must do in reflection.py... +class FieldDescriptor(DescriptorBase): + + """Descriptor for a single field in a .proto file. + + Attributes: + name (str): Name of this field, exactly as it appears in .proto. + full_name (str): Name of this field, including containing scope. This is + particularly relevant for extensions. + index (int): Dense, 0-indexed index giving the order that this + field textually appears within its message in the .proto file. + number (int): Tag number declared for this field in the .proto file. + + type (int): (One of the TYPE_* constants below) Declared type. + cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to + represent this field. + + label (int): (One of the LABEL_* constants below) Tells whether this + field is optional, required, or repeated. + has_default_value (bool): True if this field has a default value defined, + otherwise false. + default_value (Varies): Default value of this field. Only + meaningful for non-repeated scalar fields. Repeated fields + should always set this to [], and non-repeated composite + fields should always set this to None. + + containing_type (Descriptor): Descriptor of the protocol message + type that contains this field. Set by the Descriptor constructor + if we're passed into one. + Somewhat confusingly, for extension fields, this is the + descriptor of the EXTENDED message, not the descriptor + of the message containing this field. (See is_extension and + extension_scope below). + message_type (Descriptor): If a composite field, a descriptor + of the message type contained in this field. Otherwise, this is None. + enum_type (EnumDescriptor): If this field contains an enum, a + descriptor of that enum. Otherwise, this is None. + + is_extension: True iff this describes an extension field. + extension_scope (Descriptor): Only meaningful if is_extension is True. + Gives the message that immediately contains this extension field. + Will be None iff we're a top-level (file-level) extension field. + + options (descriptor_pb2.FieldOptions): Protocol message field options or + None to use default field options. + + containing_oneof (OneofDescriptor): If the field is a member of a oneof + union, contains its descriptor. Otherwise, None. + + file (FileDescriptor): Reference to file descriptor. + """ + + # Must be consistent with C++ FieldDescriptor::Type enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + TYPE_DOUBLE = 1 + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + MAX_TYPE = 18 + + # Must be consistent with C++ FieldDescriptor::CppType enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + CPPTYPE_INT32 = 1 + CPPTYPE_INT64 = 2 + CPPTYPE_UINT32 = 3 + CPPTYPE_UINT64 = 4 + CPPTYPE_DOUBLE = 5 + CPPTYPE_FLOAT = 6 + CPPTYPE_BOOL = 7 + CPPTYPE_ENUM = 8 + CPPTYPE_STRING = 9 + CPPTYPE_MESSAGE = 10 + MAX_CPPTYPE = 10 + + _PYTHON_TO_CPP_PROTO_TYPE_MAP = { + TYPE_DOUBLE: CPPTYPE_DOUBLE, + TYPE_FLOAT: CPPTYPE_FLOAT, + TYPE_ENUM: CPPTYPE_ENUM, + TYPE_INT64: CPPTYPE_INT64, + TYPE_SINT64: CPPTYPE_INT64, + TYPE_SFIXED64: CPPTYPE_INT64, + TYPE_UINT64: CPPTYPE_UINT64, + TYPE_FIXED64: CPPTYPE_UINT64, + TYPE_INT32: CPPTYPE_INT32, + TYPE_SFIXED32: CPPTYPE_INT32, + TYPE_SINT32: CPPTYPE_INT32, + TYPE_UINT32: CPPTYPE_UINT32, + TYPE_FIXED32: CPPTYPE_UINT32, + TYPE_BYTES: CPPTYPE_STRING, + TYPE_STRING: CPPTYPE_STRING, + TYPE_BOOL: CPPTYPE_BOOL, + TYPE_MESSAGE: CPPTYPE_MESSAGE, + TYPE_GROUP: CPPTYPE_MESSAGE + } + + # Must be consistent with C++ FieldDescriptor::Label enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + LABEL_OPTIONAL = 1 + LABEL_REQUIRED = 2 + LABEL_REPEATED = 3 + MAX_LABEL = 3 + + # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, + # and kLastReservedNumber in descriptor.h + MAX_FIELD_NUMBER = (1 << 29) - 1 + FIRST_RESERVED_FIELD_NUMBER = 19000 + LAST_RESERVED_FIELD_NUMBER = 19999 + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FieldDescriptor + + def __new__(cls, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + _message.Message._CheckCalledFromGeneratedFile() + if is_extension: + return _message.default_pool.FindExtensionByName(full_name) + else: + return _message.default_pool.FindFieldByName(full_name) + + def __init__(self, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + """The arguments are as described in the description of FieldDescriptor + attributes above. + + Note that containing_type may be None, and may be set later if necessary + (to deal with circular references between message types, for example). + Likewise for extension_scope. + """ + if create_key is not _internal_create_key: + _Deprecated('FieldDescriptor') + + super(FieldDescriptor, self).__init__( + options, serialized_options, 'FieldOptions') + self.name = name + self.full_name = full_name + self.file = file + self._camelcase_name = None + if json_name is None: + self.json_name = _ToJsonName(name) + else: + self.json_name = json_name + self.index = index + self.number = number + self.type = type + self.cpp_type = cpp_type + self.label = label + self.has_default_value = has_default_value + self.default_value = default_value + self.containing_type = containing_type + self.message_type = message_type + self.enum_type = enum_type + self.is_extension = is_extension + self.extension_scope = extension_scope + self.containing_oneof = containing_oneof + if api_implementation.Type() == 'cpp': + if is_extension: + self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) + else: + self._cdescriptor = _message.default_pool.FindFieldByName(full_name) + else: + self._cdescriptor = None + + @property + def camelcase_name(self): + """Camelcase name of this field. + + Returns: + str: the name in CamelCase. + """ + if self._camelcase_name is None: + self._camelcase_name = _ToCamelCase(self.name) + return self._camelcase_name + + @property + def has_presence(self): + """Whether the field distinguishes between unpopulated and default values. + + Raises: + RuntimeError: singular field that is not linked with message nor file. + """ + if self.label == FieldDescriptor.LABEL_REPEATED: + return False + if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or + self.containing_oneof): + return True + if hasattr(self.file, 'syntax'): + return self.file.syntax == 'proto2' + if hasattr(self.message_type, 'syntax'): + return self.message_type.syntax == 'proto2' + raise RuntimeError( + 'has_presence is not ready to use because field %s is not' + ' linked with message type nor file' % self.full_name) + + @staticmethod + def ProtoTypeToCppProtoType(proto_type): + """Converts from a Python proto type to a C++ Proto Type. + + The Python ProtocolBuffer classes specify both the 'Python' datatype and the + 'C++' datatype - and they're not the same. This helper method should + translate from one to another. + + Args: + proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) + Returns: + int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. + Raises: + TypeTransformationError: when the Python proto type isn't known. + """ + try: + return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] + except KeyError: + raise TypeTransformationError('Unknown proto_type: %s' % proto_type) + + +class EnumDescriptor(_NestedDescriptorBase): + + """Descriptor for an enum defined in a .proto file. + + Attributes: + name (str): Name of the enum type. + full_name (str): Full name of the type, including package name + and any enclosing type(s). + + values (list[EnumValueDescriptor]): List of the values + in this enum. + values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "name" field of each EnumValueDescriptor. + values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "number" field of each EnumValueDescriptor. + containing_type (Descriptor): Descriptor of the immediate containing + type of this enum, or None if this is an enum defined at the + top level in a .proto file. Set by Descriptor's constructor + if we're passed into one. + file (FileDescriptor): Reference to file descriptor. + options (descriptor_pb2.EnumOptions): Enum options message or + None to use default enum options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumDescriptor + + def __new__(cls, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindEnumTypeByName(full_name) + + def __init__(self, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + """Arguments are as described in the attribute description above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('EnumDescriptor') + + super(EnumDescriptor, self).__init__( + options, 'EnumOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + self.values = values + for value in self.values: + value.type = self + self.values_by_name = dict((v.name, v) for v in values) + # Values are reversed to ensure that the first alias is retained. + self.values_by_number = dict((v.number, v) for v in reversed(values)) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.EnumDescriptorProto. + + Args: + proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(EnumDescriptor, self).CopyToProto(proto) + + +class EnumValueDescriptor(DescriptorBase): + + """Descriptor for a single value within an enum. + + Attributes: + name (str): Name of this value. + index (int): Dense, 0-indexed index giving the order that this + value appears textually within its enum in the .proto file. + number (int): Actual number assigned to this enum value. + type (EnumDescriptor): :class:`EnumDescriptor` to which this value + belongs. Set by :class:`EnumDescriptor`'s constructor if we're + passed into one. + options (descriptor_pb2.EnumValueOptions): Enum value options message or + None to use default enum value options options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor + + def __new__(cls, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + # There is no way we can build a complete EnumValueDescriptor with the + # given parameters (the name of the Enum is not known, for example). + # Fortunately generated files just pass it to the EnumDescriptor() + # constructor, which will ignore it, so returning None is good enough. + return None + + def __init__(self, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('EnumValueDescriptor') + + super(EnumValueDescriptor, self).__init__( + options, serialized_options, 'EnumValueOptions') + self.name = name + self.index = index + self.number = number + self.type = type + + +class OneofDescriptor(DescriptorBase): + """Descriptor for a oneof field. + + Attributes: + name (str): Name of the oneof field. + full_name (str): Full name of the oneof field, including package name. + index (int): 0-based index giving the order of the oneof field inside + its containing type. + containing_type (Descriptor): :class:`Descriptor` of the protocol message + type that contains this field. Set by the :class:`Descriptor` constructor + if we're passed into one. + fields (list[FieldDescriptor]): The list of field descriptors this + oneof can contain. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.OneofDescriptor + + def __new__( + cls, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindOneofByName(full_name) + + def __init__( + self, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('OneofDescriptor') + + super(OneofDescriptor, self).__init__( + options, serialized_options, 'OneofOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_type = containing_type + self.fields = fields + + +class ServiceDescriptor(_NestedDescriptorBase): + + """Descriptor for a service. + + Attributes: + name (str): Name of the service. + full_name (str): Full name of the service, including package name. + index (int): 0-indexed index giving the order that this services + definition appears within the .proto file. + methods (list[MethodDescriptor]): List of methods provided by this + service. + methods_by_name (dict(str, MethodDescriptor)): Same + :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but + indexed by "name" attribute in each :class:`MethodDescriptor`. + options (descriptor_pb2.ServiceOptions): Service options message or + None to use default service options. + file (FileDescriptor): Reference to file info. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor + + def __new__( + cls, + name=None, + full_name=None, + index=None, + methods=None, + options=None, + serialized_options=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindServiceByName(full_name) + + def __init__(self, name, full_name, index, methods, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + if create_key is not _internal_create_key: + _Deprecated('ServiceDescriptor') + + super(ServiceDescriptor, self).__init__( + options, 'ServiceOptions', name, full_name, file, + None, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + self.index = index + self.methods = methods + self.methods_by_name = dict((m.name, m) for m in methods) + # Set the containing service for each method in this service. + for method in self.methods: + method.containing_service = self + + def FindMethodByName(self, name): + """Searches for the specified method, and returns its descriptor. + + Args: + name (str): Name of the method. + Returns: + MethodDescriptor or None: the descriptor for the requested method, if + found. + """ + return self.methods_by_name.get(name, None) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.ServiceDescriptorProto. + + Args: + proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(ServiceDescriptor, self).CopyToProto(proto) + + +class MethodDescriptor(DescriptorBase): + + """Descriptor for a method in a service. + + Attributes: + name (str): Name of the method within the service. + full_name (str): Full name of method. + index (int): 0-indexed index of the method inside the service. + containing_service (ServiceDescriptor): The service that contains this + method. + input_type (Descriptor): The descriptor of the message that this method + accepts. + output_type (Descriptor): The descriptor of the message that this method + returns. + client_streaming (bool): Whether this method uses client streaming. + server_streaming (bool): Whether this method uses server streaming. + options (descriptor_pb2.MethodOptions or None): Method options message, or + None to use default method options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.MethodDescriptor + + def __new__(cls, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindMethodByName(full_name) + + def __init__(self, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + """The arguments are as described in the description of MethodDescriptor + attributes above. + + Note that containing_service may be None, and may be set later if necessary. + """ + if create_key is not _internal_create_key: + _Deprecated('MethodDescriptor') + + super(MethodDescriptor, self).__init__( + options, serialized_options, 'MethodOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_service = containing_service + self.input_type = input_type + self.output_type = output_type + self.client_streaming = client_streaming + self.server_streaming = server_streaming + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.MethodDescriptorProto. + + Args: + proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. + + Raises: + Error: If self couldn't be serialized, due to too few constructor + arguments. + """ + if self.containing_service is not None: + from google.protobuf import descriptor_pb2 + service_proto = descriptor_pb2.ServiceDescriptorProto() + self.containing_service.CopyToProto(service_proto) + proto.CopyFrom(service_proto.method[self.index]) + else: + raise Error('Descriptor does not contain a service.') + + +class FileDescriptor(DescriptorBase): + """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. + + Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and + :attr:`dependencies` fields are only set by the + :py:mod:`google.protobuf.message_factory` module, and not by the generated + proto code. + + Attributes: + name (str): Name of file, relative to root of source tree. + package (str): Name of the package + syntax (str): string indicating syntax of the file (can be "proto2" or + "proto3") + serialized_pb (bytes): Byte string of serialized + :class:`descriptor_pb2.FileDescriptorProto`. + dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` + objects this :class:`FileDescriptor` depends on. + public_dependencies (list[FileDescriptor]): A subset of + :attr:`dependencies`, which were declared as "public". + message_types_by_name (dict(str, Descriptor)): Mapping from message names + to their :class:`Descriptor`. + enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to + their :class:`EnumDescriptor`. + extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension + names declared at file scope to their :class:`FieldDescriptor`. + services_by_name (dict(str, ServiceDescriptor)): Mapping from services' + names to their :class:`ServiceDescriptor`. + pool (DescriptorPool): The pool this descriptor belongs to. When not + passed to the constructor, the global default pool is used. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FileDescriptor + + def __new__(cls, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + # FileDescriptor() is called from various places, not only from generated + # files, to register dynamic proto files and messages. + # pylint: disable=g-explicit-bool-comparison + if serialized_pb == b'': + # Cpp generated code must be linked in if serialized_pb is '' + try: + return _message.default_pool.FindFileByName(name) + except KeyError: + raise RuntimeError('Please link in cpp generated lib for %s' % (name)) + elif serialized_pb: + return _message.default_pool.AddSerializedFile(serialized_pb) + else: + return super(FileDescriptor, cls).__new__(cls) + + def __init__(self, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + """Constructor.""" + if create_key is not _internal_create_key: + _Deprecated('FileDescriptor') + + super(FileDescriptor, self).__init__( + options, serialized_options, 'FileOptions') + + if pool is None: + from google.protobuf import descriptor_pool + pool = descriptor_pool.Default() + self.pool = pool + self.message_types_by_name = {} + self.name = name + self.package = package + self.syntax = syntax or "proto2" + self.serialized_pb = serialized_pb + + self.enum_types_by_name = {} + self.extensions_by_name = {} + self.services_by_name = {} + self.dependencies = (dependencies or []) + self.public_dependencies = (public_dependencies or []) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.FileDescriptorProto. + + Args: + proto: An empty descriptor_pb2.FileDescriptorProto. + """ + proto.ParseFromString(self.serialized_pb) + + +def _ParseOptions(message, string): + """Parses serialized options. + + This helper function is used to parse serialized options in generated + proto2 files. It must not be used outside proto2. + """ + message.ParseFromString(string) + return message + + +def _ToCamelCase(name): + """Converts name to camel-case and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + if result: + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + # Lower-case the first letter. + if result and result[0].isupper(): + result[0] = result[0].lower() + return ''.join(result) + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _ToJsonName(name): + """Converts name to Json name and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + return ''.join(result) + + +def MakeDescriptor(desc_proto, package='', build_file_if_cpp=True, + syntax=None): + """Make a protobuf Descriptor given a DescriptorProto protobuf. + + Handles nested descriptors. Note that this is limited to the scope of defining + a message inside of another message. Composite fields can currently only be + resolved if the message is defined in the same scope as the field. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: Optional package name for the new message Descriptor (string). + build_file_if_cpp: Update the C++ descriptor pool if api matches. + Set to False on recursion, so no duplicates are created. + syntax: The syntax/semantics that should be used. Set to "proto3" to get + proto3 field presence semantics. + Returns: + A Descriptor for protobuf messages. + """ + if api_implementation.Type() == 'cpp' and build_file_if_cpp: + # The C++ implementation requires all descriptors to be backed by the same + # definition in the C++ descriptor pool. To do this, we build a + # FileDescriptorProto with the same definition as this descriptor and build + # it into the pool. + from google.protobuf import descriptor_pb2 + file_descriptor_proto = descriptor_pb2.FileDescriptorProto() + file_descriptor_proto.message_type.add().MergeFrom(desc_proto) + + # Generate a random name for this proto file to prevent conflicts with any + # imported ones. We need to specify a file name so the descriptor pool + # accepts our FileDescriptorProto, but it is not important what that file + # name is actually set to. + proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') + + if package: + file_descriptor_proto.name = os.path.join(package.replace('.', '/'), + proto_name + '.proto') + file_descriptor_proto.package = package + else: + file_descriptor_proto.name = proto_name + '.proto' + + _message.default_pool.Add(file_descriptor_proto) + result = _message.default_pool.FindFileByName(file_descriptor_proto.name) + + if _USE_C_DESCRIPTORS: + return result.message_types_by_name[desc_proto.name] + + full_message_name = [desc_proto.name] + if package: full_message_name.insert(0, package) + + # Create Descriptors for enum types + enum_types = {} + for enum_proto in desc_proto.enum_type: + full_name = '.'.join(full_message_name + [enum_proto.name]) + enum_desc = EnumDescriptor( + enum_proto.name, full_name, None, [ + EnumValueDescriptor(enum_val.name, ii, enum_val.number, + create_key=_internal_create_key) + for ii, enum_val in enumerate(enum_proto.value)], + create_key=_internal_create_key) + enum_types[full_name] = enum_desc + + # Create Descriptors for nested types + nested_types = {} + for nested_proto in desc_proto.nested_type: + full_name = '.'.join(full_message_name + [nested_proto.name]) + # Nested types are just those defined inside of the message, not all types + # used by fields in the message, so no loops are possible here. + nested_desc = MakeDescriptor(nested_proto, + package='.'.join(full_message_name), + build_file_if_cpp=False, + syntax=syntax) + nested_types[full_name] = nested_desc + + fields = [] + for field_proto in desc_proto.field: + full_name = '.'.join(full_message_name + [field_proto.name]) + enum_desc = None + nested_desc = None + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + if field_proto.HasField('type_name'): + type_name = field_proto.type_name + full_type_name = '.'.join(full_message_name + + [type_name[type_name.rfind('.')+1:]]) + if full_type_name in nested_types: + nested_desc = nested_types[full_type_name] + elif full_type_name in enum_types: + enum_desc = enum_types[full_type_name] + # Else type_name references a non-local type, which isn't implemented + field = FieldDescriptor( + field_proto.name, full_name, field_proto.number - 1, + field_proto.number, field_proto.type, + FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), + field_proto.label, None, nested_desc, enum_desc, None, False, None, + options=_OptionsOrNone(field_proto), has_default_value=False, + json_name=json_name, create_key=_internal_create_key) + fields.append(field) + + desc_name = '.'.join(full_message_name) + return Descriptor(desc_proto.name, desc_name, None, None, fields, + list(nested_types.values()), list(enum_types.values()), [], + options=_OptionsOrNone(desc_proto), + create_key=_internal_create_key) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py new file mode 100644 index 0000000000..073eddc711 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py @@ -0,0 +1,177 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a container for DescriptorProtos.""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import warnings + + +class Error(Exception): + pass + + +class DescriptorDatabaseConflictingDefinitionError(Error): + """Raised when a proto is added with the same name & different descriptor.""" + + +class DescriptorDatabase(object): + """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" + + def __init__(self): + self._file_desc_protos_by_file = {} + self._file_desc_protos_by_symbol = {} + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this database. + + Args: + file_desc_proto: The FileDescriptorProto to add. + Raises: + DescriptorDatabaseConflictingDefinitionError: if an attempt is made to + add a proto with the same name but different definition than an + existing proto in the database. + """ + proto_name = file_desc_proto.name + if proto_name not in self._file_desc_protos_by_file: + self._file_desc_protos_by_file[proto_name] = file_desc_proto + elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: + raise DescriptorDatabaseConflictingDefinitionError( + '%s already added, but with different descriptor.' % proto_name) + else: + return + + # Add all the top-level descriptors to the index. + package = file_desc_proto.package + for message in file_desc_proto.message_type: + for name in _ExtractSymbols(message, package): + self._AddSymbol(name, file_desc_proto) + for enum in file_desc_proto.enum_type: + self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) + for enum_value in enum.value: + self._file_desc_protos_by_symbol[ + '.'.join((package, enum_value.name))] = file_desc_proto + for extension in file_desc_proto.extension: + self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) + for service in file_desc_proto.service: + self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) + + def FindFileByName(self, name): + """Finds the file descriptor proto by file name. + + Typically the file name is a relative path ending to a .proto file. The + proto with the given name will have to have been added to this database + using the Add method or else an error will be raised. + + Args: + name: The file name to find. + + Returns: + The file descriptor proto matching the name. + + Raises: + KeyError if no file by the given name was added. + """ + + return self._file_desc_protos_by_file[name] + + def FindFileContainingSymbol(self, symbol): + """Finds the file descriptor proto containing the specified symbol. + + The symbol should be a fully qualified name including the file descriptor's + package and any containing messages. Some examples: + + 'some.package.name.Message' + 'some.package.name.Message.NestedEnum' + 'some.package.name.Message.some_field' + + The file descriptor proto containing the specified symbol must be added to + this database using the Add method or else an error will be raised. + + Args: + symbol: The fully qualified symbol name. + + Returns: + The file descriptor proto containing the symbol. + + Raises: + KeyError if no file contains the specified symbol. + """ + try: + return self._file_desc_protos_by_symbol[symbol] + except KeyError: + # Fields, enum values, and nested extensions are not in + # _file_desc_protos_by_symbol. Try to find the top level + # descriptor. Non-existent nested symbol under a valid top level + # descriptor can also be found. The behavior is the same with + # protobuf C++. + top_level, _, _ = symbol.rpartition('.') + try: + return self._file_desc_protos_by_symbol[top_level] + except KeyError: + # Raise the original symbol as a KeyError for better diagnostics. + raise KeyError(symbol) + + def FindFileContainingExtension(self, extendee_name, extension_number): + # TODO(jieluo): implement this API. + return None + + def FindAllExtensionNumbers(self, extendee_name): + # TODO(jieluo): implement this API. + return [] + + def _AddSymbol(self, name, file_desc_proto): + if name in self._file_desc_protos_by_symbol: + warn_msg = ('Conflict register for file "' + file_desc_proto.name + + '": ' + name + + ' is already defined in file "' + + self._file_desc_protos_by_symbol[name].name + '"') + warnings.warn(warn_msg, RuntimeWarning) + self._file_desc_protos_by_symbol[name] = file_desc_proto + + +def _ExtractSymbols(desc_proto, package): + """Pulls out all the symbols from a descriptor proto. + + Args: + desc_proto: The proto to extract symbols from. + package: The package containing the descriptor type. + + Yields: + The fully qualified name found in the descriptor. + """ + message_name = package + '.' + desc_proto.name if package else desc_proto.name + yield message_name + for nested_type in desc_proto.nested_type: + for symbol in _ExtractSymbols(nested_type, message_name): + yield symbol + for enum_type in desc_proto.enum_type: + yield '.'.join((message_name, enum_type.name)) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py new file mode 100644 index 0000000000..f570386432 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py @@ -0,0 +1,1925 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/descriptor.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR = _descriptor.FileDescriptor( + name='google/protobuf/descriptor.proto', + package='google.protobuf', + syntax='proto2', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' + ) +else: + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') + +if _descriptor._USE_C_DESCRIPTORS == False: + _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='google.protobuf.FieldDescriptorProto.Type', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='TYPE_DOUBLE', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FLOAT', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT64', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT64', index=3, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT32', index=4, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED64', index=5, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED32', index=6, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BOOL', index=7, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_STRING', index=8, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_GROUP', index=9, number=10, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_MESSAGE', index=10, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BYTES', index=11, number=12, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT32', index=12, number=13, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_ENUM', index=13, number=14, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED32', index=14, number=15, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED64', index=15, number=16, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT32', index=16, number=17, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT64', index=17, number=18, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) + + _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( + name='Label', + full_name='google.protobuf.FieldDescriptorProto.Label', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='LABEL_OPTIONAL', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REQUIRED', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REPEATED', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) + + _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( + name='OptimizeMode', + full_name='google.protobuf.FileOptions.OptimizeMode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPEED', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CODE_SIZE', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LITE_RUNTIME', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) + + _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( + name='CType', + full_name='google.protobuf.FieldOptions.CType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CORD', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STRING_PIECE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) + + _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( + name='JSType', + full_name='google.protobuf.FieldOptions.JSType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='JS_NORMAL', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_STRING', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_NUMBER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) + + _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( + name='IdempotencyLevel', + full_name='google.protobuf.MethodOptions.IdempotencyLevel', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDEMPOTENCY_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NO_SIDE_EFFECTS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IDEMPOTENT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) + + + _FILEDESCRIPTORSET = _descriptor.Descriptor( + name='FileDescriptorSet', + full_name='google.protobuf.FileDescriptorSet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEDESCRIPTORPROTO = _descriptor.Descriptor( + name='FileDescriptorProto', + full_name='google.protobuf.FileDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, + number=10, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, + number=11, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( + name='ExtensionRange', + full_name='google.protobuf.DescriptorProto.ExtensionRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( + name='ReservedRange', + full_name='google.protobuf.DescriptorProto.ReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO = _descriptor.Descriptor( + name='DescriptorProto', + full_name='google.protobuf.DescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.DescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field', full_name='google.protobuf.DescriptorProto.field', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.options', index=7, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( + name='ExtensionRangeOptions', + full_name='google.protobuf.ExtensionRangeOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( + name='FieldDescriptorProto', + full_name='google.protobuf.FieldDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, + number=17, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDDESCRIPTORPROTO_TYPE, + _FIELDDESCRIPTORPROTO_LABEL, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( + name='OneofDescriptorProto', + full_name='google.protobuf.OneofDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( + name='EnumReservedRange', + full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumDescriptorProto', + full_name='google.protobuf.EnumDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, + number=5, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumValueDescriptorProto', + full_name='google.protobuf.EnumValueDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( + name='ServiceDescriptorProto', + full_name='google.protobuf.ServiceDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _METHODDESCRIPTORPROTO = _descriptor.Descriptor( + name='MethodDescriptorProto', + full_name='google.protobuf.MethodDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEOPTIONS = _descriptor.Descriptor( + name='FileOptions', + full_name='google.protobuf.FileOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, + number=20, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, + number=27, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, + number=9, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, + number=16, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, + number=17, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, + number=18, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10, + number=42, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11, + number=23, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12, + number=31, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13, + number=36, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14, + number=37, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15, + number=39, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16, + number=40, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17, + number=41, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18, + number=44, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19, + number=45, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FILEOPTIONS_OPTIMIZEMODE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _MESSAGEOPTIONS = _descriptor.Descriptor( + name='MessageOptions', + full_name='google.protobuf.MessageOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDOPTIONS = _descriptor.Descriptor( + name='FieldOptions', + full_name='google.protobuf.FieldOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, + number=6, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, + number=15, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDOPTIONS_CTYPE, + _FIELDOPTIONS_JSTYPE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ONEOFOPTIONS = _descriptor.Descriptor( + name='OneofOptions', + full_name='google.protobuf.OneofOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMOPTIONS = _descriptor.Descriptor( + name='EnumOptions', + full_name='google.protobuf.EnumOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMVALUEOPTIONS = _descriptor.Descriptor( + name='EnumValueOptions', + full_name='google.protobuf.EnumValueOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _SERVICEOPTIONS = _descriptor.Descriptor( + name='ServiceOptions', + full_name='google.protobuf.ServiceOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _METHODOPTIONS = _descriptor.Descriptor( + name='MethodOptions', + full_name='google.protobuf.MethodOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, + number=34, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _METHODOPTIONS_IDEMPOTENCYLEVEL, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( + name='NamePart', + full_name='google.protobuf.UninterpretedOption.NamePart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, + number=2, type=8, cpp_type=7, label=2, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _UNINTERPRETEDOPTION = _descriptor.Descriptor( + name='UninterpretedOption', + full_name='google.protobuf.UninterpretedOption', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='google.protobuf.SourceCodeInfo.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _SOURCECODEINFO = _descriptor.Descriptor( + name='SourceCodeInfo', + full_name='google.protobuf.SourceCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SOURCECODEINFO_LOCATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( + name='Annotation', + full_name='google.protobuf.GeneratedCodeInfo.Annotation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _GENERATEDCODEINFO = _descriptor.Descriptor( + name='GeneratedCodeInfo', + full_name='google.protobuf.GeneratedCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS + _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO + _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS + _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE + _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS + _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE + _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL + _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE + _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS + _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO + _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO + _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS + _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE + _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS + _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO + _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS + _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS + _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE + _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS + _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE + _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE + _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS + _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL + _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS + _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION + _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART + _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO + _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION + _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO + _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION + DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET + DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS + DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS + DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS + DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS + DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS + DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS + DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS + DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION + DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO + DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO + _sym_db.RegisterFileDescriptor(DESCRIPTOR) + +else: + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _FILEDESCRIPTORSET._serialized_start=53 + _FILEDESCRIPTORSET._serialized_end=124 + _FILEDESCRIPTORPROTO._serialized_start=127 + _FILEDESCRIPTORPROTO._serialized_end=602 + _DESCRIPTORPROTO._serialized_start=605 + _DESCRIPTORPROTO._serialized_end=1286 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286 + _EXTENSIONRANGEOPTIONS._serialized_start=1288 + _EXTENSIONRANGEOPTIONS._serialized_end=1391 + _FIELDDESCRIPTORPROTO._serialized_start=1394 + _FIELDDESCRIPTORPROTO._serialized_end=2119 + _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740 + _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050 + _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052 + _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119 + _ONEOFDESCRIPTORPROTO._serialized_start=2121 + _ONEOFDESCRIPTORPROTO._serialized_end=2205 + _ENUMDESCRIPTORPROTO._serialized_start=2208 + _ENUMDESCRIPTORPROTO._serialized_end=2500 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500 + _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502 + _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610 + _SERVICEDESCRIPTORPROTO._serialized_start=2613 + _SERVICEDESCRIPTORPROTO._serialized_end=2757 + _METHODDESCRIPTORPROTO._serialized_start=2760 + _METHODDESCRIPTORPROTO._serialized_end=2953 + _FILEOPTIONS._serialized_start=2956 + _FILEOPTIONS._serialized_end=3761 + _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686 + _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744 + _MESSAGEOPTIONS._serialized_start=3764 + _MESSAGEOPTIONS._serialized_end=4024 + _FIELDOPTIONS._serialized_start=4027 + _FIELDOPTIONS._serialized_end=4473 + _FIELDOPTIONS_CTYPE._serialized_start=4354 + _FIELDOPTIONS_CTYPE._serialized_end=4401 + _FIELDOPTIONS_JSTYPE._serialized_start=4403 + _FIELDOPTIONS_JSTYPE._serialized_end=4456 + _ONEOFOPTIONS._serialized_start=4475 + _ONEOFOPTIONS._serialized_end=4569 + _ENUMOPTIONS._serialized_start=4572 + _ENUMOPTIONS._serialized_end=4719 + _ENUMVALUEOPTIONS._serialized_start=4721 + _ENUMVALUEOPTIONS._serialized_end=4846 + _SERVICEOPTIONS._serialized_start=4848 + _SERVICEOPTIONS._serialized_end=4971 + _METHODOPTIONS._serialized_start=4974 + _METHODOPTIONS._serialized_end=5275 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264 + _UNINTERPRETEDOPTION._serialized_start=5278 + _UNINTERPRETEDOPTION._serialized_end=5564 + _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513 + _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564 + _SOURCECODEINFO._serialized_start=5567 + _SOURCECODEINFO._serialized_end=5780 + _SOURCECODEINFO_LOCATION._serialized_start=5646 + _SOURCECODEINFO_LOCATION._serialized_end=5780 + _GENERATEDCODEINFO._serialized_start=5783 + _GENERATEDCODEINFO._serialized_end=5950 + _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871 + _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py new file mode 100644 index 0000000000..911372a8b0 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py @@ -0,0 +1,1295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides DescriptorPool to use as a container for proto2 descriptors. + +The DescriptorPool is used in conjection with a DescriptorDatabase to maintain +a collection of protocol buffer descriptors for use when dynamically creating +message types at runtime. + +For most applications protocol buffers should be used via modules generated by +the protocol buffer compiler tool. This should only be used when the type of +protocol buffers used in an application or library cannot be predetermined. + +Below is a straightforward example on how to use this class:: + + pool = DescriptorPool() + file_descriptor_protos = [ ... ] + for file_descriptor_proto in file_descriptor_protos: + pool.Add(file_descriptor_proto) + my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') + +The message descriptor can be used in conjunction with the message_factory +module in order to create a protocol buffer class that can be encoded and +decoded. + +If you want to get a Python class for the specified proto, use the +helper functions inside google.protobuf.message_factory +directly instead of this class. +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import collections +import warnings + +from google.protobuf import descriptor +from google.protobuf import descriptor_database +from google.protobuf import text_encoding + + +_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access + + +def _Deprecated(func): + """Mark functions as deprecated.""" + + def NewFunc(*args, **kwargs): + warnings.warn( + 'Call to deprecated function %s(). Note: Do add unlinked descriptors ' + 'to descriptor_pool is wrong. Use Add() or AddSerializedFile() ' + 'instead.' % func.__name__, + category=DeprecationWarning) + return func(*args, **kwargs) + NewFunc.__name__ = func.__name__ + NewFunc.__doc__ = func.__doc__ + NewFunc.__dict__.update(func.__dict__) + return NewFunc + + +def _NormalizeFullyQualifiedName(name): + """Remove leading period from fully-qualified type name. + + Due to b/13860351 in descriptor_database.py, types in the root namespace are + generated with a leading period. This function removes that prefix. + + Args: + name (str): The fully-qualified symbol name. + + Returns: + str: The normalized fully-qualified symbol name. + """ + return name.lstrip('.') + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) + + +class DescriptorPool(object): + """A collection of protobufs dynamically constructed by descriptor protos.""" + + if _USE_C_DESCRIPTORS: + + def __new__(cls, descriptor_db=None): + # pylint: disable=protected-access + return descriptor._message.DescriptorPool(descriptor_db) + + def __init__(self, descriptor_db=None): + """Initializes a Pool of proto buffs. + + The descriptor_db argument to the constructor is provided to allow + specialized file descriptor proto lookup code to be triggered on demand. An + example would be an implementation which will read and compile a file + specified in a call to FindFileByName() and not require the call to Add() + at all. Results from this database will be cached internally here as well. + + Args: + descriptor_db: A secondary source of file descriptors. + """ + + self._internal_db = descriptor_database.DescriptorDatabase() + self._descriptor_db = descriptor_db + self._descriptors = {} + self._enum_descriptors = {} + self._service_descriptors = {} + self._file_descriptors = {} + self._toplevel_extensions = {} + # TODO(jieluo): Remove _file_desc_by_toplevel_extension after + # maybe year 2020 for compatibility issue (with 3.4.1 only). + self._file_desc_by_toplevel_extension = {} + self._top_enum_values = {} + # We store extensions in two two-level mappings: The first key is the + # descriptor of the message being extended, the second key is the extension + # full name or its tag number. + self._extensions_by_name = collections.defaultdict(dict) + self._extensions_by_number = collections.defaultdict(dict) + + def _CheckConflictRegister(self, desc, desc_name, file_name): + """Check if the descriptor name conflicts with another of the same name. + + Args: + desc: Descriptor of a message, enum, service, extension or enum value. + desc_name (str): the full name of desc. + file_name (str): The file name of descriptor. + """ + for register, descriptor_type in [ + (self._descriptors, descriptor.Descriptor), + (self._enum_descriptors, descriptor.EnumDescriptor), + (self._service_descriptors, descriptor.ServiceDescriptor), + (self._toplevel_extensions, descriptor.FieldDescriptor), + (self._top_enum_values, descriptor.EnumValueDescriptor)]: + if desc_name in register: + old_desc = register[desc_name] + if isinstance(old_desc, descriptor.EnumValueDescriptor): + old_file = old_desc.type.file.name + else: + old_file = old_desc.file.name + + if not isinstance(desc, descriptor_type) or ( + old_file != file_name): + error_msg = ('Conflict register for file "' + file_name + + '": ' + desc_name + + ' is already defined in file "' + + old_file + '". Please fix the conflict by adding ' + 'package name on the proto file, or use different ' + 'name for the duplication.') + if isinstance(desc, descriptor.EnumValueDescriptor): + error_msg += ('\nNote: enum values appear as ' + 'siblings of the enum type instead of ' + 'children of it.') + + raise TypeError(error_msg) + + return + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + file_desc_proto (FileDescriptorProto): The file descriptor to add. + """ + + self._internal_db.Add(file_desc_proto) + + def AddSerializedFile(self, serialized_file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + serialized_file_desc_proto (bytes): A bytes string, serialization of the + :class:`FileDescriptorProto` to add. + + Returns: + FileDescriptor: Descriptor for the added file. + """ + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( + serialized_file_desc_proto) + file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) + file_desc.serialized_pb = serialized_file_desc_proto + return file_desc + + # Add Descriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddDescriptor(self, desc): + self._AddDescriptor(desc) + + # Never call this method. It is for internal usage only. + def _AddDescriptor(self, desc): + """Adds a Descriptor to the pool, non-recursively. + + If the Descriptor contains nested messages or enums, the caller must + explicitly register them. This method also registers the FileDescriptor + associated with the message. + + Args: + desc: A Descriptor. + """ + if not isinstance(desc, descriptor.Descriptor): + raise TypeError('Expected instance of descriptor.Descriptor.') + + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + + self._descriptors[desc.full_name] = desc + self._AddFileDescriptor(desc.file) + + # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddEnumDescriptor(self, enum_desc): + self._AddEnumDescriptor(enum_desc) + + # Never call this method. It is for internal usage only. + def _AddEnumDescriptor(self, enum_desc): + """Adds an EnumDescriptor to the pool. + + This method also registers the FileDescriptor associated with the enum. + + Args: + enum_desc: An EnumDescriptor. + """ + + if not isinstance(enum_desc, descriptor.EnumDescriptor): + raise TypeError('Expected instance of descriptor.EnumDescriptor.') + + file_name = enum_desc.file.name + self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) + self._enum_descriptors[enum_desc.full_name] = enum_desc + + # Top enum values need to be indexed. + # Count the number of dots to see whether the enum is toplevel or nested + # in a message. We cannot use enum_desc.containing_type at this stage. + if enum_desc.file.package: + top_level = (enum_desc.full_name.count('.') + - enum_desc.file.package.count('.') == 1) + else: + top_level = enum_desc.full_name.count('.') == 0 + if top_level: + file_name = enum_desc.file.name + package = enum_desc.file.package + for enum_value in enum_desc.values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, enum_value.name))) + self._CheckConflictRegister(enum_value, full_name, file_name) + self._top_enum_values[full_name] = enum_value + self._AddFileDescriptor(enum_desc.file) + + # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddServiceDescriptor(self, service_desc): + self._AddServiceDescriptor(service_desc) + + # Never call this method. It is for internal usage only. + def _AddServiceDescriptor(self, service_desc): + """Adds a ServiceDescriptor to the pool. + + Args: + service_desc: A ServiceDescriptor. + """ + + if not isinstance(service_desc, descriptor.ServiceDescriptor): + raise TypeError('Expected instance of descriptor.ServiceDescriptor.') + + self._CheckConflictRegister(service_desc, service_desc.full_name, + service_desc.file.name) + self._service_descriptors[service_desc.full_name] = service_desc + + # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddExtensionDescriptor(self, extension): + self._AddExtensionDescriptor(extension) + + # Never call this method. It is for internal usage only. + def _AddExtensionDescriptor(self, extension): + """Adds a FieldDescriptor describing an extension to the pool. + + Args: + extension: A FieldDescriptor. + + Raises: + AssertionError: when another extension with the same number extends the + same message. + TypeError: when the specified extension is not a + descriptor.FieldDescriptor. + """ + if not (isinstance(extension, descriptor.FieldDescriptor) and + extension.is_extension): + raise TypeError('Expected an extension descriptor.') + + if extension.extension_scope is None: + self._toplevel_extensions[extension.full_name] = extension + + try: + existing_desc = self._extensions_by_number[ + extension.containing_type][extension.number] + except KeyError: + pass + else: + if extension is not existing_desc: + raise AssertionError( + 'Extensions "%s" and "%s" both try to extend message type "%s" ' + 'with field number %d.' % + (extension.full_name, existing_desc.full_name, + extension.containing_type.full_name, extension.number)) + + self._extensions_by_number[extension.containing_type][ + extension.number] = extension + self._extensions_by_name[extension.containing_type][ + extension.full_name] = extension + + # Also register MessageSet extensions with the type name. + if _IsMessageSetExtension(extension): + self._extensions_by_name[extension.containing_type][ + extension.message_type.full_name] = extension + + @_Deprecated + def AddFileDescriptor(self, file_desc): + self._InternalAddFileDescriptor(file_desc) + + # Never call this method. It is for internal usage only. + def _InternalAddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + self._AddFileDescriptor(file_desc) + # TODO(jieluo): This is a temporary solution for FieldDescriptor.file. + # FieldDescriptor.file is added in code gen. Remove this solution after + # maybe 2020 for compatibility reason (with 3.4.1 only). + for extension in file_desc.extensions_by_name.values(): + self._file_desc_by_toplevel_extension[ + extension.full_name] = file_desc + + def _AddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + if not isinstance(file_desc, descriptor.FileDescriptor): + raise TypeError('Expected instance of descriptor.FileDescriptor.') + self._file_descriptors[file_desc.name] = file_desc + + def FindFileByName(self, file_name): + """Gets a FileDescriptor by file name. + + Args: + file_name (str): The path to the file to get a descriptor for. + + Returns: + FileDescriptor: The descriptor for the named file. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + try: + return self._file_descriptors[file_name] + except KeyError: + pass + + try: + file_proto = self._internal_db.FindFileByName(file_name) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileByName(file_name) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file named %s' % file_name) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def FindFileContainingSymbol(self, symbol): + """Gets the FileDescriptor for the file containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + symbol = _NormalizeFullyQualifiedName(symbol) + try: + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + pass + + try: + # Try fallback database. Build and find again if possible. + self._FindFileContainingSymbolInDb(symbol) + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + raise KeyError('Cannot find a file containing %s' % symbol) + + def _InternalFindFileContainingSymbol(self, symbol): + """Gets the already built FileDescriptor containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + try: + return self._descriptors[symbol].file + except KeyError: + pass + + try: + return self._enum_descriptors[symbol].file + except KeyError: + pass + + try: + return self._service_descriptors[symbol].file + except KeyError: + pass + + try: + return self._top_enum_values[symbol].type.file + except KeyError: + pass + + try: + return self._file_desc_by_toplevel_extension[symbol] + except KeyError: + pass + + # Try fields, enum values and nested extensions inside a message. + top_name, _, sub_name = symbol.rpartition('.') + try: + message = self.FindMessageTypeByName(top_name) + assert (sub_name in message.extensions_by_name or + sub_name in message.fields_by_name or + sub_name in message.enum_values_by_name) + return message.file + except (KeyError, AssertionError): + raise KeyError('Cannot find a file containing %s' % symbol) + + def FindMessageTypeByName(self, full_name): + """Loads the named descriptor from the pool. + + Args: + full_name (str): The full name of the descriptor to load. + + Returns: + Descriptor: The descriptor for the named type. + + Raises: + KeyError: if the message cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._descriptors[full_name] + + def FindEnumTypeByName(self, full_name): + """Loads the named enum descriptor from the pool. + + Args: + full_name (str): The full name of the enum descriptor to load. + + Returns: + EnumDescriptor: The enum descriptor for the named type. + + Raises: + KeyError: if the enum cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._enum_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._enum_descriptors[full_name] + + def FindFieldByName(self, full_name): + """Loads the named field descriptor from the pool. + + Args: + full_name (str): The full name of the field descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named field. + + Raises: + KeyError: if the field cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, field_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.fields_by_name[field_name] + + def FindOneofByName(self, full_name): + """Loads the named oneof descriptor from the pool. + + Args: + full_name (str): The full name of the oneof descriptor to load. + + Returns: + OneofDescriptor: The oneof descriptor for the named oneof. + + Raises: + KeyError: if the oneof cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, oneof_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.oneofs_by_name[oneof_name] + + def FindExtensionByName(self, full_name): + """Loads the named extension descriptor from the pool. + + Args: + full_name (str): The full name of the extension descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named extension. + + Raises: + KeyError: if the extension cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + try: + # The proto compiler does not give any link between the FileDescriptor + # and top-level extensions unless the FileDescriptorProto is added to + # the DescriptorDatabase, but this can impact memory usage. + # So we registered these extensions by name explicitly. + return self._toplevel_extensions[full_name] + except KeyError: + pass + message_name, _, extension_name = full_name.rpartition('.') + try: + # Most extensions are nested inside a message. + scope = self.FindMessageTypeByName(message_name) + except KeyError: + # Some extensions are defined at file scope. + scope = self._FindFileContainingSymbolInDb(full_name) + return scope.extensions_by_name[extension_name] + + def FindExtensionByNumber(self, message_descriptor, number): + """Gets the extension of the specified message with the specified number. + + Extensions have to be registered to this pool by calling :func:`Add` or + :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): descriptor of the extended message. + number (int): Number of the extension field. + + Returns: + FieldDescriptor: The descriptor for the extension. + + Raises: + KeyError: when no extension with the given number is known for the + specified message. + """ + try: + return self._extensions_by_number[message_descriptor][number] + except KeyError: + self._TryLoadExtensionFromDB(message_descriptor, number) + return self._extensions_by_number[message_descriptor][number] + + def FindAllExtensions(self, message_descriptor): + """Gets all the known extensions of a given message. + + Extensions have to be registered to this pool by build related + :func:`Add` or :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): Descriptor of the extended message. + + Returns: + list[FieldDescriptor]: Field descriptors describing the extensions. + """ + # Fallback to descriptor db if FindAllExtensionNumbers is provided. + if self._descriptor_db and hasattr( + self._descriptor_db, 'FindAllExtensionNumbers'): + full_name = message_descriptor.full_name + all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) + for number in all_numbers: + if number in self._extensions_by_number[message_descriptor]: + continue + self._TryLoadExtensionFromDB(message_descriptor, number) + + return list(self._extensions_by_number[message_descriptor].values()) + + def _TryLoadExtensionFromDB(self, message_descriptor, number): + """Try to Load extensions from descriptor db. + + Args: + message_descriptor: descriptor of the extended message. + number: the extension number that needs to be loaded. + """ + if not self._descriptor_db: + return + # Only supported when FindFileContainingExtension is provided. + if not hasattr( + self._descriptor_db, 'FindFileContainingExtension'): + return + + full_name = message_descriptor.full_name + file_proto = self._descriptor_db.FindFileContainingExtension( + full_name, number) + + if file_proto is None: + return + + try: + self._ConvertFileProtoToFileDescriptor(file_proto) + except: + warn_msg = ('Unable to load proto file %s for extension number %d.' % + (file_proto.name, number)) + warnings.warn(warn_msg, RuntimeWarning) + + def FindServiceByName(self, full_name): + """Loads the named service descriptor from the pool. + + Args: + full_name (str): The full name of the service descriptor to load. + + Returns: + ServiceDescriptor: The service descriptor for the named service. + + Raises: + KeyError: if the service cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._service_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._service_descriptors[full_name] + + def FindMethodByName(self, full_name): + """Loads the named service method descriptor from the pool. + + Args: + full_name (str): The full name of the method descriptor to load. + + Returns: + MethodDescriptor: The method descriptor for the service method. + + Raises: + KeyError: if the method cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + service_name, _, method_name = full_name.rpartition('.') + service_descriptor = self.FindServiceByName(service_name) + return service_descriptor.methods_by_name[method_name] + + def _FindFileContainingSymbolInDb(self, symbol): + """Finds the file in descriptor DB containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: The file that contains the specified symbol. + + Raises: + KeyError: if the file cannot be found in the descriptor database. + """ + try: + file_proto = self._internal_db.FindFileContainingSymbol(symbol) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file containing %s' % symbol) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def _ConvertFileProtoToFileDescriptor(self, file_proto): + """Creates a FileDescriptor from a proto or returns a cached copy. + + This method also has the side effect of loading all the symbols found in + the file into the appropriate dictionaries in the pool. + + Args: + file_proto: The proto to convert. + + Returns: + A FileDescriptor matching the passed in proto. + """ + if file_proto.name not in self._file_descriptors: + built_deps = list(self._GetDeps(file_proto.dependency)) + direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] + public_deps = [direct_deps[i] for i in file_proto.public_dependency] + + file_descriptor = descriptor.FileDescriptor( + pool=self, + name=file_proto.name, + package=file_proto.package, + syntax=file_proto.syntax, + options=_OptionsOrNone(file_proto), + serialized_pb=file_proto.SerializeToString(), + dependencies=direct_deps, + public_dependencies=public_deps, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope = {} + + # This loop extracts all the message and enum types from all the + # dependencies of the file_proto. This is necessary to create the + # scope of available message types when defining the passed in + # file proto. + for dependency in built_deps: + scope.update(self._ExtractSymbols( + dependency.message_types_by_name.values())) + scope.update((_PrefixWithDot(enum.full_name), enum) + for enum in dependency.enum_types_by_name.values()) + + for message_type in file_proto.message_type: + message_desc = self._ConvertMessageDescriptor( + message_type, file_proto.package, file_descriptor, scope, + file_proto.syntax) + file_descriptor.message_types_by_name[message_desc.name] = ( + message_desc) + + for enum_type in file_proto.enum_type: + file_descriptor.enum_types_by_name[enum_type.name] = ( + self._ConvertEnumDescriptor(enum_type, file_proto.package, + file_descriptor, None, scope, True)) + + for index, extension_proto in enumerate(file_proto.extension): + extension_desc = self._MakeFieldDescriptor( + extension_proto, file_proto.package, index, file_descriptor, + is_extension=True) + extension_desc.containing_type = self._GetTypeFromScope( + file_descriptor.package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, + file_descriptor.package, scope) + file_descriptor.extensions_by_name[extension_desc.name] = ( + extension_desc) + self._file_desc_by_toplevel_extension[extension_desc.full_name] = ( + file_descriptor) + + for desc_proto in file_proto.message_type: + self._SetAllFieldTypes(file_proto.package, desc_proto, scope) + + if file_proto.package: + desc_proto_prefix = _PrefixWithDot(file_proto.package) + else: + desc_proto_prefix = '' + + for desc_proto in file_proto.message_type: + desc = self._GetTypeFromScope( + desc_proto_prefix, desc_proto.name, scope) + file_descriptor.message_types_by_name[desc_proto.name] = desc + + for index, service_proto in enumerate(file_proto.service): + file_descriptor.services_by_name[service_proto.name] = ( + self._MakeServiceDescriptor(service_proto, index, scope, + file_proto.package, file_descriptor)) + + self._file_descriptors[file_proto.name] = file_descriptor + + # Add extensions to the pool + file_desc = self._file_descriptors[file_proto.name] + for extension in file_desc.extensions_by_name.values(): + self._AddExtensionDescriptor(extension) + for message_type in file_desc.message_types_by_name.values(): + for extension in message_type.extensions: + self._AddExtensionDescriptor(extension) + + return file_desc + + def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, + scope=None, syntax=None): + """Adds the proto to the pool in the specified package. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: The package the proto should be located in. + file_desc: The file containing this message. + scope: Dict mapping short and full symbols to message and enum types. + syntax: string indicating syntax of the file ("proto2" or "proto3") + + Returns: + The added descriptor. + """ + + if package: + desc_name = '.'.join((package, desc_proto.name)) + else: + desc_name = desc_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + if scope is None: + scope = {} + + nested = [ + self._ConvertMessageDescriptor( + nested, desc_name, file_desc, scope, syntax) + for nested in desc_proto.nested_type] + enums = [ + self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, + scope, False) + for enum in desc_proto.enum_type] + fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) + for index, field in enumerate(desc_proto.field)] + extensions = [ + self._MakeFieldDescriptor(extension, desc_name, index, file_desc, + is_extension=True) + for index, extension in enumerate(desc_proto.extension)] + oneofs = [ + # pylint: disable=g-complex-comprehension + descriptor.OneofDescriptor( + desc.name, + '.'.join((desc_name, desc.name)), + index, + None, + [], + _OptionsOrNone(desc), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for index, desc in enumerate(desc_proto.oneof_decl) + ] + extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] + if extension_ranges: + is_extendable = True + else: + is_extendable = False + desc = descriptor.Descriptor( + name=desc_proto.name, + full_name=desc_name, + filename=file_name, + containing_type=None, + fields=fields, + oneofs=oneofs, + nested_types=nested, + enum_types=enums, + extensions=extensions, + options=_OptionsOrNone(desc_proto), + is_extendable=is_extendable, + extension_ranges=extension_ranges, + file=file_desc, + serialized_start=None, + serialized_end=None, + syntax=syntax, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for nested in desc.nested_types: + nested.containing_type = desc + for enum in desc.enum_types: + enum.containing_type = desc + for field_index, field_desc in enumerate(desc_proto.field): + if field_desc.HasField('oneof_index'): + oneof_index = field_desc.oneof_index + oneofs[oneof_index].fields.append(fields[field_index]) + fields[field_index].containing_oneof = oneofs[oneof_index] + + scope[_PrefixWithDot(desc_name)] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._descriptors[desc_name] = desc + return desc + + def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, + containing_type=None, scope=None, top_level=False): + """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. + + Args: + enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the enum descriptor. + containing_type: The type containing this enum. + scope: Scope containing available types. + top_level: If True, the enum is a top level symbol. If False, the enum + is defined inside a message. + + Returns: + The added descriptor + """ + + if package: + enum_name = '.'.join((package, enum_proto.name)) + else: + enum_name = enum_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + values = [self._MakeEnumValueDescriptor(value, index) + for index, value in enumerate(enum_proto.value)] + desc = descriptor.EnumDescriptor(name=enum_proto.name, + full_name=enum_name, + filename=file_name, + file=file_desc, + values=values, + containing_type=containing_type, + options=_OptionsOrNone(enum_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope['.%s' % enum_name] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._enum_descriptors[enum_name] = desc + + # Add top level enum values. + if top_level: + for value in values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, value.name))) + self._CheckConflictRegister(value, full_name, file_name) + self._top_enum_values[full_name] = value + + return desc + + def _MakeFieldDescriptor(self, field_proto, message_name, index, + file_desc, is_extension=False): + """Creates a field descriptor from a FieldDescriptorProto. + + For message and enum type fields, this method will do a look up + in the pool for the appropriate descriptor for that type. If it + is unavailable, it will fall back to the _source function to + create it. If this type is still unavailable, construction will + fail. + + Args: + field_proto: The proto describing the field. + message_name: The name of the containing message. + index: Index of the field + file_desc: The file containing the field descriptor. + is_extension: Indication that this field is for an extension. + + Returns: + An initialized FieldDescriptor object + """ + + if message_name: + full_name = '.'.join((message_name, field_proto.name)) + else: + full_name = field_proto.name + + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + + return descriptor.FieldDescriptor( + name=field_proto.name, + full_name=full_name, + index=index, + number=field_proto.number, + type=field_proto.type, + cpp_type=None, + message_type=None, + enum_type=None, + containing_type=None, + label=field_proto.label, + has_default_value=False, + default_value=None, + is_extension=is_extension, + extension_scope=None, + options=_OptionsOrNone(field_proto), + json_name=json_name, + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _SetAllFieldTypes(self, package, desc_proto, scope): + """Sets all the descriptor's fields's types. + + This method also sets the containing types on any extensions. + + Args: + package: The current package of desc_proto. + desc_proto: The message descriptor to update. + scope: Enclosing scope of available types. + """ + + package = _PrefixWithDot(package) + + main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) + + if package == '.': + nested_package = _PrefixWithDot(desc_proto.name) + else: + nested_package = '.'.join([package, desc_proto.name]) + + for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): + self._SetFieldType(field_proto, field_desc, nested_package, scope) + + for extension_proto, extension_desc in ( + zip(desc_proto.extension, main_desc.extensions)): + extension_desc.containing_type = self._GetTypeFromScope( + nested_package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, nested_package, scope) + + for nested_type in desc_proto.nested_type: + self._SetAllFieldTypes(nested_package, nested_type, scope) + + def _SetFieldType(self, field_proto, field_desc, package, scope): + """Sets the field's type, cpp_type, message_type and enum_type. + + Args: + field_proto: Data about the field in proto format. + field_desc: The descriptor to modify. + package: The package the field's container is in. + scope: Enclosing scope of available types. + """ + if field_proto.type_name: + desc = self._GetTypeFromScope(package, field_proto.type_name, scope) + else: + desc = None + + if not field_proto.HasField('type'): + if isinstance(desc, descriptor.Descriptor): + field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE + else: + field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM + + field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( + field_proto.type) + + if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE + or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): + field_desc.message_type = desc + + if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.enum_type = desc + + if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: + field_desc.has_default_value = False + field_desc.default_value = [] + elif field_proto.HasField('default_value'): + field_desc.has_default_value = True + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = float(field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = field_proto.default_value + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = field_proto.default_value.lower() == 'true' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values_by_name[ + field_proto.default_value].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = text_encoding.CUnescape( + field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = int(field_proto.default_value) + else: + field_desc.has_default_value = False + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = 0.0 + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = u'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = False + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values[0].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = b'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = 0 + + field_desc.type = field_proto.type + + def _MakeEnumValueDescriptor(self, value_proto, index): + """Creates a enum value descriptor object from a enum value proto. + + Args: + value_proto: The proto describing the enum value. + index: The index of the enum value. + + Returns: + An initialized EnumValueDescriptor object. + """ + + return descriptor.EnumValueDescriptor( + name=value_proto.name, + index=index, + number=value_proto.number, + options=_OptionsOrNone(value_proto), + type=None, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _MakeServiceDescriptor(self, service_proto, service_index, scope, + package, file_desc): + """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. + + Args: + service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. + service_index: The index of the service in the File. + scope: Dict mapping short and full symbols to message and enum types. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the service descriptor. + + Returns: + The added descriptor. + """ + + if package: + service_name = '.'.join((package, service_proto.name)) + else: + service_name = service_proto.name + + methods = [self._MakeMethodDescriptor(method_proto, service_name, package, + scope, index) + for index, method_proto in enumerate(service_proto.method)] + desc = descriptor.ServiceDescriptor( + name=service_proto.name, + full_name=service_name, + index=service_index, + methods=methods, + options=_OptionsOrNone(service_proto), + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._service_descriptors[service_name] = desc + return desc + + def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, + index): + """Creates a method descriptor from a MethodDescriptorProto. + + Args: + method_proto: The proto describing the method. + service_name: The name of the containing service. + package: Optional package name to look up for types. + scope: Scope containing available types. + index: Index of the method in the service. + + Returns: + An initialized MethodDescriptor object. + """ + full_name = '.'.join((service_name, method_proto.name)) + input_type = self._GetTypeFromScope( + package, method_proto.input_type, scope) + output_type = self._GetTypeFromScope( + package, method_proto.output_type, scope) + return descriptor.MethodDescriptor( + name=method_proto.name, + full_name=full_name, + index=index, + containing_service=None, + input_type=input_type, + output_type=output_type, + client_streaming=method_proto.client_streaming, + server_streaming=method_proto.server_streaming, + options=_OptionsOrNone(method_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _ExtractSymbols(self, descriptors): + """Pulls out all the symbols from descriptor protos. + + Args: + descriptors: The messages to extract descriptors from. + Yields: + A two element tuple of the type name and descriptor object. + """ + + for desc in descriptors: + yield (_PrefixWithDot(desc.full_name), desc) + for symbol in self._ExtractSymbols(desc.nested_types): + yield symbol + for enum in desc.enum_types: + yield (_PrefixWithDot(enum.full_name), enum) + + def _GetDeps(self, dependencies, visited=None): + """Recursively finds dependencies for file protos. + + Args: + dependencies: The names of the files being depended on. + visited: The names of files already found. + + Yields: + Each direct and indirect dependency. + """ + + visited = visited or set() + for dependency in dependencies: + if dependency not in visited: + visited.add(dependency) + dep_desc = self.FindFileByName(dependency) + yield dep_desc + public_files = [d.name for d in dep_desc.public_dependencies] + yield from self._GetDeps(public_files, visited) + + def _GetTypeFromScope(self, package, type_name, scope): + """Finds a given type name in the current scope. + + Args: + package: The package the proto should be located in. + type_name: The name of the type to be found in the scope. + scope: Dict mapping short and full symbols to message and enum types. + + Returns: + The descriptor for the requested type. + """ + if type_name not in scope: + components = _PrefixWithDot(package).split('.') + while components: + possible_match = '.'.join(components + [type_name]) + if possible_match in scope: + type_name = possible_match + break + else: + components.pop(-1) + return scope[type_name] + + +def _PrefixWithDot(name): + return name if name.startswith('.') else '.%s' % name + + +if _USE_C_DESCRIPTORS: + # TODO(amauryfa): This pool could be constructed from Python code, when we + # support a flag like 'use_cpp_generated_pool=True'. + # pylint: disable=protected-access + _DEFAULT = descriptor._message.default_pool +else: + _DEFAULT = DescriptorPool() + + +def Default(): + return _DEFAULT diff --git a/openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py new file mode 100644 index 0000000000..a8ecc07bdf --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/duration.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\"*\n\x08\x44uration\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DURATION._serialized_start=51 + _DURATION._serialized_end=93 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py new file mode 100644 index 0000000000..0b4d554db3 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/empty.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _EMPTY._serialized_start=48 + _EMPTY._serialized_end=55 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py new file mode 100644 index 0000000000..80a4e96e59 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/field_mask.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"\x1a\n\tFieldMask\x12\r\n\x05paths\x18\x01 \x03(\tB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _FIELDMASK._serialized_start=53 + _FIELDMASK._serialized_end=79 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/__init__.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py new file mode 100644 index 0000000000..afdbb78c36 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py @@ -0,0 +1,443 @@ +#! /usr/bin/env python +# +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Adds support for parameterized tests to Python's unittest TestCase class. + +A parameterized test is a method in a test case that is invoked with different +argument tuples. + +A simple example: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + (1, 2, 3), + (4, 5, 9), + (1, 1, 3)) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Each invocation is a separate test case and properly isolated just +like a normal test method, with its own setUp/tearDown cycle. In the +example above, there are three separate testcases, one of which will +fail due to an assertion error (1 + 1 != 3). + +Parameters for individual test cases can be tuples (with positional parameters) +or dictionaries (with named parameters): + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + {'op1': 1, 'op2': 2, 'result': 3}, + {'op1': 4, 'op2': 5, 'result': 9}, + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +If a parameterized test fails, the error message will show the +original test name (which is modified internally) and the arguments +for the specific invocation, which are part of the string returned by +the shortDescription() method on test cases. + +The id method of the test, used internally by the unittest framework, +is also modified to show the arguments. To make sure that test names +stay the same across several invocations, object representations like + + >>> class Foo(object): + ... pass + >>> repr(Foo()) + '<__main__.Foo object at 0x23d8610>' + +are turned into '<__main__.Foo>'. For even more descriptive names, +especially in test logs, you can use the named_parameters decorator. In +this case, only tuples are supported, and the first parameters has to +be a string (or an object that returns an apt name when converted via +str()): + + class NamedExample(parameterized.TestCase): + @parameterized.named_parameters( + ('Normal', 'aa', 'aaa', True), + ('EmptyPrefix', '', 'abc', True), + ('BothEmpty', '', '', True)) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, strings.startswith(prefix)) + +Named tests also have the benefit that they can be run individually +from the command line: + + $ testmodule.py NamedExample.testStartsWithNormal + . + -------------------------------------------------------------------- + Ran 1 test in 0.000s + + OK + +Parameterized Classes +===================== +If invocation arguments are shared across test methods in a single +TestCase class, instead of decorating all test methods +individually, the class itself can be decorated: + + @parameterized.parameters( + (1, 2, 3) + (4, 5, 9)) + class ArithmeticTest(parameterized.TestCase): + def testAdd(self, arg1, arg2, result): + self.assertEqual(arg1 + arg2, result) + + def testSubtract(self, arg2, arg2, result): + self.assertEqual(result - arg1, arg2) + +Inputs from Iterables +===================== +If parameters should be shared across several test cases, or are dynamically +created from other sources, a single non-tuple iterable can be passed into +the decorator. This iterable will be used to obtain the test cases: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + c.op1, c.op2, c.result for c in testcases + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Single-Argument Test Methods +============================ +If a test method takes only one argument, the single argument does not need to +be wrapped into a tuple: + + class NegativeNumberExample(parameterized.TestCase): + @parameterized.parameters( + -1, -3, -4, -5 + ) + def testIsNegative(self, arg): + self.assertTrue(IsNegative(arg)) +""" + +__author__ = 'tmarek@google.com (Torsten Marek)' + +import functools +import re +import types +import unittest +import uuid + +try: + # Since python 3 + import collections.abc as collections_abc +except ImportError: + # Won't work after python 3.8 + import collections as collections_abc + +ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') +_SEPARATOR = uuid.uuid1().hex +_FIRST_ARG = object() +_ARGUMENT_REPR = object() + + +def _CleanRepr(obj): + return ADDR_RE.sub(r'<\1>', repr(obj)) + + +# Helper function formerly from the unittest module, removed from it in +# Python 2.7. +def _StrClass(cls): + return '%s.%s' % (cls.__module__, cls.__name__) + + +def _NonStringIterable(obj): + return (isinstance(obj, collections_abc.Iterable) and + not isinstance(obj, str)) + + +def _FormatParameterList(testcase_params): + if isinstance(testcase_params, collections_abc.Mapping): + return ', '.join('%s=%s' % (argname, _CleanRepr(value)) + for argname, value in testcase_params.items()) + elif _NonStringIterable(testcase_params): + return ', '.join(map(_CleanRepr, testcase_params)) + else: + return _FormatParameterList((testcase_params,)) + + +class _ParameterizedTestIter(object): + """Callable and iterable class for producing new test cases.""" + + def __init__(self, test_method, testcases, naming_type): + """Returns concrete test functions for a test and a list of parameters. + + The naming_type is used to determine the name of the concrete + functions as reported by the unittest framework. If naming_type is + _FIRST_ARG, the testcases must be tuples, and the first element must + have a string representation that is a valid Python identifier. + + Args: + test_method: The decorated test method. + testcases: (list of tuple/dict) A list of parameter + tuples/dicts for individual test invocations. + naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. + """ + self._test_method = test_method + self.testcases = testcases + self._naming_type = naming_type + + def __call__(self, *args, **kwargs): + raise RuntimeError('You appear to be running a parameterized test case ' + 'without having inherited from parameterized.' + 'TestCase. This is bad because none of ' + 'your test cases are actually being run.') + + def __iter__(self): + test_method = self._test_method + naming_type = self._naming_type + + def MakeBoundParamTest(testcase_params): + @functools.wraps(test_method) + def BoundParamTest(self): + if isinstance(testcase_params, collections_abc.Mapping): + test_method(self, **testcase_params) + elif _NonStringIterable(testcase_params): + test_method(self, *testcase_params) + else: + test_method(self, testcase_params) + + if naming_type is _FIRST_ARG: + # Signal the metaclass that the name of the test function is unique + # and descriptive. + BoundParamTest.__x_use_name__ = True + BoundParamTest.__name__ += str(testcase_params[0]) + testcase_params = testcase_params[1:] + elif naming_type is _ARGUMENT_REPR: + # __x_extra_id__ is used to pass naming information to the __new__ + # method of TestGeneratorMetaclass. + # The metaclass will make sure to create a unique, but nondescriptive + # name for this test. + BoundParamTest.__x_extra_id__ = '(%s)' % ( + _FormatParameterList(testcase_params),) + else: + raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) + + BoundParamTest.__doc__ = '%s(%s)' % ( + BoundParamTest.__name__, _FormatParameterList(testcase_params)) + if test_method.__doc__: + BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) + return BoundParamTest + return (MakeBoundParamTest(c) for c in self.testcases) + + +def _IsSingletonList(testcases): + """True iff testcases contains only a single non-tuple element.""" + return len(testcases) == 1 and not isinstance(testcases[0], tuple) + + +def _ModifyClass(class_object, testcases, naming_type): + assert not getattr(class_object, '_id_suffix', None), ( + 'Cannot add parameters to %s,' + ' which already has parameterized methods.' % (class_object,)) + class_object._id_suffix = id_suffix = {} + # We change the size of __dict__ while we iterate over it, + # which Python 3.x will complain about, so use copy(). + for name, obj in class_object.__dict__.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) + and isinstance(obj, types.FunctionType)): + delattr(class_object, name) + methods = {} + _UpdateClassDictForParamTestCase( + methods, id_suffix, name, + _ParameterizedTestIter(obj, testcases, naming_type)) + for name, meth in methods.items(): + setattr(class_object, name, meth) + + +def _ParameterDecorator(naming_type, testcases): + """Implementation of the parameterization decorators. + + Args: + naming_type: The naming type. + testcases: Testcase parameters. + + Returns: + A function for modifying the decorated object. + """ + def _Apply(obj): + if isinstance(obj, type): + _ModifyClass( + obj, + list(testcases) if not isinstance(testcases, collections_abc.Sequence) + else testcases, + naming_type) + return obj + else: + return _ParameterizedTestIter(obj, testcases, naming_type) + + if _IsSingletonList(testcases): + assert _NonStringIterable(testcases[0]), ( + 'Single parameter argument must be a non-string iterable') + testcases = testcases[0] + + return _Apply + + +def parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples/dicts/objects (for tests + with only one argument). + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_ARGUMENT_REPR, testcases) + + +def named_parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. The first element of + each parameter tuple should be a string and will be appended to the + name of the test method. + + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_FIRST_ARG, testcases) + + +class TestGeneratorMetaclass(type): + """Metaclass for test cases with test generators. + + A test generator is an iterable in a testcase that produces callables. These + callables must be single-argument methods. These methods are injected into + the class namespace and the original iterable is removed. If the name of the + iterable conforms to the test pattern, the injected methods will be picked + up as tests by the unittest framework. + + In general, it is supposed to be used in conjunction with the + parameters decorator. + """ + + def __new__(mcs, class_name, bases, dct): + dct['_id_suffix'] = id_suffix = {} + for name, obj in dct.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) and + _NonStringIterable(obj)): + iterator = iter(obj) + dct.pop(name) + _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) + + return type.__new__(mcs, class_name, bases, dct) + + +def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): + """Adds individual test cases to a dictionary. + + Args: + dct: The target dictionary. + id_suffix: The dictionary for mapping names to test IDs. + name: The original name of the test case. + iterator: The iterator generating the individual test cases. + """ + for idx, func in enumerate(iterator): + assert callable(func), 'Test generators must yield callables, got %r' % ( + func,) + if getattr(func, '__x_use_name__', False): + new_name = func.__name__ + else: + new_name = '%s%s%d' % (name, _SEPARATOR, idx) + assert new_name not in dct, ( + 'Name of parameterized test case "%s" not unique' % (new_name,)) + dct[new_name] = func + id_suffix[new_name] = getattr(func, '__x_extra_id__', '') + + +class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): + """Base class for test cases using the parameters decorator.""" + + def _OriginalName(self): + return self._testMethodName.split(_SEPARATOR)[0] + + def __str__(self): + return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) + + def id(self): # pylint: disable=invalid-name + """Returns the descriptive ID of the test. + + This is used internally by the unittesting framework to get a name + for the test to be used in reports. + + Returns: + The test id. + """ + return '%s.%s%s' % (_StrClass(self.__class__), + self._OriginalName(), + self._id_suffix.get(self._testMethodName, '')) + + +def CoopTestCase(other_base_class): + """Returns a new base class with a cooperative metaclass base. + + This enables the TestCase to be used in combination + with other base classes that have custom metaclasses, such as + mox.MoxTestBase. + + Only works with metaclasses that do not override type.__new__. + + Example: + + import google3 + import mox + + from google3.testing.pybase import parameterized + + class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): + ... + + Args: + other_base_class: (class) A test case base class. + + Returns: + A new class object. + """ + metaclass = type( + 'CoopMetaclass', + (other_base_class.__metaclass__, + TestGeneratorMetaclass), {}) + return metaclass( + 'CoopTestCase', + (other_base_class, TestCase), {}) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py new file mode 100644 index 0000000000..7fef237670 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py @@ -0,0 +1,112 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Determine which implementation of the protobuf API is used in this process. +""" + +import os +import sys +import warnings + +try: + # pylint: disable=g-import-not-at-top + from google.protobuf.internal import _api_implementation + # The compile-time constants in the _api_implementation module can be used to + # switch to a certain implementation of the Python API at build time. + _api_version = _api_implementation.api_version +except ImportError: + _api_version = -1 # Unspecified by compiler flags. + +if _api_version == 1: + raise ValueError('api_version=1 is no longer supported.') + + +_default_implementation_type = ('cpp' if _api_version > 0 else 'python') + + +# This environment variable can be used to switch to a certain implementation +# of the Python API, overriding the compile-time constants in the +# _api_implementation module. Right now only 'python' and 'cpp' are valid +# values. Any other value will be ignored. +_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', + _default_implementation_type) + +if _implementation_type != 'python': + _implementation_type = 'cpp' + +if 'PyPy' in sys.version and _implementation_type == 'cpp': + warnings.warn('PyPy does not work yet with cpp protocol buffers. ' + 'Falling back to the python implementation.') + _implementation_type = 'python' + + +# Detect if serialization should be deterministic by default +try: + # The presence of this module in a build allows the proto implementation to + # be upgraded merely via build deps. + # + # NOTE: Merely importing this automatically enables deterministic proto + # serialization for C++ code, but we still need to export it as a boolean so + # that we can do the same for `_implementation_type == 'python'`. + # + # NOTE2: It is possible for C++ code to enable deterministic serialization by + # default _without_ affecting Python code, if the C++ implementation is not in + # use by this module. That is intended behavior, so we don't actually expose + # this boolean outside of this module. + # + # pylint: disable=g-import-not-at-top,unused-import + from google.protobuf import enable_deterministic_proto_serialization + _python_deterministic_proto_serialization = True +except ImportError: + _python_deterministic_proto_serialization = False + + +# Usage of this function is discouraged. Clients shouldn't care which +# implementation of the API is in use. Note that there is no guarantee +# that differences between APIs will be maintained. +# Please don't use this function if possible. +def Type(): + return _implementation_type + + +def _SetType(implementation_type): + """Never use! Only for protobuf benchmark.""" + global _implementation_type + _implementation_type = implementation_type + + +# See comment on 'Type' above. +def Version(): + return 2 + + +# For internal use only +def IsPythonDefaultSerializationDeterministic(): + return _python_deterministic_proto_serialization diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py new file mode 100644 index 0000000000..64353ee4af --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py @@ -0,0 +1,130 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Builds descriptors, message classes and services for generated _pb2.py. + +This file is only called in python generated _pb2.py files. It builds +descriptors, message classes and services that users can directly use +in generated code. +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +_sym_db = _symbol_database.Default() + + +def BuildMessageAndEnumDescriptors(file_des, module): + """Builds message and enum descriptors. + + Args: + file_des: FileDescriptor of the .proto file + module: Generated _pb2 module + """ + + def BuildNestedDescriptors(msg_des, prefix): + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + module_name = prefix + name.upper() + module[module_name] = nested_msg + BuildNestedDescriptors(nested_msg, module_name + '_') + for enum_des in msg_des.enum_types: + module[prefix + enum_des.name.upper()] = enum_des + + for (name, msg_des) in file_des.message_types_by_name.items(): + module_name = '_' + name.upper() + module[module_name] = msg_des + BuildNestedDescriptors(msg_des, module_name + '_') + + +def BuildTopDescriptorsAndMessages(file_des, module_name, module): + """Builds top level descriptors and message classes. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + + def BuildMessage(msg_des): + create_dict = {} + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + create_dict[name] = BuildMessage(nested_msg) + create_dict['DESCRIPTOR'] = msg_des + create_dict['__module__'] = module_name + message_class = _reflection.GeneratedProtocolMessageType( + msg_des.name, (_message.Message,), create_dict) + _sym_db.RegisterMessage(message_class) + return message_class + + # top level enums + for (name, enum_des) in file_des.enum_types_by_name.items(): + module['_' + name.upper()] = enum_des + module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) + for enum_value in enum_des.values: + module[enum_value.name] = enum_value.number + + # top level extensions + for (name, extension_des) in file_des.extensions_by_name.items(): + module[name.upper() + '_FIELD_NUMBER'] = extension_des.number + module[name] = extension_des + + # services + for (name, service) in file_des.services_by_name.items(): + module['_' + name.upper()] = service + + # Build messages. + for (name, msg_des) in file_des.message_types_by_name.items(): + module[name] = BuildMessage(msg_des) + + +def BuildServices(file_des, module_name, module): + """Builds services classes and services stub class. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import service as _service + from google.protobuf import service_reflection + # pylint: enable=g-import-not-at-top + for (name, service) in file_des.services_by_name.items(): + module[name] = service_reflection.GeneratedServiceType( + name, (_service.Service,), + dict(DESCRIPTOR=service, __module__=module_name)) + stub_name = name + '_Stub' + module[stub_name] = service_reflection.GeneratedServiceStubType( + stub_name, (module[name],), + dict(DESCRIPTOR=service, __module__=module_name)) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py new file mode 100644 index 0000000000..29fbb53d2f --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py @@ -0,0 +1,710 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains container classes to represent different protocol buffer types. + +This file defines container classes which represent categories of protocol +buffer field types which need extra maintenance. Currently these categories +are: + +- Repeated scalar fields - These are all repeated fields which aren't + composite (e.g. they are of simple types like int32, string, etc). +- Repeated composite fields - Repeated fields which are composite. This + includes groups and nested messages. +""" + +import collections.abc +import copy +import pickle +from typing import ( + Any, + Iterable, + Iterator, + List, + MutableMapping, + MutableSequence, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + overload, +) + + +_T = TypeVar('_T') +_K = TypeVar('_K') +_V = TypeVar('_V') + + +class BaseContainer(Sequence[_T]): + """Base container class.""" + + # Minimizes memory usage and disallows assignment to other attributes. + __slots__ = ['_message_listener', '_values'] + + def __init__(self, message_listener: Any) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The RepeatedScalarFieldContainer will call this object's + Modified() method when it is modified. + """ + self._message_listener = message_listener + self._values = [] + + @overload + def __getitem__(self, key: int) -> _T: + ... + + @overload + def __getitem__(self, key: slice) -> List[_T]: + ... + + def __getitem__(self, key): + """Retrieves item by the specified key.""" + return self._values[key] + + def __len__(self) -> int: + """Returns the number of elements in the container.""" + return len(self._values) + + def __ne__(self, other: Any) -> bool: + """Checks if another instance isn't equal to this one.""" + # The concrete classes should define __eq__. + return not self == other + + __hash__ = None + + def __repr__(self) -> str: + return repr(self._values) + + def sort(self, *args, **kwargs) -> None: + # Continue to support the old sort_function keyword argument. + # This is expected to be a rare occurrence, so use LBYL to avoid + # the overhead of actually catching KeyError. + if 'sort_function' in kwargs: + kwargs['cmp'] = kwargs.pop('sort_function') + self._values.sort(*args, **kwargs) + + def reverse(self) -> None: + self._values.reverse() + + +# TODO(slebedev): Remove this. BaseContainer does *not* conform to +# MutableSequence, only its subclasses do. +collections.abc.MutableSequence.register(BaseContainer) + + +class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, type-checked, list-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_type_checker'] + + def __init__( + self, + message_listener: Any, + type_checker: Any, + ) -> None: + """Args: + + message_listener: A MessageListener implementation. The + RepeatedScalarFieldContainer will call this object's Modified() method + when it is modified. + type_checker: A type_checkers.ValueChecker instance to run on elements + inserted into this container. + """ + super().__init__(message_listener) + self._type_checker = type_checker + + def append(self, value: _T) -> None: + """Appends an item to the list. Similar to list.append().""" + self._values.append(self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position. Similar to list.insert().""" + self._values.insert(key, self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given iterable. Similar to list.extend().""" + if elem_seq is None: + return + try: + elem_seq_iter = iter(elem_seq) + except TypeError: + if not elem_seq: + # silently ignore falsy inputs :-/. + # TODO(ptucker): Deprecate this behavior. b/18413862 + return + raise + + new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] + if new_values: + self._values.extend(new_values) + self._message_listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one. We do not check the types of the individual fields. + """ + self._values.extend(other) + self._message_listener.Modified() + + def remove(self, elem: _T): + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value) -> None: + """Sets the item on the specified position.""" + if isinstance(key, slice): + if key.step is not None: + raise ValueError('Extended slices not supported') + self._values[key] = map(self._type_checker.CheckValue, value) + self._message_listener.Modified() + else: + self._values[key] = self._type_checker.CheckValue(value) + self._message_listener.Modified() + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + # Special case for the same type which should be common and fast. + if isinstance(other, self.__class__): + return other._values == self._values + # We are presumably comparing against some other sequence type. + return other == self._values + + def __deepcopy__( + self, + unused_memo: Any = None, + ) -> 'RepeatedScalarFieldContainer[_T]': + clone = RepeatedScalarFieldContainer( + copy.deepcopy(self._message_listener), self._type_checker) + clone.MergeFrom(self) + return clone + + def __reduce__(self, **kwargs) -> NoReturn: + raise pickle.PickleError( + "Can't pickle repeated scalar fields, convert to list first") + + +# TODO(slebedev): Constrain T to be a subtype of Message. +class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, list-like container for holding repeated composite fields.""" + + # Disallows assignment to other attributes. + __slots__ = ['_message_descriptor'] + + def __init__(self, message_listener: Any, message_descriptor: Any) -> None: + """ + Note that we pass in a descriptor instead of the generated directly, + since at the time we construct a _RepeatedCompositeFieldContainer we + haven't yet necessarily initialized the type that will be contained in the + container. + + Args: + message_listener: A MessageListener implementation. + The RepeatedCompositeFieldContainer will call this object's + Modified() method when it is modified. + message_descriptor: A Descriptor instance describing the protocol type + that should be present in this container. We'll use the + _concrete_class field of this descriptor when the client calls add(). + """ + super().__init__(message_listener) + self._message_descriptor = message_descriptor + + def add(self, **kwargs: Any) -> _T: + """Adds a new element at the end of the list and returns it. Keyword + arguments may be used to initialize the element. + """ + new_element = self._message_descriptor._concrete_class(**kwargs) + new_element._SetListener(self._message_listener) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + return new_element + + def append(self, value: _T) -> None: + """Appends one element by copying the message.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position by copying.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.insert(key, new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given sequence of elements of the same type + + as this one, copying each individual message. + """ + message_class = self._message_descriptor._concrete_class + listener = self._message_listener + values = self._values + for message in elem_seq: + new_element = message_class() + new_element._SetListener(listener) + new_element.MergeFrom(message) + values.append(new_element) + listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one, copying each individual message. + """ + self.extend(other) + + def remove(self, elem: _T) -> None: + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value): + # This method is implemented to make RepeatedCompositeFieldContainer + # structurally compatible with typing.MutableSequence. It is + # otherwise unsupported and will always raise an error. + raise TypeError( + f'{self.__class__.__name__} object does not support item assignment') + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + if not isinstance(other, self.__class__): + raise TypeError('Can only compare repeated composite fields against ' + 'other repeated composite fields.') + return self._values == other._values + + +class ScalarMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', + '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + key_checker: Any, + value_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._key_checker = key_checker + self._value_checker = value_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + try: + return self._values[key] + except KeyError: + key = self._key_checker.CheckValue(key) + val = self._value_checker.DefaultValue() + self._values[key] = val + return val + + def __contains__(self, item: _K) -> bool: + # We check the key's type to match the strong-typing flavor of the API. + # Also this makes it easier to match the behavior of the C++ implementation. + self._key_checker.CheckValue(item) + return item in self._values + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __setitem__(self, key: _K, value: _V) -> _T: + checked_key = self._key_checker.CheckValue(key) + checked_value = self._value_checker.CheckValue(value) + self._values[checked_key] = checked_value + self._message_listener.Modified() + + def __delitem__(self, key: _K) -> None: + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: + self._values.update(other._values) + self._message_listener.Modified() + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class MessageMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for with submessage values.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_values', '_message_listener', + '_message_descriptor', '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + message_descriptor: Any, + key_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._message_descriptor = message_descriptor + self._key_checker = key_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + key = self._key_checker.CheckValue(key) + try: + return self._values[key] + except KeyError: + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + self._values[key] = new_element + self._message_listener.Modified() + return new_element + + def get_or_create(self, key: _K) -> _V: + """get_or_create() is an alias for getitem (ie. map[key]). + + Args: + key: The key to get or create in the map. + + This is useful in cases where you want to be explicit that the call is + mutating the map. This can avoid lint errors for statements like this + that otherwise would appear to be pointless statements: + + msg.my_map[key] + """ + return self[key] + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __contains__(self, item: _K) -> bool: + item = self._key_checker.CheckValue(item) + return item in self._values + + def __setitem__(self, key: _K, value: _V) -> NoReturn: + raise ValueError('May not set values directly, call my_map[key].foo = 5') + + def __delitem__(self, key: _K) -> None: + key = self._key_checker.CheckValue(key) + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: + # pylint: disable=protected-access + for key in other._values: + # According to documentation: "When parsing from the wire or when merging, + # if there are duplicate map keys the last key seen is used". + if key in self: + del self[key] + self[key].CopyFrom(other[key]) + # self._message_listener.Modified() not required here, because + # mutations to submessages already propagate. + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class _UnknownField: + """A parsed unknown field.""" + + # Disallows assignment to other attributes. + __slots__ = ['_field_number', '_wire_type', '_data'] + + def __init__(self, field_number, wire_type, data): + self._field_number = field_number + self._wire_type = wire_type + self._data = data + return + + def __lt__(self, other): + # pylint: disable=protected-access + return self._field_number < other._field_number + + def __eq__(self, other): + if self is other: + return True + # pylint: disable=protected-access + return (self._field_number == other._field_number and + self._wire_type == other._wire_type and + self._data == other._data) + + +class UnknownFieldRef: # pylint: disable=missing-class-docstring + + def __init__(self, parent, index): + self._parent = parent + self._index = index + + def _check_valid(self): + if not self._parent: + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + if self._index >= len(self._parent): + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + + @property + def field_number(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._field_number + + @property + def wire_type(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._wire_type + + @property + def data(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._data + + +class UnknownFieldSet: + """UnknownField container""" + + # Disallows assignment to other attributes. + __slots__ = ['_values'] + + def __init__(self): + self._values = [] + + def __getitem__(self, index): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + size = len(self._values) + if index < 0: + index += size + if index < 0 or index >= size: + raise IndexError('index %d out of range'.index) + + return UnknownFieldRef(self, index) + + def _internal_get(self, index): + return self._values[index] + + def __len__(self): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + return len(self._values) + + def _add(self, field_number, wire_type, data): + unknown_field = _UnknownField(field_number, wire_type, data) + self._values.append(unknown_field) + return unknown_field + + def __iter__(self): + for i in range(len(self)): + yield UnknownFieldRef(self, i) + + def _extend(self, other): + if other is None: + return + # pylint: disable=protected-access + self._values.extend(other._values) + + def __eq__(self, other): + if self is other: + return True + # Sort unknown fields because their order shouldn't + # affect equality test. + values = list(self._values) + if other is None: + return not values + values.sort() + # pylint: disable=protected-access + other_values = sorted(other._values) + return values == other_values + + def _clear(self): + for value in self._values: + # pylint: disable=protected-access + if isinstance(value._data, UnknownFieldSet): + value._data._clear() # pylint: disable=protected-access + self._values = None diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py new file mode 100644 index 0000000000..bc1b7b785c --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py @@ -0,0 +1,1029 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Code for decoding protocol buffer primitives. + +This code is very similar to encoder.py -- read the docs for that module first. + +A "decoder" is a function with the signature: + Decode(buffer, pos, end, message, field_dict) +The arguments are: + buffer: The string containing the encoded message. + pos: The current position in the string. + end: The position in the string where the current message ends. May be + less than len(buffer) if we're reading a sub-message. + message: The message object into which we're parsing. + field_dict: message._fields (avoids a hashtable lookup). +The decoder reads the field and stores it into field_dict, returning the new +buffer position. A decoder for a repeated field may proactively decode all of +the elements of that field, if they appear consecutively. + +Note that decoders may throw any of the following: + IndexError: Indicates a truncated message. + struct.error: Unpacking of a fixed-width field failed. + message.DecodeError: Other errors. + +Decoders are expected to raise an exception if they are called with pos > end. +This allows callers to be lax about bounds checking: it's fineto read past +"end" as long as you are sure that someone else will notice and throw an +exception later on. + +Something up the call stack is expected to catch IndexError and struct.error +and convert them to message.DecodeError. + +Decoders are constructed using decoder constructors with the signature: + MakeDecoder(field_number, is_repeated, is_packed, key, new_default) +The arguments are: + field_number: The field number of the field we want to decode. + is_repeated: Is the field a repeated field? (bool) + is_packed: Is the field a packed field? (bool) + key: The key to use when looking up the field within field_dict. + (This is actually the FieldDescriptor but nothing in this + file should depend on that.) + new_default: A function which takes a message object as a parameter and + returns a new instance of the default value for this field. + (This is called for repeated fields and sub-messages, when an + instance does not already exist.) + +As with encoders, we define a decoder constructor for every type of field. +Then, for every field of every message class we construct an actual decoder. +That decoder goes into a dict indexed by tag, so when we decode a message +we repeatedly read a tag, look up the corresponding decoder, and invoke it. +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +import math +import struct + +from google.protobuf.internal import containers +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import message + + +# This is not for optimization, but rather to avoid conflicts with local +# variables named "message". +_DecodeError = message.DecodeError + + +def _VarintDecoder(mask, result_type): + """Return an encoder for a basic varint value (does not include tag). + + Decoded values will be bitwise-anded with the given mask before being + returned, e.g. to limit them to 32 bits. The returned decoder does not + take the usual "end" parameter -- the caller is expected to do bounds checking + after the fact (often the caller can defer such checking until later). The + decoder returns a (value, new_pos) pair. + """ + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + + +def _SignedVarintDecoder(bits, result_type): + """Like _VarintDecoder() but decodes signed values.""" + + signbit = 1 << (bits - 1) + mask = (1 << bits) - 1 + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = (result ^ signbit) - signbit + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + +# All 32-bit and 64-bit values are represented as int. +_DecodeVarint = _VarintDecoder((1 << 64) - 1, int) +_DecodeSignedVarint = _SignedVarintDecoder(64, int) + +# Use these versions for values which must be limited to 32 bits. +_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) +_DecodeSignedVarint32 = _SignedVarintDecoder(32, int) + + +def ReadTag(buffer, pos): + """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. + + We return the raw bytes of the tag rather than decoding them. The raw + bytes can then be used to look up the proper decoder. This effectively allows + us to trade some work that would be done in pure-python (decoding a varint) + for work that is done in C (searching for a byte string in a hash table). + In a low-level language it would be much cheaper to decode the varint and + use that, but not in Python. + + Args: + buffer: memoryview object of the encoded bytes + pos: int of the current position to start from + + Returns: + Tuple[bytes, int] of the tag data and new position. + """ + start = pos + while buffer[pos] & 0x80: + pos += 1 + pos += 1 + + tag_bytes = buffer[start:pos].tobytes() + return tag_bytes, pos + + +# -------------------------------------------------------------------- + + +def _SimpleDecoder(wire_type, decode_value): + """Return a constructor for a decoder for fields of a particular type. + + Args: + wire_type: The field's wire type. + decode_value: A function which decodes an individual value, e.g. + _DecodeVarint() + """ + + def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + if is_packed: + local_DecodeVarint = _DecodeVarint + def DecodePackedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + (endpoint, pos) = local_DecodeVarint(buffer, pos) + endpoint += pos + if endpoint > end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + (element, pos) = decode_value(buffer, pos) + value.append(element) + if pos > endpoint: + del value[-1] # Discard corrupt value. + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_type) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = decode_value(buffer, pos) + value.append(element) + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (new_value, pos) = decode_value(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not new_value: + field_dict.pop(key, None) + else: + field_dict[key] = new_value + return pos + return DecodeField + + return SpecificDecoder + + +def _ModifiedDecoder(wire_type, decode_value, modify_value): + """Like SimpleDecoder but additionally invokes modify_value on every value + before storing it. Usually modify_value is ZigZagDecode. + """ + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + def InnerDecode(buffer, pos): + (result, new_pos) = decode_value(buffer, pos) + return (modify_value(result), new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _StructPackDecoder(wire_type, format): + """Return a constructor for a decoder for a fixed-width field. + + Args: + wire_type: The field's wire type. + format: The format string to pass to struct.unpack(). + """ + + value_size = struct.calcsize(format) + local_unpack = struct.unpack + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + + def InnerDecode(buffer, pos): + new_pos = pos + value_size + result = local_unpack(format, buffer[pos:new_pos])[0] + return (result, new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _FloatDecoder(): + """Returns a decoder for a float field. + + This code works around a bug in struct.unpack for non-finite 32-bit + floating-point values. + """ + + local_unpack = struct.unpack + + def InnerDecode(buffer, pos): + """Decode serialized float to a float and new position. + + Args: + buffer: memoryview of the serialized bytes + pos: int, position in the memory view to start at. + + Returns: + Tuple[float, int] of the deserialized float value and new position + in the serialized data. + """ + # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign + # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. + new_pos = pos + 4 + float_bytes = buffer[pos:new_pos].tobytes() + + # If this value has all its exponent bits set, then it's non-finite. + # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. + # To avoid that, we parse it specially. + if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): + # If at least one significand bit is set... + if float_bytes[0:3] != b'\x00\x00\x80': + return (math.nan, new_pos) + # If sign bit is set... + if float_bytes[3:4] == b'\xFF': + return (-math.inf, new_pos) + return (math.inf, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack('= b'\xF0') + and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): + return (math.nan, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack(' end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + value_start_pos = pos + (element, pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + if pos > endpoint: + if element in enum_type.values_by_number: + del value[-1] # Discard corrupt value. + else: + del message._unknown_fields[-1] + # pylint: disable=protected-access + del message._unknown_field_set._values[-1] + # pylint: enable=protected-access + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (tag_bytes, buffer[pos:new_pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value_start_pos = pos + (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not enum_value: + field_dict.pop(key, None) + return pos + # pylint: disable=protected-access + if enum_value in enum_type.values_by_number: + field_dict[key] = enum_value + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, enum_value) + # pylint: enable=protected-access + return pos + return DecodeField + + +# -------------------------------------------------------------------- + + +Int32Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) + +Int64Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) + +UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) +UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) + +SInt32Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) +SInt64Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, ' end: + raise _DecodeError('Truncated string.') + value.append(_ConvertToUnicode(buffer[pos:new_pos])) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) + return new_pos + return DecodeField + + +def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for a bytes field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + value.append(buffer[pos:new_pos].tobytes()) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = buffer[pos:new_pos].tobytes() + return new_pos + return DecodeField + + +def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a group field.""" + + end_tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_END_GROUP) + end_tag_len = len(end_tag_bytes) + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_START_GROUP) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value.add()._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + return new_pos + return DecodeField + + +def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a message field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + return new_pos + return DecodeField + + +# -------------------------------------------------------------------- + +MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) + +def MessageSetItemDecoder(descriptor): + """Returns a decoder for a MessageSet item. + + The parameter is the message Descriptor. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + + type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) + message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) + item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) + + local_ReadTag = ReadTag + local_DecodeVarint = _DecodeVarint + local_SkipField = SkipField + + def DecodeItem(buffer, pos, end, message, field_dict): + """Decode serialized message set to its value and new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + message_set_item_start = pos + type_id = -1 + message_start = -1 + message_end = -1 + + # Technically, type_id and message can appear in any order, so we need + # a little loop here. + while 1: + (tag_bytes, pos) = local_ReadTag(buffer, pos) + if tag_bytes == type_id_tag_bytes: + (type_id, pos) = local_DecodeVarint(buffer, pos) + elif tag_bytes == message_tag_bytes: + (size, message_start) = local_DecodeVarint(buffer, pos) + pos = message_end = message_start + size + elif tag_bytes == item_end_tag_bytes: + break + else: + pos = SkipField(buffer, pos, end, tag_bytes) + if pos == -1: + raise _DecodeError('Missing group end tag.') + + if pos > end: + raise _DecodeError('Truncated message.') + + if type_id == -1: + raise _DecodeError('MessageSet item missing type_id.') + if message_start == -1: + raise _DecodeError('MessageSet item missing message.') + + extension = message.Extensions._FindExtensionByNumber(type_id) + # pylint: disable=protected-access + if extension is not None: + value = field_dict.get(extension) + if value is None: + message_type = extension.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + message._FACTORY.GetPrototype(message_type) + value = field_dict.setdefault( + extension, message_type._concrete_class()) + if value._InternalParse(buffer, message_start,message_end) != message_end: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + type_id, + wire_format.WIRETYPE_LENGTH_DELIMITED, + buffer[message_start:message_end].tobytes()) + # pylint: enable=protected-access + + return pos + + return DecodeItem + +# -------------------------------------------------------------------- + +def MapDecoder(field_descriptor, new_default, is_message_map): + """Returns a decoder for a map field.""" + + key = field_descriptor + tag_bytes = encoder.TagBytes(field_descriptor.number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + local_DecodeVarint = _DecodeVarint + # Can't read _concrete_class yet; might not be initialized. + message_type = field_descriptor.message_type + + def DecodeMap(buffer, pos, end, message, field_dict): + submsg = message_type._concrete_class() + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + submsg.Clear() + if submsg._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + + if is_message_map: + value[submsg.key].CopyFrom(submsg.value) + else: + value[submsg.key] = submsg.value + + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + + return DecodeMap + +# -------------------------------------------------------------------- +# Optimization is not as heavy here because calls to SkipField() are rare, +# except for handling end-group tags. + +def _SkipVarint(buffer, pos, end): + """Skip a varint value. Returns the new position.""" + # Previously ord(buffer[pos]) raised IndexError when pos is out of range. + # With this code, ord(b'') raises TypeError. Both are handled in + # python_message.py to generate a 'Truncated message' error. + while ord(buffer[pos:pos+1].tobytes()) & 0x80: + pos += 1 + pos += 1 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + +def _SkipFixed64(buffer, pos, end): + """Skip a fixed64 value. Returns the new position.""" + + pos += 8 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed64(buffer, pos): + """Decode a fixed64.""" + new_pos = pos + 8 + return (struct.unpack(' end: + raise _DecodeError('Truncated message.') + return pos + + +def _SkipGroup(buffer, pos, end): + """Skip sub-group. Returns the new position.""" + + while 1: + (tag_bytes, pos) = ReadTag(buffer, pos) + new_pos = SkipField(buffer, pos, end, tag_bytes) + if new_pos == -1: + return pos + pos = new_pos + + +def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): + """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" + + unknown_field_set = containers.UnknownFieldSet() + while end_pos is None or pos < end_pos: + (tag_bytes, pos) = ReadTag(buffer, pos) + (tag, _) = _DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if wire_type == wire_format.WIRETYPE_END_GROUP: + break + (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + + return (unknown_field_set, pos) + + +def _DecodeUnknownField(buffer, pos, wire_type): + """Decode a unknown field. Returns the UnknownField and new position.""" + + if wire_type == wire_format.WIRETYPE_VARINT: + (data, pos) = _DecodeVarint(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED64: + (data, pos) = _DecodeFixed64(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED32: + (data, pos) = _DecodeFixed32(buffer, pos) + elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: + (size, pos) = _DecodeVarint(buffer, pos) + data = buffer[pos:pos+size].tobytes() + pos += size + elif wire_type == wire_format.WIRETYPE_START_GROUP: + (data, pos) = _DecodeUnknownFieldSet(buffer, pos) + elif wire_type == wire_format.WIRETYPE_END_GROUP: + return (0, -1) + else: + raise _DecodeError('Wrong wire type in tag.') + + return (data, pos) + + +def _EndGroup(buffer, pos, end): + """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" + + return -1 + + +def _SkipFixed32(buffer, pos, end): + """Skip a fixed32 value. Returns the new position.""" + + pos += 4 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed32(buffer, pos): + """Decode a fixed32.""" + + new_pos = pos + 4 + return (struct.unpack('B').pack + + def EncodeVarint(write, value, unused_deterministic=None): + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeVarint + + +def _SignedVarintEncoder(): + """Return an encoder for a basic signed varint value (does not include + tag).""" + + local_int2byte = struct.Struct('>B').pack + + def EncodeSignedVarint(write, value, unused_deterministic=None): + if value < 0: + value += (1 << 64) + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeSignedVarint + + +_EncodeVarint = _VarintEncoder() +_EncodeSignedVarint = _SignedVarintEncoder() + + +def _VarintBytes(value): + """Encode the given integer as a varint and return the bytes. This is only + called at startup time so it doesn't need to be fast.""" + + pieces = [] + _EncodeVarint(pieces.append, value, True) + return b"".join(pieces) + + +def TagBytes(field_number, wire_type): + """Encode the given tag and return the bytes. Only called at startup.""" + + return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) + +# -------------------------------------------------------------------- +# As with sizers (see above), we have a number of common encoder +# implementations. + + +def _SimpleEncoder(wire_type, encode_value, compute_value_size): + """Return a constructor for an encoder for fields of a particular type. + + Args: + wire_type: The field's wire type, for encoding tags. + encode_value: A function which encodes an individual value, e.g. + _EncodeVarint(). + compute_value_size: A function which computes the size of an individual + value, e.g. _VarintSize(). + """ + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(element) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, element, deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, element, deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, value, deterministic) + return EncodeField + + return SpecificEncoder + + +def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): + """Like SimpleEncoder but additionally invokes modify_value on every value + before passing it to encode_value. Usually modify_value is ZigZagEncode.""" + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(modify_value(element)) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, modify_value(element), deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, modify_value(element), deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, modify_value(value), deterministic) + return EncodeField + + return SpecificEncoder + + +def _StructPackEncoder(wire_type, format): + """Return a constructor for an encoder for a fixed-width field. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + write(local_struct_pack(format, element)) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + write(local_struct_pack(format, element)) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + return write(local_struct_pack(format, value)) + return EncodeField + + return SpecificEncoder + + +def _FloatingPointEncoder(wire_type, format): + """Return a constructor for an encoder for float fields. + + This is like StructPackEncoder, but catches errors that may be due to + passing non-finite floating-point values to struct.pack, and makes a + second attempt to encode those values. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + if value_size == 4: + def EncodeNonFiniteOrRaise(write, value): + # Remember that the serialized form uses little-endian byte order. + if value == _POS_INF: + write(b'\x00\x00\x80\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x80\xFF') + elif value != value: # NaN + write(b'\x00\x00\xC0\x7F') + else: + raise + elif value_size == 8: + def EncodeNonFiniteOrRaise(write, value): + if value == _POS_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') + elif value != value: # NaN + write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') + else: + raise + else: + raise ValueError('Can\'t encode floating-point values that are ' + '%d bytes long (only 4 or 8)' % value_size) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + # This try/except block is going to be faster than any code that + # we could write to check whether element is finite. + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + try: + write(local_struct_pack(format, value)) + except SystemError: + EncodeNonFiniteOrRaise(write, value) + return EncodeField + + return SpecificEncoder + + +# ==================================================================== +# Here we declare an encoder constructor for each field type. These work +# very similarly to sizer constructors, described earlier. + + +Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) + +UInt32Encoder = UInt64Encoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) + +SInt32Encoder = SInt64Encoder = _ModifiedEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, + wire_format.ZigZagEncode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str + ValueType = int + + def __init__(self, enum_type): + """Inits EnumTypeWrapper with an EnumDescriptor.""" + self._enum_type = enum_type + self.DESCRIPTOR = enum_type # pylint: disable=invalid-name + + def Name(self, number): # pylint: disable=invalid-name + """Returns a string containing the name of an enum value.""" + try: + return self._enum_type.values_by_number[number].name + except KeyError: + pass # fall out to break exception chaining + + if not isinstance(number, int): + raise TypeError( + 'Enum value for {} must be an int, but got {} {!r}.'.format( + self._enum_type.name, type(number), number)) + else: + # repr here to handle the odd case when you pass in a boolean. + raise ValueError('Enum {} has no name defined for value {!r}'.format( + self._enum_type.name, number)) + + def Value(self, name): # pylint: disable=invalid-name + """Returns the value corresponding to the given enum name.""" + try: + return self._enum_type.values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise ValueError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) + + def keys(self): + """Return a list of the string names in the enum. + + Returns: + A list of strs, in the order they were defined in the .proto file. + """ + + return [value_descriptor.name + for value_descriptor in self._enum_type.values] + + def values(self): + """Return a list of the integer values in the enum. + + Returns: + A list of ints, in the order they were defined in the .proto file. + """ + + return [value_descriptor.number + for value_descriptor in self._enum_type.values] + + def items(self): + """Return a list of the (name, value) pairs of the enum. + + Returns: + A list of (str, int) pairs, in the order they were defined + in the .proto file. + """ + return [(value_descriptor.name, value_descriptor.number) + for value_descriptor in self._enum_type.values] + + def __getattr__(self, name): + """Returns the value corresponding to the given enum name.""" + try: + return super( + EnumTypeWrapper, + self).__getattribute__('_enum_type').values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise AttributeError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py new file mode 100644 index 0000000000..b346cf283e --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py @@ -0,0 +1,213 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains _ExtensionDict class to represent extensions. +""" + +from google.protobuf.internal import type_checkers +from google.protobuf.descriptor import FieldDescriptor + + +def _VerifyExtensionHandle(message, extension_handle): + """Verify that the given extension handle is valid.""" + + if not isinstance(extension_handle, FieldDescriptor): + raise KeyError('HasExtension() expects an extension handle, got: %s' % + extension_handle) + + if not extension_handle.is_extension: + raise KeyError('"%s" is not an extension.' % extension_handle.full_name) + + if not extension_handle.containing_type: + raise KeyError('"%s" is missing a containing_type.' + % extension_handle.full_name) + + if extension_handle.containing_type is not message.DESCRIPTOR: + raise KeyError('Extension "%s" extends message type "%s", but this ' + 'message is of type "%s".' % + (extension_handle.full_name, + extension_handle.containing_type.full_name, + message.DESCRIPTOR.full_name)) + + +# TODO(robinson): Unify error handling of "unknown extension" crap. +# TODO(robinson): Support iteritems()-style iteration over all +# extensions with the "has" bits turned on? +class _ExtensionDict(object): + + """Dict-like container for Extension fields on proto instances. + + Note that in all cases we expect extension handles to be + FieldDescriptors. + """ + + def __init__(self, extended_message): + """ + Args: + extended_message: Message instance for which we are the Extensions dict. + """ + self._extended_message = extended_message + + def __getitem__(self, extension_handle): + """Returns the current value of the given extension handle.""" + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + result = self._extended_message._fields.get(extension_handle) + if result is not None: + return result + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + result = extension_handle._default_constructor(self._extended_message) + elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + message_type = extension_handle.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + self._extended_message._FACTORY.GetPrototype(message_type) + assert getattr(extension_handle.message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (extension_handle.full_name, + extension_handle.message_type.full_name)) + result = extension_handle.message_type._concrete_class() + try: + result._SetListener(self._extended_message._listener_for_children) + except ReferenceError: + pass + else: + # Singular scalar -- just return the default without inserting into the + # dict. + return extension_handle.default_value + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + result = self._extended_message._fields.setdefault( + extension_handle, result) + + return result + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + my_fields = self._extended_message.ListFields() + other_fields = other._extended_message.ListFields() + + # Get rid of non-extension fields. + my_fields = [field for field in my_fields if field.is_extension] + other_fields = [field for field in other_fields if field.is_extension] + + return my_fields == other_fields + + def __ne__(self, other): + return not self == other + + def __len__(self): + fields = self._extended_message.ListFields() + # Get rid of non-extension fields. + extension_fields = [field for field in fields if field[0].is_extension] + return len(extension_fields) + + def __hash__(self): + raise TypeError('unhashable object') + + # Note that this is only meaningful for non-repeated, scalar extension + # fields. Note also that we may have to call _Modified() when we do + # successfully set a field this way, to set any necessary "has" bits in the + # ancestors of the extended message. + def __setitem__(self, extension_handle, value): + """If extension_handle specifies a non-repeated, scalar extension + field, sets the value of that field. + """ + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or + extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): + raise TypeError( + 'Cannot assign to extension "%s" because it is a repeated or ' + 'composite type.' % extension_handle.full_name) + + # It's slightly wasteful to lookup the type checker each time, + # but we expect this to be a vanishingly uncommon case anyway. + type_checker = type_checkers.GetTypeChecker(extension_handle) + # pylint: disable=protected-access + self._extended_message._fields[extension_handle] = ( + type_checker.CheckValue(value)) + self._extended_message._Modified() + + def __delitem__(self, extension_handle): + self._extended_message.ClearExtension(extension_handle) + + def _FindExtensionByName(self, name): + """Tries to find a known extension with the specified name. + + Args: + name: Extension full name. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_name.get(name, None) + + def _FindExtensionByNumber(self, number): + """Tries to find a known extension with the field number. + + Args: + number: Extension field number. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_number.get(number, None) + + def __iter__(self): + # Return a generator over the populated extension fields + return (f[0] for f in self._extended_message.ListFields() + if f[0].is_extension) + + def __contains__(self, extension_handle): + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if extension_handle not in self._extended_message._fields: + return False + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + return bool(self._extended_message._fields.get(extension_handle)) + + if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + value = self._extended_message._fields.get(extension_handle) + # pylint: disable=protected-access + return value is not None and value._is_present_in_parent + + return True diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py new file mode 100644 index 0000000000..0fc255a774 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py @@ -0,0 +1,78 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Defines a listener interface for observing certain +state transitions on Message objects. + +Also defines a null implementation of this interface. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +class MessageListener(object): + + """Listens for modifications made to a message. Meant to be registered via + Message._SetListener(). + + Attributes: + dirty: If True, then calling Modified() would be a no-op. This can be + used to avoid these calls entirely in the common case. + """ + + def Modified(self): + """Called every time the message is modified in such a way that the parent + message may need to be updated. This currently means either: + (a) The message was modified for the first time, so the parent message + should henceforth mark the message as present. + (b) The message's cached byte size became dirty -- i.e. the message was + modified for the first time after a previous call to ByteSize(). + Therefore the parent should also mark its byte size as dirty. + Note that (a) implies (b), since new objects start out with a client cached + size (zero). However, we document (a) explicitly because it is important. + + Modified() will *only* be called in response to one of these two events -- + not every time the sub-message is modified. + + Note that if the listener's |dirty| attribute is true, then calling + Modified at the moment would be a no-op, so it can be skipped. Performance- + sensitive callers should check this attribute directly before calling since + it will be true most of the time. + """ + + raise NotImplementedError + + +class NullMessageListener(object): + + """No-op MessageListener implementation.""" + + def Modified(self): + pass diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py new file mode 100644 index 0000000000..63651a3f19 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/message_set_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5google/protobuf/internal/message_set_extensions.proto\x12\x18google.protobuf.internal\"\x1e\n\x0eTestMessageSet*\x08\x08\x04\x10\xff\xff\xff\xff\x07:\x02\x08\x01\"\xa5\x01\n\x18TestMessageSetExtension1\x12\t\n\x01i\x18\x0f \x01(\x05\x32~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xab\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension1\"\xa7\x01\n\x18TestMessageSetExtension2\x12\x0b\n\x03str\x18\x19 \x01(\t2~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xca\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension2\"(\n\x18TestMessageSetExtension3\x12\x0c\n\x04text\x18# \x01(\t:\x7f\n\x16message_set_extension3\x12(.google.protobuf.internal.TestMessageSet\x18\xdf\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageSet.RegisterExtension(message_set_extension3) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension']) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension']) + + DESCRIPTOR._options = None + _TESTMESSAGESET._options = None + _TESTMESSAGESET._serialized_options = b'\010\001' + _TESTMESSAGESET._serialized_start=83 + _TESTMESSAGESET._serialized_end=113 + _TESTMESSAGESETEXTENSION1._serialized_start=116 + _TESTMESSAGESETEXTENSION1._serialized_end=281 + _TESTMESSAGESETEXTENSION2._serialized_start=284 + _TESTMESSAGESETEXTENSION2._serialized_end=451 + _TESTMESSAGESETEXTENSION3._serialized_start=453 + _TESTMESSAGESETEXTENSION3._serialized_end=493 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py new file mode 100644 index 0000000000..5497083197 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/missing_enum_values.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2google/protobuf/internal/missing_enum_values.proto\x12\x1fgoogle.protobuf.python.internal\"\xc1\x02\n\x0eTestEnumValues\x12X\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12X\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12Z\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\x02\x10\x01\"\x1f\n\nNestedEnum\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\"\xd3\x02\n\x15TestMissingEnumValues\x12_\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12_\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12\x61\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\x02\x10\x01\"\x15\n\nNestedEnum\x12\x07\n\x03TWO\x10\x02\"\x1b\n\nJustString\x12\r\n\x05\x64ummy\x18\x01 \x02(\t') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTENUMVALUES._serialized_start=88 + _TESTENUMVALUES._serialized_end=409 + _TESTENUMVALUES_NESTEDENUM._serialized_start=378 + _TESTENUMVALUES_NESTEDENUM._serialized_end=409 + _TESTMISSINGENUMVALUES._serialized_start=412 + _TESTMISSINGENUMVALUES._serialized_end=751 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751 + _JUSTSTRING._serialized_start=753 + _JUSTSTRING._serialized_end=780 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py new file mode 100644 index 0000000000..0953706bac --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions_dynamic.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6google/protobuf/internal/more_extensions_dynamic.proto\x12\x18google.protobuf.internal\x1a.google/protobuf/internal/more_extensions.proto\"\x1f\n\x12\x44ynamicMessageType\x12\t\n\x01\x61\x18\x01 \x01(\x05:J\n\x17\x64ynamic_int32_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x64 \x01(\x05:z\n\x19\x64ynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x65 \x01(\x0b\x32,.google.protobuf.internal.DynamicMessageType:\x83\x01\n\"repeated_dynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x66 \x03(\x0b\x32,.google.protobuf.internal.DynamicMessageType') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension) + + DESCRIPTOR._options = None + _DYNAMICMESSAGETYPE._serialized_start=132 + _DYNAMICMESSAGETYPE._serialized_end=163 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py new file mode 100644 index 0000000000..1cfa1b7c8b --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.google/protobuf/internal/more_extensions.proto\x12\x18google.protobuf.internal\"\x99\x01\n\x0fTopLevelMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\x12\x43\n\x0enested_message\x18\x02 \x01(\x0b\x32\'.google.protobuf.internal.NestedMessageB\x02(\x01\"R\n\rNestedMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\"K\n\x0f\x45xtendedMessage\x12\x17\n\x0eoptional_int32\x18\xe9\x07 \x01(\x05\x12\x18\n\x0frepeated_string\x18\xea\x07 \x03(\t*\x05\x08\x01\x10\xe8\x07\"-\n\x0e\x46oreignMessage\x12\x1b\n\x13\x66oreign_message_int\x18\x01 \x01(\x05:I\n\x16optional_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x01 \x01(\x05:w\n\x1aoptional_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x02 \x01(\x0b\x32(.google.protobuf.internal.ForeignMessage:I\n\x16repeated_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x03 \x03(\x05:w\n\x1arepeated_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x04 \x03(\x0b\x32(.google.protobuf.internal.ForeignMessage') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + ExtendedMessage.RegisterExtension(optional_int_extension) + ExtendedMessage.RegisterExtension(optional_message_extension) + ExtendedMessage.RegisterExtension(repeated_int_extension) + ExtendedMessage.RegisterExtension(repeated_message_extension) + + DESCRIPTOR._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None + _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\001' + _NESTEDMESSAGE.fields_by_name['submessage']._options = None + _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE._serialized_start=77 + _TOPLEVELMESSAGE._serialized_end=230 + _NESTEDMESSAGE._serialized_start=232 + _NESTEDMESSAGE._serialized_end=314 + _EXTENDEDMESSAGE._serialized_start=316 + _EXTENDEDMESSAGE._serialized_end=391 + _FOREIGNMESSAGE._serialized_start=393 + _FOREIGNMESSAGE._serialized_end=438 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py new file mode 100644 index 0000000000..d7f7115609 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py @@ -0,0 +1,556 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_messages.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,google/protobuf/internal/more_messages.proto\x12\x18google.protobuf.internal\"h\n\x10OutOfOrderFields\x12\x17\n\x0foptional_sint32\x18\x05 \x01(\x11\x12\x17\n\x0foptional_uint32\x18\x03 \x01(\r\x12\x16\n\x0eoptional_int32\x18\x01 \x01(\x05*\x04\x08\x04\x10\x05*\x04\x08\x02\x10\x03\"\xcd\x02\n\x05\x63lass\x12\x1b\n\tint_field\x18\x01 \x01(\x05R\x08json_int\x12\n\n\x02if\x18\x02 \x01(\x05\x12(\n\x02\x61s\x18\x03 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12\x30\n\nenum_field\x18\x04 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12>\n\x11nested_enum_field\x18\x05 \x01(\x0e\x32#.google.protobuf.internal.class.for\x12;\n\x0enested_message\x18\x06 \x01(\x0b\x32#.google.protobuf.internal.class.try\x1a\x1c\n\x03try\x12\r\n\x05\x66ield\x18\x01 \x01(\x05*\x06\x08\xe7\x07\x10\x90N\"\x1c\n\x03\x66or\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04True\x10\x01*\x06\x08\xe7\x07\x10\x90N\"?\n\x0b\x45xtendClass20\n\x06return\x12\x1f.google.protobuf.internal.class\x18\xea\x07 \x01(\x05\"~\n\x0fTestFullKeyword\x12:\n\x06\x66ield1\x18\x01 \x01(\x0b\x32*.google.protobuf.internal.OutOfOrderFields\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.google.protobuf.internal.class\"\xa5\x0f\n\x11LotsNestedMessage\x1a\x04\n\x02\x42\x30\x1a\x04\n\x02\x42\x31\x1a\x04\n\x02\x42\x32\x1a\x04\n\x02\x42\x33\x1a\x04\n\x02\x42\x34\x1a\x04\n\x02\x42\x35\x1a\x04\n\x02\x42\x36\x1a\x04\n\x02\x42\x37\x1a\x04\n\x02\x42\x38\x1a\x04\n\x02\x42\x39\x1a\x05\n\x03\x42\x31\x30\x1a\x05\n\x03\x42\x31\x31\x1a\x05\n\x03\x42\x31\x32\x1a\x05\n\x03\x42\x31\x33\x1a\x05\n\x03\x42\x31\x34\x1a\x05\n\x03\x42\x31\x35\x1a\x05\n\x03\x42\x31\x36\x1a\x05\n\x03\x42\x31\x37\x1a\x05\n\x03\x42\x31\x38\x1a\x05\n\x03\x42\x31\x39\x1a\x05\n\x03\x42\x32\x30\x1a\x05\n\x03\x42\x32\x31\x1a\x05\n\x03\x42\x32\x32\x1a\x05\n\x03\x42\x32\x33\x1a\x05\n\x03\x42\x32\x34\x1a\x05\n\x03\x42\x32\x35\x1a\x05\n\x03\x42\x32\x36\x1a\x05\n\x03\x42\x32\x37\x1a\x05\n\x03\x42\x32\x38\x1a\x05\n\x03\x42\x32\x39\x1a\x05\n\x03\x42\x33\x30\x1a\x05\n\x03\x42\x33\x31\x1a\x05\n\x03\x42\x33\x32\x1a\x05\n\x03\x42\x33\x33\x1a\x05\n\x03\x42\x33\x34\x1a\x05\n\x03\x42\x33\x35\x1a\x05\n\x03\x42\x33\x36\x1a\x05\n\x03\x42\x33\x37\x1a\x05\n\x03\x42\x33\x38\x1a\x05\n\x03\x42\x33\x39\x1a\x05\n\x03\x42\x34\x30\x1a\x05\n\x03\x42\x34\x31\x1a\x05\n\x03\x42\x34\x32\x1a\x05\n\x03\x42\x34\x33\x1a\x05\n\x03\x42\x34\x34\x1a\x05\n\x03\x42\x34\x35\x1a\x05\n\x03\x42\x34\x36\x1a\x05\n\x03\x42\x34\x37\x1a\x05\n\x03\x42\x34\x38\x1a\x05\n\x03\x42\x34\x39\x1a\x05\n\x03\x42\x35\x30\x1a\x05\n\x03\x42\x35\x31\x1a\x05\n\x03\x42\x35\x32\x1a\x05\n\x03\x42\x35\x33\x1a\x05\n\x03\x42\x35\x34\x1a\x05\n\x03\x42\x35\x35\x1a\x05\n\x03\x42\x35\x36\x1a\x05\n\x03\x42\x35\x37\x1a\x05\n\x03\x42\x35\x38\x1a\x05\n\x03\x42\x35\x39\x1a\x05\n\x03\x42\x36\x30\x1a\x05\n\x03\x42\x36\x31\x1a\x05\n\x03\x42\x36\x32\x1a\x05\n\x03\x42\x36\x33\x1a\x05\n\x03\x42\x36\x34\x1a\x05\n\x03\x42\x36\x35\x1a\x05\n\x03\x42\x36\x36\x1a\x05\n\x03\x42\x36\x37\x1a\x05\n\x03\x42\x36\x38\x1a\x05\n\x03\x42\x36\x39\x1a\x05\n\x03\x42\x37\x30\x1a\x05\n\x03\x42\x37\x31\x1a\x05\n\x03\x42\x37\x32\x1a\x05\n\x03\x42\x37\x33\x1a\x05\n\x03\x42\x37\x34\x1a\x05\n\x03\x42\x37\x35\x1a\x05\n\x03\x42\x37\x36\x1a\x05\n\x03\x42\x37\x37\x1a\x05\n\x03\x42\x37\x38\x1a\x05\n\x03\x42\x37\x39\x1a\x05\n\x03\x42\x38\x30\x1a\x05\n\x03\x42\x38\x31\x1a\x05\n\x03\x42\x38\x32\x1a\x05\n\x03\x42\x38\x33\x1a\x05\n\x03\x42\x38\x34\x1a\x05\n\x03\x42\x38\x35\x1a\x05\n\x03\x42\x38\x36\x1a\x05\n\x03\x42\x38\x37\x1a\x05\n\x03\x42\x38\x38\x1a\x05\n\x03\x42\x38\x39\x1a\x05\n\x03\x42\x39\x30\x1a\x05\n\x03\x42\x39\x31\x1a\x05\n\x03\x42\x39\x32\x1a\x05\n\x03\x42\x39\x33\x1a\x05\n\x03\x42\x39\x34\x1a\x05\n\x03\x42\x39\x35\x1a\x05\n\x03\x42\x39\x36\x1a\x05\n\x03\x42\x39\x37\x1a\x05\n\x03\x42\x39\x38\x1a\x05\n\x03\x42\x39\x39\x1a\x06\n\x04\x42\x31\x30\x30\x1a\x06\n\x04\x42\x31\x30\x31\x1a\x06\n\x04\x42\x31\x30\x32\x1a\x06\n\x04\x42\x31\x30\x33\x1a\x06\n\x04\x42\x31\x30\x34\x1a\x06\n\x04\x42\x31\x30\x35\x1a\x06\n\x04\x42\x31\x30\x36\x1a\x06\n\x04\x42\x31\x30\x37\x1a\x06\n\x04\x42\x31\x30\x38\x1a\x06\n\x04\x42\x31\x30\x39\x1a\x06\n\x04\x42\x31\x31\x30\x1a\x06\n\x04\x42\x31\x31\x31\x1a\x06\n\x04\x42\x31\x31\x32\x1a\x06\n\x04\x42\x31\x31\x33\x1a\x06\n\x04\x42\x31\x31\x34\x1a\x06\n\x04\x42\x31\x31\x35\x1a\x06\n\x04\x42\x31\x31\x36\x1a\x06\n\x04\x42\x31\x31\x37\x1a\x06\n\x04\x42\x31\x31\x38\x1a\x06\n\x04\x42\x31\x31\x39\x1a\x06\n\x04\x42\x31\x32\x30\x1a\x06\n\x04\x42\x31\x32\x31\x1a\x06\n\x04\x42\x31\x32\x32\x1a\x06\n\x04\x42\x31\x32\x33\x1a\x06\n\x04\x42\x31\x32\x34\x1a\x06\n\x04\x42\x31\x32\x35\x1a\x06\n\x04\x42\x31\x32\x36\x1a\x06\n\x04\x42\x31\x32\x37\x1a\x06\n\x04\x42\x31\x32\x38\x1a\x06\n\x04\x42\x31\x32\x39\x1a\x06\n\x04\x42\x31\x33\x30\x1a\x06\n\x04\x42\x31\x33\x31\x1a\x06\n\x04\x42\x31\x33\x32\x1a\x06\n\x04\x42\x31\x33\x33\x1a\x06\n\x04\x42\x31\x33\x34\x1a\x06\n\x04\x42\x31\x33\x35\x1a\x06\n\x04\x42\x31\x33\x36\x1a\x06\n\x04\x42\x31\x33\x37\x1a\x06\n\x04\x42\x31\x33\x38\x1a\x06\n\x04\x42\x31\x33\x39\x1a\x06\n\x04\x42\x31\x34\x30\x1a\x06\n\x04\x42\x31\x34\x31\x1a\x06\n\x04\x42\x31\x34\x32\x1a\x06\n\x04\x42\x31\x34\x33\x1a\x06\n\x04\x42\x31\x34\x34\x1a\x06\n\x04\x42\x31\x34\x35\x1a\x06\n\x04\x42\x31\x34\x36\x1a\x06\n\x04\x42\x31\x34\x37\x1a\x06\n\x04\x42\x31\x34\x38\x1a\x06\n\x04\x42\x31\x34\x39\x1a\x06\n\x04\x42\x31\x35\x30\x1a\x06\n\x04\x42\x31\x35\x31\x1a\x06\n\x04\x42\x31\x35\x32\x1a\x06\n\x04\x42\x31\x35\x33\x1a\x06\n\x04\x42\x31\x35\x34\x1a\x06\n\x04\x42\x31\x35\x35\x1a\x06\n\x04\x42\x31\x35\x36\x1a\x06\n\x04\x42\x31\x35\x37\x1a\x06\n\x04\x42\x31\x35\x38\x1a\x06\n\x04\x42\x31\x35\x39\x1a\x06\n\x04\x42\x31\x36\x30\x1a\x06\n\x04\x42\x31\x36\x31\x1a\x06\n\x04\x42\x31\x36\x32\x1a\x06\n\x04\x42\x31\x36\x33\x1a\x06\n\x04\x42\x31\x36\x34\x1a\x06\n\x04\x42\x31\x36\x35\x1a\x06\n\x04\x42\x31\x36\x36\x1a\x06\n\x04\x42\x31\x36\x37\x1a\x06\n\x04\x42\x31\x36\x38\x1a\x06\n\x04\x42\x31\x36\x39\x1a\x06\n\x04\x42\x31\x37\x30\x1a\x06\n\x04\x42\x31\x37\x31\x1a\x06\n\x04\x42\x31\x37\x32\x1a\x06\n\x04\x42\x31\x37\x33\x1a\x06\n\x04\x42\x31\x37\x34\x1a\x06\n\x04\x42\x31\x37\x35\x1a\x06\n\x04\x42\x31\x37\x36\x1a\x06\n\x04\x42\x31\x37\x37\x1a\x06\n\x04\x42\x31\x37\x38\x1a\x06\n\x04\x42\x31\x37\x39\x1a\x06\n\x04\x42\x31\x38\x30\x1a\x06\n\x04\x42\x31\x38\x31\x1a\x06\n\x04\x42\x31\x38\x32\x1a\x06\n\x04\x42\x31\x38\x33\x1a\x06\n\x04\x42\x31\x38\x34\x1a\x06\n\x04\x42\x31\x38\x35\x1a\x06\n\x04\x42\x31\x38\x36\x1a\x06\n\x04\x42\x31\x38\x37\x1a\x06\n\x04\x42\x31\x38\x38\x1a\x06\n\x04\x42\x31\x38\x39\x1a\x06\n\x04\x42\x31\x39\x30\x1a\x06\n\x04\x42\x31\x39\x31\x1a\x06\n\x04\x42\x31\x39\x32\x1a\x06\n\x04\x42\x31\x39\x33\x1a\x06\n\x04\x42\x31\x39\x34\x1a\x06\n\x04\x42\x31\x39\x35\x1a\x06\n\x04\x42\x31\x39\x36\x1a\x06\n\x04\x42\x31\x39\x37\x1a\x06\n\x04\x42\x31\x39\x38\x1a\x06\n\x04\x42\x31\x39\x39\x1a\x06\n\x04\x42\x32\x30\x30\x1a\x06\n\x04\x42\x32\x30\x31\x1a\x06\n\x04\x42\x32\x30\x32\x1a\x06\n\x04\x42\x32\x30\x33\x1a\x06\n\x04\x42\x32\x30\x34\x1a\x06\n\x04\x42\x32\x30\x35\x1a\x06\n\x04\x42\x32\x30\x36\x1a\x06\n\x04\x42\x32\x30\x37\x1a\x06\n\x04\x42\x32\x30\x38\x1a\x06\n\x04\x42\x32\x30\x39\x1a\x06\n\x04\x42\x32\x31\x30\x1a\x06\n\x04\x42\x32\x31\x31\x1a\x06\n\x04\x42\x32\x31\x32\x1a\x06\n\x04\x42\x32\x31\x33\x1a\x06\n\x04\x42\x32\x31\x34\x1a\x06\n\x04\x42\x32\x31\x35\x1a\x06\n\x04\x42\x32\x31\x36\x1a\x06\n\x04\x42\x32\x31\x37\x1a\x06\n\x04\x42\x32\x31\x38\x1a\x06\n\x04\x42\x32\x31\x39\x1a\x06\n\x04\x42\x32\x32\x30\x1a\x06\n\x04\x42\x32\x32\x31\x1a\x06\n\x04\x42\x32\x32\x32\x1a\x06\n\x04\x42\x32\x32\x33\x1a\x06\n\x04\x42\x32\x32\x34\x1a\x06\n\x04\x42\x32\x32\x35\x1a\x06\n\x04\x42\x32\x32\x36\x1a\x06\n\x04\x42\x32\x32\x37\x1a\x06\n\x04\x42\x32\x32\x38\x1a\x06\n\x04\x42\x32\x32\x39\x1a\x06\n\x04\x42\x32\x33\x30\x1a\x06\n\x04\x42\x32\x33\x31\x1a\x06\n\x04\x42\x32\x33\x32\x1a\x06\n\x04\x42\x32\x33\x33\x1a\x06\n\x04\x42\x32\x33\x34\x1a\x06\n\x04\x42\x32\x33\x35\x1a\x06\n\x04\x42\x32\x33\x36\x1a\x06\n\x04\x42\x32\x33\x37\x1a\x06\n\x04\x42\x32\x33\x38\x1a\x06\n\x04\x42\x32\x33\x39\x1a\x06\n\x04\x42\x32\x34\x30\x1a\x06\n\x04\x42\x32\x34\x31\x1a\x06\n\x04\x42\x32\x34\x32\x1a\x06\n\x04\x42\x32\x34\x33\x1a\x06\n\x04\x42\x32\x34\x34\x1a\x06\n\x04\x42\x32\x34\x35\x1a\x06\n\x04\x42\x32\x34\x36\x1a\x06\n\x04\x42\x32\x34\x37\x1a\x06\n\x04\x42\x32\x34\x38\x1a\x06\n\x04\x42\x32\x34\x39\x1a\x06\n\x04\x42\x32\x35\x30\x1a\x06\n\x04\x42\x32\x35\x31\x1a\x06\n\x04\x42\x32\x35\x32\x1a\x06\n\x04\x42\x32\x35\x33\x1a\x06\n\x04\x42\x32\x35\x34\x1a\x06\n\x04\x42\x32\x35\x35*\x1b\n\x02is\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04\x65lse\x10\x01:C\n\x0foptional_uint64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x04 \x01(\x04:B\n\x0eoptional_int64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x02 \x01(\x03:2\n\x08\x63ontinue\x12\x1f.google.protobuf.internal.class\x18\xe9\x07 \x01(\x05:2\n\x04with\x12#.google.protobuf.internal.class.try\x18\xe9\x07 \x01(\x05') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + OutOfOrderFields.RegisterExtension(optional_uint64) + OutOfOrderFields.RegisterExtension(optional_int64) + globals()['class'].RegisterExtension(globals()['continue']) + getattr(globals()['class'], 'try').RegisterExtension(globals()['with']) + globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return']) + + DESCRIPTOR._options = None + _IS._serialized_start=2669 + _IS._serialized_end=2696 + _OUTOFORDERFIELDS._serialized_start=74 + _OUTOFORDERFIELDS._serialized_end=178 + _CLASS._serialized_start=181 + _CLASS._serialized_end=514 + _CLASS_TRY._serialized_start=448 + _CLASS_TRY._serialized_end=476 + _CLASS_FOR._serialized_start=478 + _CLASS_FOR._serialized_end=506 + _EXTENDCLASS._serialized_start=516 + _EXTENDCLASS._serialized_end=579 + _TESTFULLKEYWORD._serialized_start=581 + _TESTFULLKEYWORD._serialized_end=707 + _LOTSNESTEDMESSAGE._serialized_start=710 + _LOTSNESTEDMESSAGE._serialized_end=2667 + _LOTSNESTEDMESSAGE_B0._serialized_start=731 + _LOTSNESTEDMESSAGE_B0._serialized_end=735 + _LOTSNESTEDMESSAGE_B1._serialized_start=737 + _LOTSNESTEDMESSAGE_B1._serialized_end=741 + _LOTSNESTEDMESSAGE_B2._serialized_start=743 + _LOTSNESTEDMESSAGE_B2._serialized_end=747 + _LOTSNESTEDMESSAGE_B3._serialized_start=749 + _LOTSNESTEDMESSAGE_B3._serialized_end=753 + _LOTSNESTEDMESSAGE_B4._serialized_start=755 + _LOTSNESTEDMESSAGE_B4._serialized_end=759 + _LOTSNESTEDMESSAGE_B5._serialized_start=761 + _LOTSNESTEDMESSAGE_B5._serialized_end=765 + _LOTSNESTEDMESSAGE_B6._serialized_start=767 + _LOTSNESTEDMESSAGE_B6._serialized_end=771 + _LOTSNESTEDMESSAGE_B7._serialized_start=773 + _LOTSNESTEDMESSAGE_B7._serialized_end=777 + _LOTSNESTEDMESSAGE_B8._serialized_start=779 + _LOTSNESTEDMESSAGE_B8._serialized_end=783 + _LOTSNESTEDMESSAGE_B9._serialized_start=785 + _LOTSNESTEDMESSAGE_B9._serialized_end=789 + _LOTSNESTEDMESSAGE_B10._serialized_start=791 + _LOTSNESTEDMESSAGE_B10._serialized_end=796 + _LOTSNESTEDMESSAGE_B11._serialized_start=798 + _LOTSNESTEDMESSAGE_B11._serialized_end=803 + _LOTSNESTEDMESSAGE_B12._serialized_start=805 + _LOTSNESTEDMESSAGE_B12._serialized_end=810 + _LOTSNESTEDMESSAGE_B13._serialized_start=812 + _LOTSNESTEDMESSAGE_B13._serialized_end=817 + _LOTSNESTEDMESSAGE_B14._serialized_start=819 + _LOTSNESTEDMESSAGE_B14._serialized_end=824 + _LOTSNESTEDMESSAGE_B15._serialized_start=826 + _LOTSNESTEDMESSAGE_B15._serialized_end=831 + _LOTSNESTEDMESSAGE_B16._serialized_start=833 + _LOTSNESTEDMESSAGE_B16._serialized_end=838 + _LOTSNESTEDMESSAGE_B17._serialized_start=840 + _LOTSNESTEDMESSAGE_B17._serialized_end=845 + _LOTSNESTEDMESSAGE_B18._serialized_start=847 + _LOTSNESTEDMESSAGE_B18._serialized_end=852 + _LOTSNESTEDMESSAGE_B19._serialized_start=854 + _LOTSNESTEDMESSAGE_B19._serialized_end=859 + _LOTSNESTEDMESSAGE_B20._serialized_start=861 + _LOTSNESTEDMESSAGE_B20._serialized_end=866 + _LOTSNESTEDMESSAGE_B21._serialized_start=868 + _LOTSNESTEDMESSAGE_B21._serialized_end=873 + _LOTSNESTEDMESSAGE_B22._serialized_start=875 + _LOTSNESTEDMESSAGE_B22._serialized_end=880 + _LOTSNESTEDMESSAGE_B23._serialized_start=882 + _LOTSNESTEDMESSAGE_B23._serialized_end=887 + _LOTSNESTEDMESSAGE_B24._serialized_start=889 + _LOTSNESTEDMESSAGE_B24._serialized_end=894 + _LOTSNESTEDMESSAGE_B25._serialized_start=896 + _LOTSNESTEDMESSAGE_B25._serialized_end=901 + _LOTSNESTEDMESSAGE_B26._serialized_start=903 + _LOTSNESTEDMESSAGE_B26._serialized_end=908 + _LOTSNESTEDMESSAGE_B27._serialized_start=910 + _LOTSNESTEDMESSAGE_B27._serialized_end=915 + _LOTSNESTEDMESSAGE_B28._serialized_start=917 + _LOTSNESTEDMESSAGE_B28._serialized_end=922 + _LOTSNESTEDMESSAGE_B29._serialized_start=924 + _LOTSNESTEDMESSAGE_B29._serialized_end=929 + _LOTSNESTEDMESSAGE_B30._serialized_start=931 + _LOTSNESTEDMESSAGE_B30._serialized_end=936 + _LOTSNESTEDMESSAGE_B31._serialized_start=938 + _LOTSNESTEDMESSAGE_B31._serialized_end=943 + _LOTSNESTEDMESSAGE_B32._serialized_start=945 + _LOTSNESTEDMESSAGE_B32._serialized_end=950 + _LOTSNESTEDMESSAGE_B33._serialized_start=952 + _LOTSNESTEDMESSAGE_B33._serialized_end=957 + _LOTSNESTEDMESSAGE_B34._serialized_start=959 + _LOTSNESTEDMESSAGE_B34._serialized_end=964 + _LOTSNESTEDMESSAGE_B35._serialized_start=966 + _LOTSNESTEDMESSAGE_B35._serialized_end=971 + _LOTSNESTEDMESSAGE_B36._serialized_start=973 + _LOTSNESTEDMESSAGE_B36._serialized_end=978 + _LOTSNESTEDMESSAGE_B37._serialized_start=980 + _LOTSNESTEDMESSAGE_B37._serialized_end=985 + _LOTSNESTEDMESSAGE_B38._serialized_start=987 + _LOTSNESTEDMESSAGE_B38._serialized_end=992 + _LOTSNESTEDMESSAGE_B39._serialized_start=994 + _LOTSNESTEDMESSAGE_B39._serialized_end=999 + _LOTSNESTEDMESSAGE_B40._serialized_start=1001 + _LOTSNESTEDMESSAGE_B40._serialized_end=1006 + _LOTSNESTEDMESSAGE_B41._serialized_start=1008 + _LOTSNESTEDMESSAGE_B41._serialized_end=1013 + _LOTSNESTEDMESSAGE_B42._serialized_start=1015 + _LOTSNESTEDMESSAGE_B42._serialized_end=1020 + _LOTSNESTEDMESSAGE_B43._serialized_start=1022 + _LOTSNESTEDMESSAGE_B43._serialized_end=1027 + _LOTSNESTEDMESSAGE_B44._serialized_start=1029 + _LOTSNESTEDMESSAGE_B44._serialized_end=1034 + _LOTSNESTEDMESSAGE_B45._serialized_start=1036 + _LOTSNESTEDMESSAGE_B45._serialized_end=1041 + _LOTSNESTEDMESSAGE_B46._serialized_start=1043 + _LOTSNESTEDMESSAGE_B46._serialized_end=1048 + _LOTSNESTEDMESSAGE_B47._serialized_start=1050 + _LOTSNESTEDMESSAGE_B47._serialized_end=1055 + _LOTSNESTEDMESSAGE_B48._serialized_start=1057 + _LOTSNESTEDMESSAGE_B48._serialized_end=1062 + _LOTSNESTEDMESSAGE_B49._serialized_start=1064 + _LOTSNESTEDMESSAGE_B49._serialized_end=1069 + _LOTSNESTEDMESSAGE_B50._serialized_start=1071 + _LOTSNESTEDMESSAGE_B50._serialized_end=1076 + _LOTSNESTEDMESSAGE_B51._serialized_start=1078 + _LOTSNESTEDMESSAGE_B51._serialized_end=1083 + _LOTSNESTEDMESSAGE_B52._serialized_start=1085 + _LOTSNESTEDMESSAGE_B52._serialized_end=1090 + _LOTSNESTEDMESSAGE_B53._serialized_start=1092 + _LOTSNESTEDMESSAGE_B53._serialized_end=1097 + _LOTSNESTEDMESSAGE_B54._serialized_start=1099 + _LOTSNESTEDMESSAGE_B54._serialized_end=1104 + _LOTSNESTEDMESSAGE_B55._serialized_start=1106 + _LOTSNESTEDMESSAGE_B55._serialized_end=1111 + _LOTSNESTEDMESSAGE_B56._serialized_start=1113 + _LOTSNESTEDMESSAGE_B56._serialized_end=1118 + _LOTSNESTEDMESSAGE_B57._serialized_start=1120 + _LOTSNESTEDMESSAGE_B57._serialized_end=1125 + _LOTSNESTEDMESSAGE_B58._serialized_start=1127 + _LOTSNESTEDMESSAGE_B58._serialized_end=1132 + _LOTSNESTEDMESSAGE_B59._serialized_start=1134 + _LOTSNESTEDMESSAGE_B59._serialized_end=1139 + _LOTSNESTEDMESSAGE_B60._serialized_start=1141 + _LOTSNESTEDMESSAGE_B60._serialized_end=1146 + _LOTSNESTEDMESSAGE_B61._serialized_start=1148 + _LOTSNESTEDMESSAGE_B61._serialized_end=1153 + _LOTSNESTEDMESSAGE_B62._serialized_start=1155 + _LOTSNESTEDMESSAGE_B62._serialized_end=1160 + _LOTSNESTEDMESSAGE_B63._serialized_start=1162 + _LOTSNESTEDMESSAGE_B63._serialized_end=1167 + _LOTSNESTEDMESSAGE_B64._serialized_start=1169 + _LOTSNESTEDMESSAGE_B64._serialized_end=1174 + _LOTSNESTEDMESSAGE_B65._serialized_start=1176 + _LOTSNESTEDMESSAGE_B65._serialized_end=1181 + _LOTSNESTEDMESSAGE_B66._serialized_start=1183 + _LOTSNESTEDMESSAGE_B66._serialized_end=1188 + _LOTSNESTEDMESSAGE_B67._serialized_start=1190 + _LOTSNESTEDMESSAGE_B67._serialized_end=1195 + _LOTSNESTEDMESSAGE_B68._serialized_start=1197 + _LOTSNESTEDMESSAGE_B68._serialized_end=1202 + _LOTSNESTEDMESSAGE_B69._serialized_start=1204 + _LOTSNESTEDMESSAGE_B69._serialized_end=1209 + _LOTSNESTEDMESSAGE_B70._serialized_start=1211 + _LOTSNESTEDMESSAGE_B70._serialized_end=1216 + _LOTSNESTEDMESSAGE_B71._serialized_start=1218 + _LOTSNESTEDMESSAGE_B71._serialized_end=1223 + _LOTSNESTEDMESSAGE_B72._serialized_start=1225 + _LOTSNESTEDMESSAGE_B72._serialized_end=1230 + _LOTSNESTEDMESSAGE_B73._serialized_start=1232 + _LOTSNESTEDMESSAGE_B73._serialized_end=1237 + _LOTSNESTEDMESSAGE_B74._serialized_start=1239 + _LOTSNESTEDMESSAGE_B74._serialized_end=1244 + _LOTSNESTEDMESSAGE_B75._serialized_start=1246 + _LOTSNESTEDMESSAGE_B75._serialized_end=1251 + _LOTSNESTEDMESSAGE_B76._serialized_start=1253 + _LOTSNESTEDMESSAGE_B76._serialized_end=1258 + _LOTSNESTEDMESSAGE_B77._serialized_start=1260 + _LOTSNESTEDMESSAGE_B77._serialized_end=1265 + _LOTSNESTEDMESSAGE_B78._serialized_start=1267 + _LOTSNESTEDMESSAGE_B78._serialized_end=1272 + _LOTSNESTEDMESSAGE_B79._serialized_start=1274 + _LOTSNESTEDMESSAGE_B79._serialized_end=1279 + _LOTSNESTEDMESSAGE_B80._serialized_start=1281 + _LOTSNESTEDMESSAGE_B80._serialized_end=1286 + _LOTSNESTEDMESSAGE_B81._serialized_start=1288 + _LOTSNESTEDMESSAGE_B81._serialized_end=1293 + _LOTSNESTEDMESSAGE_B82._serialized_start=1295 + _LOTSNESTEDMESSAGE_B82._serialized_end=1300 + _LOTSNESTEDMESSAGE_B83._serialized_start=1302 + _LOTSNESTEDMESSAGE_B83._serialized_end=1307 + _LOTSNESTEDMESSAGE_B84._serialized_start=1309 + _LOTSNESTEDMESSAGE_B84._serialized_end=1314 + _LOTSNESTEDMESSAGE_B85._serialized_start=1316 + _LOTSNESTEDMESSAGE_B85._serialized_end=1321 + _LOTSNESTEDMESSAGE_B86._serialized_start=1323 + _LOTSNESTEDMESSAGE_B86._serialized_end=1328 + _LOTSNESTEDMESSAGE_B87._serialized_start=1330 + _LOTSNESTEDMESSAGE_B87._serialized_end=1335 + _LOTSNESTEDMESSAGE_B88._serialized_start=1337 + _LOTSNESTEDMESSAGE_B88._serialized_end=1342 + _LOTSNESTEDMESSAGE_B89._serialized_start=1344 + _LOTSNESTEDMESSAGE_B89._serialized_end=1349 + _LOTSNESTEDMESSAGE_B90._serialized_start=1351 + _LOTSNESTEDMESSAGE_B90._serialized_end=1356 + _LOTSNESTEDMESSAGE_B91._serialized_start=1358 + _LOTSNESTEDMESSAGE_B91._serialized_end=1363 + _LOTSNESTEDMESSAGE_B92._serialized_start=1365 + _LOTSNESTEDMESSAGE_B92._serialized_end=1370 + _LOTSNESTEDMESSAGE_B93._serialized_start=1372 + _LOTSNESTEDMESSAGE_B93._serialized_end=1377 + _LOTSNESTEDMESSAGE_B94._serialized_start=1379 + _LOTSNESTEDMESSAGE_B94._serialized_end=1384 + _LOTSNESTEDMESSAGE_B95._serialized_start=1386 + _LOTSNESTEDMESSAGE_B95._serialized_end=1391 + _LOTSNESTEDMESSAGE_B96._serialized_start=1393 + _LOTSNESTEDMESSAGE_B96._serialized_end=1398 + _LOTSNESTEDMESSAGE_B97._serialized_start=1400 + _LOTSNESTEDMESSAGE_B97._serialized_end=1405 + _LOTSNESTEDMESSAGE_B98._serialized_start=1407 + _LOTSNESTEDMESSAGE_B98._serialized_end=1412 + _LOTSNESTEDMESSAGE_B99._serialized_start=1414 + _LOTSNESTEDMESSAGE_B99._serialized_end=1419 + _LOTSNESTEDMESSAGE_B100._serialized_start=1421 + _LOTSNESTEDMESSAGE_B100._serialized_end=1427 + _LOTSNESTEDMESSAGE_B101._serialized_start=1429 + _LOTSNESTEDMESSAGE_B101._serialized_end=1435 + _LOTSNESTEDMESSAGE_B102._serialized_start=1437 + _LOTSNESTEDMESSAGE_B102._serialized_end=1443 + _LOTSNESTEDMESSAGE_B103._serialized_start=1445 + _LOTSNESTEDMESSAGE_B103._serialized_end=1451 + _LOTSNESTEDMESSAGE_B104._serialized_start=1453 + _LOTSNESTEDMESSAGE_B104._serialized_end=1459 + _LOTSNESTEDMESSAGE_B105._serialized_start=1461 + _LOTSNESTEDMESSAGE_B105._serialized_end=1467 + _LOTSNESTEDMESSAGE_B106._serialized_start=1469 + _LOTSNESTEDMESSAGE_B106._serialized_end=1475 + _LOTSNESTEDMESSAGE_B107._serialized_start=1477 + _LOTSNESTEDMESSAGE_B107._serialized_end=1483 + _LOTSNESTEDMESSAGE_B108._serialized_start=1485 + _LOTSNESTEDMESSAGE_B108._serialized_end=1491 + _LOTSNESTEDMESSAGE_B109._serialized_start=1493 + _LOTSNESTEDMESSAGE_B109._serialized_end=1499 + _LOTSNESTEDMESSAGE_B110._serialized_start=1501 + _LOTSNESTEDMESSAGE_B110._serialized_end=1507 + _LOTSNESTEDMESSAGE_B111._serialized_start=1509 + _LOTSNESTEDMESSAGE_B111._serialized_end=1515 + _LOTSNESTEDMESSAGE_B112._serialized_start=1517 + _LOTSNESTEDMESSAGE_B112._serialized_end=1523 + _LOTSNESTEDMESSAGE_B113._serialized_start=1525 + _LOTSNESTEDMESSAGE_B113._serialized_end=1531 + _LOTSNESTEDMESSAGE_B114._serialized_start=1533 + _LOTSNESTEDMESSAGE_B114._serialized_end=1539 + _LOTSNESTEDMESSAGE_B115._serialized_start=1541 + _LOTSNESTEDMESSAGE_B115._serialized_end=1547 + _LOTSNESTEDMESSAGE_B116._serialized_start=1549 + _LOTSNESTEDMESSAGE_B116._serialized_end=1555 + _LOTSNESTEDMESSAGE_B117._serialized_start=1557 + _LOTSNESTEDMESSAGE_B117._serialized_end=1563 + _LOTSNESTEDMESSAGE_B118._serialized_start=1565 + _LOTSNESTEDMESSAGE_B118._serialized_end=1571 + _LOTSNESTEDMESSAGE_B119._serialized_start=1573 + _LOTSNESTEDMESSAGE_B119._serialized_end=1579 + _LOTSNESTEDMESSAGE_B120._serialized_start=1581 + _LOTSNESTEDMESSAGE_B120._serialized_end=1587 + _LOTSNESTEDMESSAGE_B121._serialized_start=1589 + _LOTSNESTEDMESSAGE_B121._serialized_end=1595 + _LOTSNESTEDMESSAGE_B122._serialized_start=1597 + _LOTSNESTEDMESSAGE_B122._serialized_end=1603 + _LOTSNESTEDMESSAGE_B123._serialized_start=1605 + _LOTSNESTEDMESSAGE_B123._serialized_end=1611 + _LOTSNESTEDMESSAGE_B124._serialized_start=1613 + _LOTSNESTEDMESSAGE_B124._serialized_end=1619 + _LOTSNESTEDMESSAGE_B125._serialized_start=1621 + _LOTSNESTEDMESSAGE_B125._serialized_end=1627 + _LOTSNESTEDMESSAGE_B126._serialized_start=1629 + _LOTSNESTEDMESSAGE_B126._serialized_end=1635 + _LOTSNESTEDMESSAGE_B127._serialized_start=1637 + _LOTSNESTEDMESSAGE_B127._serialized_end=1643 + _LOTSNESTEDMESSAGE_B128._serialized_start=1645 + _LOTSNESTEDMESSAGE_B128._serialized_end=1651 + _LOTSNESTEDMESSAGE_B129._serialized_start=1653 + _LOTSNESTEDMESSAGE_B129._serialized_end=1659 + _LOTSNESTEDMESSAGE_B130._serialized_start=1661 + _LOTSNESTEDMESSAGE_B130._serialized_end=1667 + _LOTSNESTEDMESSAGE_B131._serialized_start=1669 + _LOTSNESTEDMESSAGE_B131._serialized_end=1675 + _LOTSNESTEDMESSAGE_B132._serialized_start=1677 + _LOTSNESTEDMESSAGE_B132._serialized_end=1683 + _LOTSNESTEDMESSAGE_B133._serialized_start=1685 + _LOTSNESTEDMESSAGE_B133._serialized_end=1691 + _LOTSNESTEDMESSAGE_B134._serialized_start=1693 + _LOTSNESTEDMESSAGE_B134._serialized_end=1699 + _LOTSNESTEDMESSAGE_B135._serialized_start=1701 + _LOTSNESTEDMESSAGE_B135._serialized_end=1707 + _LOTSNESTEDMESSAGE_B136._serialized_start=1709 + _LOTSNESTEDMESSAGE_B136._serialized_end=1715 + _LOTSNESTEDMESSAGE_B137._serialized_start=1717 + _LOTSNESTEDMESSAGE_B137._serialized_end=1723 + _LOTSNESTEDMESSAGE_B138._serialized_start=1725 + _LOTSNESTEDMESSAGE_B138._serialized_end=1731 + _LOTSNESTEDMESSAGE_B139._serialized_start=1733 + _LOTSNESTEDMESSAGE_B139._serialized_end=1739 + _LOTSNESTEDMESSAGE_B140._serialized_start=1741 + _LOTSNESTEDMESSAGE_B140._serialized_end=1747 + _LOTSNESTEDMESSAGE_B141._serialized_start=1749 + _LOTSNESTEDMESSAGE_B141._serialized_end=1755 + _LOTSNESTEDMESSAGE_B142._serialized_start=1757 + _LOTSNESTEDMESSAGE_B142._serialized_end=1763 + _LOTSNESTEDMESSAGE_B143._serialized_start=1765 + _LOTSNESTEDMESSAGE_B143._serialized_end=1771 + _LOTSNESTEDMESSAGE_B144._serialized_start=1773 + _LOTSNESTEDMESSAGE_B144._serialized_end=1779 + _LOTSNESTEDMESSAGE_B145._serialized_start=1781 + _LOTSNESTEDMESSAGE_B145._serialized_end=1787 + _LOTSNESTEDMESSAGE_B146._serialized_start=1789 + _LOTSNESTEDMESSAGE_B146._serialized_end=1795 + _LOTSNESTEDMESSAGE_B147._serialized_start=1797 + _LOTSNESTEDMESSAGE_B147._serialized_end=1803 + _LOTSNESTEDMESSAGE_B148._serialized_start=1805 + _LOTSNESTEDMESSAGE_B148._serialized_end=1811 + _LOTSNESTEDMESSAGE_B149._serialized_start=1813 + _LOTSNESTEDMESSAGE_B149._serialized_end=1819 + _LOTSNESTEDMESSAGE_B150._serialized_start=1821 + _LOTSNESTEDMESSAGE_B150._serialized_end=1827 + _LOTSNESTEDMESSAGE_B151._serialized_start=1829 + _LOTSNESTEDMESSAGE_B151._serialized_end=1835 + _LOTSNESTEDMESSAGE_B152._serialized_start=1837 + _LOTSNESTEDMESSAGE_B152._serialized_end=1843 + _LOTSNESTEDMESSAGE_B153._serialized_start=1845 + _LOTSNESTEDMESSAGE_B153._serialized_end=1851 + _LOTSNESTEDMESSAGE_B154._serialized_start=1853 + _LOTSNESTEDMESSAGE_B154._serialized_end=1859 + _LOTSNESTEDMESSAGE_B155._serialized_start=1861 + _LOTSNESTEDMESSAGE_B155._serialized_end=1867 + _LOTSNESTEDMESSAGE_B156._serialized_start=1869 + _LOTSNESTEDMESSAGE_B156._serialized_end=1875 + _LOTSNESTEDMESSAGE_B157._serialized_start=1877 + _LOTSNESTEDMESSAGE_B157._serialized_end=1883 + _LOTSNESTEDMESSAGE_B158._serialized_start=1885 + _LOTSNESTEDMESSAGE_B158._serialized_end=1891 + _LOTSNESTEDMESSAGE_B159._serialized_start=1893 + _LOTSNESTEDMESSAGE_B159._serialized_end=1899 + _LOTSNESTEDMESSAGE_B160._serialized_start=1901 + _LOTSNESTEDMESSAGE_B160._serialized_end=1907 + _LOTSNESTEDMESSAGE_B161._serialized_start=1909 + _LOTSNESTEDMESSAGE_B161._serialized_end=1915 + _LOTSNESTEDMESSAGE_B162._serialized_start=1917 + _LOTSNESTEDMESSAGE_B162._serialized_end=1923 + _LOTSNESTEDMESSAGE_B163._serialized_start=1925 + _LOTSNESTEDMESSAGE_B163._serialized_end=1931 + _LOTSNESTEDMESSAGE_B164._serialized_start=1933 + _LOTSNESTEDMESSAGE_B164._serialized_end=1939 + _LOTSNESTEDMESSAGE_B165._serialized_start=1941 + _LOTSNESTEDMESSAGE_B165._serialized_end=1947 + _LOTSNESTEDMESSAGE_B166._serialized_start=1949 + _LOTSNESTEDMESSAGE_B166._serialized_end=1955 + _LOTSNESTEDMESSAGE_B167._serialized_start=1957 + _LOTSNESTEDMESSAGE_B167._serialized_end=1963 + _LOTSNESTEDMESSAGE_B168._serialized_start=1965 + _LOTSNESTEDMESSAGE_B168._serialized_end=1971 + _LOTSNESTEDMESSAGE_B169._serialized_start=1973 + _LOTSNESTEDMESSAGE_B169._serialized_end=1979 + _LOTSNESTEDMESSAGE_B170._serialized_start=1981 + _LOTSNESTEDMESSAGE_B170._serialized_end=1987 + _LOTSNESTEDMESSAGE_B171._serialized_start=1989 + _LOTSNESTEDMESSAGE_B171._serialized_end=1995 + _LOTSNESTEDMESSAGE_B172._serialized_start=1997 + _LOTSNESTEDMESSAGE_B172._serialized_end=2003 + _LOTSNESTEDMESSAGE_B173._serialized_start=2005 + _LOTSNESTEDMESSAGE_B173._serialized_end=2011 + _LOTSNESTEDMESSAGE_B174._serialized_start=2013 + _LOTSNESTEDMESSAGE_B174._serialized_end=2019 + _LOTSNESTEDMESSAGE_B175._serialized_start=2021 + _LOTSNESTEDMESSAGE_B175._serialized_end=2027 + _LOTSNESTEDMESSAGE_B176._serialized_start=2029 + _LOTSNESTEDMESSAGE_B176._serialized_end=2035 + _LOTSNESTEDMESSAGE_B177._serialized_start=2037 + _LOTSNESTEDMESSAGE_B177._serialized_end=2043 + _LOTSNESTEDMESSAGE_B178._serialized_start=2045 + _LOTSNESTEDMESSAGE_B178._serialized_end=2051 + _LOTSNESTEDMESSAGE_B179._serialized_start=2053 + _LOTSNESTEDMESSAGE_B179._serialized_end=2059 + _LOTSNESTEDMESSAGE_B180._serialized_start=2061 + _LOTSNESTEDMESSAGE_B180._serialized_end=2067 + _LOTSNESTEDMESSAGE_B181._serialized_start=2069 + _LOTSNESTEDMESSAGE_B181._serialized_end=2075 + _LOTSNESTEDMESSAGE_B182._serialized_start=2077 + _LOTSNESTEDMESSAGE_B182._serialized_end=2083 + _LOTSNESTEDMESSAGE_B183._serialized_start=2085 + _LOTSNESTEDMESSAGE_B183._serialized_end=2091 + _LOTSNESTEDMESSAGE_B184._serialized_start=2093 + _LOTSNESTEDMESSAGE_B184._serialized_end=2099 + _LOTSNESTEDMESSAGE_B185._serialized_start=2101 + _LOTSNESTEDMESSAGE_B185._serialized_end=2107 + _LOTSNESTEDMESSAGE_B186._serialized_start=2109 + _LOTSNESTEDMESSAGE_B186._serialized_end=2115 + _LOTSNESTEDMESSAGE_B187._serialized_start=2117 + _LOTSNESTEDMESSAGE_B187._serialized_end=2123 + _LOTSNESTEDMESSAGE_B188._serialized_start=2125 + _LOTSNESTEDMESSAGE_B188._serialized_end=2131 + _LOTSNESTEDMESSAGE_B189._serialized_start=2133 + _LOTSNESTEDMESSAGE_B189._serialized_end=2139 + _LOTSNESTEDMESSAGE_B190._serialized_start=2141 + _LOTSNESTEDMESSAGE_B190._serialized_end=2147 + _LOTSNESTEDMESSAGE_B191._serialized_start=2149 + _LOTSNESTEDMESSAGE_B191._serialized_end=2155 + _LOTSNESTEDMESSAGE_B192._serialized_start=2157 + _LOTSNESTEDMESSAGE_B192._serialized_end=2163 + _LOTSNESTEDMESSAGE_B193._serialized_start=2165 + _LOTSNESTEDMESSAGE_B193._serialized_end=2171 + _LOTSNESTEDMESSAGE_B194._serialized_start=2173 + _LOTSNESTEDMESSAGE_B194._serialized_end=2179 + _LOTSNESTEDMESSAGE_B195._serialized_start=2181 + _LOTSNESTEDMESSAGE_B195._serialized_end=2187 + _LOTSNESTEDMESSAGE_B196._serialized_start=2189 + _LOTSNESTEDMESSAGE_B196._serialized_end=2195 + _LOTSNESTEDMESSAGE_B197._serialized_start=2197 + _LOTSNESTEDMESSAGE_B197._serialized_end=2203 + _LOTSNESTEDMESSAGE_B198._serialized_start=2205 + _LOTSNESTEDMESSAGE_B198._serialized_end=2211 + _LOTSNESTEDMESSAGE_B199._serialized_start=2213 + _LOTSNESTEDMESSAGE_B199._serialized_end=2219 + _LOTSNESTEDMESSAGE_B200._serialized_start=2221 + _LOTSNESTEDMESSAGE_B200._serialized_end=2227 + _LOTSNESTEDMESSAGE_B201._serialized_start=2229 + _LOTSNESTEDMESSAGE_B201._serialized_end=2235 + _LOTSNESTEDMESSAGE_B202._serialized_start=2237 + _LOTSNESTEDMESSAGE_B202._serialized_end=2243 + _LOTSNESTEDMESSAGE_B203._serialized_start=2245 + _LOTSNESTEDMESSAGE_B203._serialized_end=2251 + _LOTSNESTEDMESSAGE_B204._serialized_start=2253 + _LOTSNESTEDMESSAGE_B204._serialized_end=2259 + _LOTSNESTEDMESSAGE_B205._serialized_start=2261 + _LOTSNESTEDMESSAGE_B205._serialized_end=2267 + _LOTSNESTEDMESSAGE_B206._serialized_start=2269 + _LOTSNESTEDMESSAGE_B206._serialized_end=2275 + _LOTSNESTEDMESSAGE_B207._serialized_start=2277 + _LOTSNESTEDMESSAGE_B207._serialized_end=2283 + _LOTSNESTEDMESSAGE_B208._serialized_start=2285 + _LOTSNESTEDMESSAGE_B208._serialized_end=2291 + _LOTSNESTEDMESSAGE_B209._serialized_start=2293 + _LOTSNESTEDMESSAGE_B209._serialized_end=2299 + _LOTSNESTEDMESSAGE_B210._serialized_start=2301 + _LOTSNESTEDMESSAGE_B210._serialized_end=2307 + _LOTSNESTEDMESSAGE_B211._serialized_start=2309 + _LOTSNESTEDMESSAGE_B211._serialized_end=2315 + _LOTSNESTEDMESSAGE_B212._serialized_start=2317 + _LOTSNESTEDMESSAGE_B212._serialized_end=2323 + _LOTSNESTEDMESSAGE_B213._serialized_start=2325 + _LOTSNESTEDMESSAGE_B213._serialized_end=2331 + _LOTSNESTEDMESSAGE_B214._serialized_start=2333 + _LOTSNESTEDMESSAGE_B214._serialized_end=2339 + _LOTSNESTEDMESSAGE_B215._serialized_start=2341 + _LOTSNESTEDMESSAGE_B215._serialized_end=2347 + _LOTSNESTEDMESSAGE_B216._serialized_start=2349 + _LOTSNESTEDMESSAGE_B216._serialized_end=2355 + _LOTSNESTEDMESSAGE_B217._serialized_start=2357 + _LOTSNESTEDMESSAGE_B217._serialized_end=2363 + _LOTSNESTEDMESSAGE_B218._serialized_start=2365 + _LOTSNESTEDMESSAGE_B218._serialized_end=2371 + _LOTSNESTEDMESSAGE_B219._serialized_start=2373 + _LOTSNESTEDMESSAGE_B219._serialized_end=2379 + _LOTSNESTEDMESSAGE_B220._serialized_start=2381 + _LOTSNESTEDMESSAGE_B220._serialized_end=2387 + _LOTSNESTEDMESSAGE_B221._serialized_start=2389 + _LOTSNESTEDMESSAGE_B221._serialized_end=2395 + _LOTSNESTEDMESSAGE_B222._serialized_start=2397 + _LOTSNESTEDMESSAGE_B222._serialized_end=2403 + _LOTSNESTEDMESSAGE_B223._serialized_start=2405 + _LOTSNESTEDMESSAGE_B223._serialized_end=2411 + _LOTSNESTEDMESSAGE_B224._serialized_start=2413 + _LOTSNESTEDMESSAGE_B224._serialized_end=2419 + _LOTSNESTEDMESSAGE_B225._serialized_start=2421 + _LOTSNESTEDMESSAGE_B225._serialized_end=2427 + _LOTSNESTEDMESSAGE_B226._serialized_start=2429 + _LOTSNESTEDMESSAGE_B226._serialized_end=2435 + _LOTSNESTEDMESSAGE_B227._serialized_start=2437 + _LOTSNESTEDMESSAGE_B227._serialized_end=2443 + _LOTSNESTEDMESSAGE_B228._serialized_start=2445 + _LOTSNESTEDMESSAGE_B228._serialized_end=2451 + _LOTSNESTEDMESSAGE_B229._serialized_start=2453 + _LOTSNESTEDMESSAGE_B229._serialized_end=2459 + _LOTSNESTEDMESSAGE_B230._serialized_start=2461 + _LOTSNESTEDMESSAGE_B230._serialized_end=2467 + _LOTSNESTEDMESSAGE_B231._serialized_start=2469 + _LOTSNESTEDMESSAGE_B231._serialized_end=2475 + _LOTSNESTEDMESSAGE_B232._serialized_start=2477 + _LOTSNESTEDMESSAGE_B232._serialized_end=2483 + _LOTSNESTEDMESSAGE_B233._serialized_start=2485 + _LOTSNESTEDMESSAGE_B233._serialized_end=2491 + _LOTSNESTEDMESSAGE_B234._serialized_start=2493 + _LOTSNESTEDMESSAGE_B234._serialized_end=2499 + _LOTSNESTEDMESSAGE_B235._serialized_start=2501 + _LOTSNESTEDMESSAGE_B235._serialized_end=2507 + _LOTSNESTEDMESSAGE_B236._serialized_start=2509 + _LOTSNESTEDMESSAGE_B236._serialized_end=2515 + _LOTSNESTEDMESSAGE_B237._serialized_start=2517 + _LOTSNESTEDMESSAGE_B237._serialized_end=2523 + _LOTSNESTEDMESSAGE_B238._serialized_start=2525 + _LOTSNESTEDMESSAGE_B238._serialized_end=2531 + _LOTSNESTEDMESSAGE_B239._serialized_start=2533 + _LOTSNESTEDMESSAGE_B239._serialized_end=2539 + _LOTSNESTEDMESSAGE_B240._serialized_start=2541 + _LOTSNESTEDMESSAGE_B240._serialized_end=2547 + _LOTSNESTEDMESSAGE_B241._serialized_start=2549 + _LOTSNESTEDMESSAGE_B241._serialized_end=2555 + _LOTSNESTEDMESSAGE_B242._serialized_start=2557 + _LOTSNESTEDMESSAGE_B242._serialized_end=2563 + _LOTSNESTEDMESSAGE_B243._serialized_start=2565 + _LOTSNESTEDMESSAGE_B243._serialized_end=2571 + _LOTSNESTEDMESSAGE_B244._serialized_start=2573 + _LOTSNESTEDMESSAGE_B244._serialized_end=2579 + _LOTSNESTEDMESSAGE_B245._serialized_start=2581 + _LOTSNESTEDMESSAGE_B245._serialized_end=2587 + _LOTSNESTEDMESSAGE_B246._serialized_start=2589 + _LOTSNESTEDMESSAGE_B246._serialized_end=2595 + _LOTSNESTEDMESSAGE_B247._serialized_start=2597 + _LOTSNESTEDMESSAGE_B247._serialized_end=2603 + _LOTSNESTEDMESSAGE_B248._serialized_start=2605 + _LOTSNESTEDMESSAGE_B248._serialized_end=2611 + _LOTSNESTEDMESSAGE_B249._serialized_start=2613 + _LOTSNESTEDMESSAGE_B249._serialized_end=2619 + _LOTSNESTEDMESSAGE_B250._serialized_start=2621 + _LOTSNESTEDMESSAGE_B250._serialized_end=2627 + _LOTSNESTEDMESSAGE_B251._serialized_start=2629 + _LOTSNESTEDMESSAGE_B251._serialized_end=2635 + _LOTSNESTEDMESSAGE_B252._serialized_start=2637 + _LOTSNESTEDMESSAGE_B252._serialized_end=2643 + _LOTSNESTEDMESSAGE_B253._serialized_start=2645 + _LOTSNESTEDMESSAGE_B253._serialized_end=2651 + _LOTSNESTEDMESSAGE_B254._serialized_start=2653 + _LOTSNESTEDMESSAGE_B254._serialized_end=2659 + _LOTSNESTEDMESSAGE_B255._serialized_start=2661 + _LOTSNESTEDMESSAGE_B255._serialized_end=2667 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py new file mode 100644 index 0000000000..d46dee080a --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/no_package.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)google/protobuf/internal/no_package.proto\";\n\x10NoPackageMessage\x12\'\n\x0fno_package_enum\x18\x01 \x01(\x0e\x32\x0e.NoPackageEnum*?\n\rNoPackageEnum\x12\x16\n\x12NO_PACKAGE_VALUE_0\x10\x00\x12\x16\n\x12NO_PACKAGE_VALUE_1\x10\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _NOPACKAGEENUM._serialized_start=106 + _NOPACKAGEENUM._serialized_end=169 + _NOPACKAGEMESSAGE._serialized_start=45 + _NOPACKAGEMESSAGE._serialized_end=104 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py new file mode 100644 index 0000000000..2921d5cb6e --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py @@ -0,0 +1,1539 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. +# +# TODO(robinson): Helpers for verbose, common checks like seeing if a +# descriptor's cpp_type is CPPTYPE_MESSAGE. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +from io import BytesIO +import struct +import sys +import weakref + +# We use "as" to avoid name collisions with variables. +from google.protobuf.internal import api_implementation +from google.protobuf.internal import containers +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import extension_dict +from google.protobuf.internal import message_listener as message_listener_mod +from google.protobuf.internal import type_checkers +from google.protobuf.internal import well_known_types +from google.protobuf.internal import wire_format +from google.protobuf import descriptor as descriptor_mod +from google.protobuf import message as message_mod +from google.protobuf import text_format + +_FieldDescriptor = descriptor_mod.FieldDescriptor +_AnyFullTypeName = 'google.protobuf.Any' +_ExtensionDict = extension_dict._ExtensionDict + +class GeneratedProtocolMessageType(type): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + We add implementations for all methods described in the Message class. We + also create properties to allow getting/setting all fields in the protocol + message. Finally, we create slots to prevent users from accidentally + "setting" nonexistent fields in the protocol message, which then wouldn't get + serialized / deserialized properly. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __new__(cls, name, bases, dictionary): + """Custom allocation for runtime-generated class types. + + We override __new__ because this is apparently the only place + where we can meaningfully set __slots__ on the class we're creating(?). + (The interplay between metaclasses and slots is not very well-documented). + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + + Returns: + Newly-allocated class. + + Raises: + RuntimeError: Generated code only work with python cpp extension. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + if isinstance(descriptor, str): + raise RuntimeError('The generated code only work with python cpp ' + 'extension, but it is using pure python runtime.') + + # If a concrete class already exists for this descriptor, don't try to + # create another. Doing so will break any messages that already exist with + # the existing class. + # + # The C++ implementation appears to have its own internal `PyMessageFactory` + # to achieve similar results. + # + # This most commonly happens in `text_format.py` when using descriptors from + # a custom pool; it calls symbol_database.Global().getPrototype() on a + # descriptor which already has an existing concrete class. + new_class = getattr(descriptor, '_concrete_class', None) + if new_class: + return new_class + + if descriptor.full_name in well_known_types.WKTBASES: + bases += (well_known_types.WKTBASES[descriptor.full_name],) + _AddClassAttributesForNestedExtensions(descriptor, dictionary) + _AddSlots(descriptor, dictionary) + + superclass = super(GeneratedProtocolMessageType, cls) + new_class = superclass.__new__(cls, name, bases, dictionary) + return new_class + + def __init__(cls, name, bases, dictionary): + """Here we perform the majority of our work on the class. + We add enum getters, an __init__ method, implementations + of all Message methods, and properties for all fields + in the protocol type. + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + # If this is an _existing_ class looked up via `_concrete_class` in the + # __new__ method above, then we don't need to re-initialize anything. + existing_class = getattr(descriptor, '_concrete_class', None) + if existing_class: + assert existing_class is cls, ( + 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' + % (descriptor.full_name)) + return + + cls._decoders_by_tag = {} + if (descriptor.has_options and + descriptor.GetOptions().message_set_wire_format): + cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( + decoder.MessageSetItemDecoder(descriptor), None) + + # Attach stuff to each FieldDescriptor for quick lookup later on. + for field in descriptor.fields: + _AttachFieldHelpers(cls, field) + + descriptor._concrete_class = cls # pylint: disable=protected-access + _AddEnumValues(descriptor, cls) + _AddInitMethod(descriptor, cls) + _AddPropertiesForFields(descriptor, cls) + _AddPropertiesForExtensions(descriptor, cls) + _AddStaticMethods(cls) + _AddMessageMethods(descriptor, cls) + _AddPrivateHelperMethods(descriptor, cls) + + superclass = super(GeneratedProtocolMessageType, cls) + superclass.__init__(name, bases, dictionary) + + +# Stateless helpers for GeneratedProtocolMessageType below. +# Outside clients should not access these directly. +# +# I opted not to make any of these methods on the metaclass, to make it more +# clear that I'm not really using any state there and to keep clients from +# thinking that they have direct access to these construction helpers. + + +def _PropertyName(proto_field_name): + """Returns the name of the public property attribute which + clients can use to get and (in some cases) set the value + of a protocol message field. + + Args: + proto_field_name: The protocol message field name, exactly + as it appears (or would appear) in a .proto file. + """ + # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. + # nnorwitz makes my day by writing: + # """ + # FYI. See the keyword module in the stdlib. This could be as simple as: + # + # if keyword.iskeyword(proto_field_name): + # return proto_field_name + "_" + # return proto_field_name + # """ + # Kenton says: The above is a BAD IDEA. People rely on being able to use + # getattr() and setattr() to reflectively manipulate field values. If we + # rename the properties, then every such user has to also make sure to apply + # the same transformation. Note that currently if you name a field "yield", + # you can still access it just fine using getattr/setattr -- it's not even + # that cumbersome to do so. + # TODO(kenton): Remove this method entirely if/when everyone agrees with my + # position. + return proto_field_name + + +def _AddSlots(message_descriptor, dictionary): + """Adds a __slots__ entry to dictionary, containing the names of all valid + attributes for this message type. + + Args: + message_descriptor: A Descriptor instance describing this message type. + dictionary: Class dictionary to which we'll add a '__slots__' entry. + """ + dictionary['__slots__'] = ['_cached_byte_size', + '_cached_byte_size_dirty', + '_fields', + '_unknown_fields', + '_unknown_field_set', + '_is_present_in_parent', + '_listener', + '_listener_for_children', + '__weakref__', + '_oneofs'] + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == _FieldDescriptor.TYPE_MESSAGE and + field.label == _FieldDescriptor.LABEL_OPTIONAL) + + +def _IsMapField(field): + return (field.type == _FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def _IsMessageMapField(field): + value_type = field.message_type.fields_by_name['value'] + return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE + + +def _AttachFieldHelpers(cls, field_descriptor): + is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) + is_packable = (is_repeated and + wire_format.IsTypePackable(field_descriptor.type)) + is_proto3 = field_descriptor.containing_type.syntax == 'proto3' + if not is_packable: + is_packed = False + elif field_descriptor.containing_type.syntax == 'proto2': + is_packed = (field_descriptor.has_options and + field_descriptor.GetOptions().packed) + else: + has_packed_false = (field_descriptor.has_options and + field_descriptor.GetOptions().HasField('packed') and + field_descriptor.GetOptions().packed == False) + is_packed = not has_packed_false + is_map_entry = _IsMapField(field_descriptor) + + if is_map_entry: + field_encoder = encoder.MapEncoder(field_descriptor) + sizer = encoder.MapSizer(field_descriptor, + _IsMessageMapField(field_descriptor)) + elif _IsMessageSetExtension(field_descriptor): + field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) + sizer = encoder.MessageSetItemSizer(field_descriptor.number) + else: + field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + + field_descriptor._encoder = field_encoder + field_descriptor._sizer = sizer + field_descriptor._default_constructor = _DefaultValueConstructorForField( + field_descriptor) + + def AddDecoder(wiretype, is_packed): + tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) + decode_type = field_descriptor.type + if (decode_type == _FieldDescriptor.TYPE_ENUM and + type_checkers.SupportsOpenEnums(field_descriptor)): + decode_type = _FieldDescriptor.TYPE_INT32 + + oneof_descriptor = None + clear_if_default = False + if field_descriptor.containing_oneof is not None: + oneof_descriptor = field_descriptor + elif (is_proto3 and not is_repeated and + field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE): + clear_if_default = True + + if is_map_entry: + is_message_map = _IsMessageMapField(field_descriptor) + + field_decoder = decoder.MapDecoder( + field_descriptor, _GetInitializeDefaultForMap(field_descriptor), + is_message_map) + elif decode_type == _FieldDescriptor.TYPE_STRING: + field_decoder = decoder.StringDecoder( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor) + else: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + # pylint: disable=protected-access + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + + cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) + + AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], + False) + + if is_repeated and wire_format.IsTypePackable(field_descriptor.type): + # To support wire compatibility of adding packed = true, add a decoder for + # packed values regardless of the field's options. + AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) + + +def _AddClassAttributesForNestedExtensions(descriptor, dictionary): + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + assert extension_name not in dictionary + dictionary[extension_name] = extension_field + + +def _AddEnumValues(descriptor, cls): + """Sets class-level attributes for all enum fields defined in this message. + + Also exporting a class-level object that can name enum values. + + Args: + descriptor: Descriptor object for this message type. + cls: Class we're constructing for this message type. + """ + for enum_type in descriptor.enum_types: + setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) + for enum_value in enum_type.values: + setattr(cls, enum_value.name, enum_value.number) + + +def _GetInitializeDefaultForMap(field): + if field.label != _FieldDescriptor.LABEL_REPEATED: + raise ValueError('map_entry set on non-repeated field %s' % ( + field.name)) + fields_by_name = field.message_type.fields_by_name + key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) + + value_field = fields_by_name['value'] + if _IsMessageMapField(field): + def MakeMessageMapDefault(message): + return containers.MessageMap( + message._listener_for_children, value_field.message_type, key_checker, + field.message_type) + return MakeMessageMapDefault + else: + value_checker = type_checkers.GetTypeChecker(value_field) + def MakePrimitiveMapDefault(message): + return containers.ScalarMap( + message._listener_for_children, key_checker, value_checker, + field.message_type) + return MakePrimitiveMapDefault + +def _DefaultValueConstructorForField(field): + """Returns a function which returns a default value for a field. + + Args: + field: FieldDescriptor object for this field. + + The returned function has one argument: + message: Message instance containing this field, or a weakref proxy + of same. + + That function in turn returns a default value for this field. The default + value may refer back to |message| via a weak reference. + """ + + if _IsMapField(field): + return _GetInitializeDefaultForMap(field) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + if field.has_default_value and field.default_value != []: + raise ValueError('Repeated field default value not empty list: %s' % ( + field.default_value)) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # We can't look at _concrete_class yet since it might not have + # been set. (Depends on order in which we initialize the classes). + message_type = field.message_type + def MakeRepeatedMessageDefault(message): + return containers.RepeatedCompositeFieldContainer( + message._listener_for_children, field.message_type) + return MakeRepeatedMessageDefault + else: + type_checker = type_checkers.GetTypeChecker(field) + def MakeRepeatedScalarDefault(message): + return containers.RepeatedScalarFieldContainer( + message._listener_for_children, type_checker) + return MakeRepeatedScalarDefault + + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # _concrete_class may not yet be initialized. + message_type = field.message_type + def MakeSubMessageDefault(message): + assert getattr(message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (field.full_name, message_type.full_name)) + result = message_type._concrete_class() + result._SetListener( + _OneofListener(message, field) + if field.containing_oneof is not None + else message._listener_for_children) + return result + return MakeSubMessageDefault + + def MakeScalarDefault(message): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return field.default_value + return MakeScalarDefault + + +def _ReraiseTypeErrorWithFieldName(message_name, field_name): + """Re-raise the currently-handled TypeError with the field name added.""" + exc = sys.exc_info()[1] + if len(exc.args) == 1 and type(exc) is TypeError: + # simple TypeError; add field name to exception message + exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) + + # re-raise possibly-amended exception with original traceback: + raise exc.with_traceback(sys.exc_info()[2]) + + +def _AddInitMethod(message_descriptor, cls): + """Adds an __init__ method to cls.""" + + def _GetIntegerEnumValue(enum_type, value): + """Convert a string or integer enum value to an integer. + + If the value is a string, it is converted to the enum value in + enum_type with the same name. If the value is not a string, it's + returned as-is. (No conversion or bounds-checking is done.) + """ + if isinstance(value, str): + try: + return enum_type.values_by_name[value].number + except KeyError: + raise ValueError('Enum type %s: unknown label "%s"' % ( + enum_type.full_name, value)) + return value + + def init(self, **kwargs): + self._cached_byte_size = 0 + self._cached_byte_size_dirty = len(kwargs) > 0 + self._fields = {} + # Contains a mapping from oneof field descriptors to the descriptor + # of the currently set field in that oneof field. + self._oneofs = {} + + # _unknown_fields is () when empty for efficiency, and will be turned into + # a list if fields are added. + self._unknown_fields = () + # _unknown_field_set is None when empty for efficiency, and will be + # turned into UnknownFieldSet struct if fields are added. + self._unknown_field_set = None # pylint: disable=protected-access + self._is_present_in_parent = False + self._listener = message_listener_mod.NullMessageListener() + self._listener_for_children = _Listener(self) + for field_name, field_value in kwargs.items(): + field = _GetFieldByName(message_descriptor, field_name) + if field is None: + raise TypeError('%s() got an unexpected keyword argument "%s"' % + (message_descriptor.name, field_name)) + if field_value is None: + # field=None is the same as no field at all. + continue + if field.label == _FieldDescriptor.LABEL_REPEATED: + copy = field._default_constructor(self) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite + if _IsMapField(field): + if _IsMessageMapField(field): + for key in field_value: + copy[key].MergeFrom(field_value[key]) + else: + copy.update(field_value) + else: + for val in field_value: + if isinstance(val, dict): + copy.add(**val) + else: + copy.add().MergeFrom(val) + else: # Scalar + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = [_GetIntegerEnumValue(field.enum_type, val) + for val in field_value] + copy.extend(field_value) + self._fields[field] = copy + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + copy = field._default_constructor(self) + new_val = field_value + if isinstance(field_value, dict): + new_val = field.message_type._concrete_class(**field_value) + try: + copy.MergeFrom(new_val) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + self._fields[field] = copy + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = _GetIntegerEnumValue(field.enum_type, field_value) + try: + setattr(self, field_name, field_value) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + + init.__module__ = None + init.__doc__ = None + cls.__init__ = init + + +def _GetFieldByName(message_descriptor, field_name): + """Returns a field descriptor by field name. + + Args: + message_descriptor: A Descriptor describing all fields in message. + field_name: The name of the field to retrieve. + Returns: + The field descriptor associated with the field name. + """ + try: + return message_descriptor.fields_by_name[field_name] + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + +def _AddPropertiesForFields(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + for field in descriptor.fields: + _AddPropertiesForField(field, cls) + + if descriptor.is_extendable: + # _ExtensionDict is just an adaptor with no state so we allocate a new one + # every time it is accessed. + cls.Extensions = property(lambda self: _ExtensionDict(self)) + + +def _AddPropertiesForField(field, cls): + """Adds a public property for a protocol message field. + Clients can use this property to get and (in the case + of non-repeated scalar fields) directly set the value + of a protocol message field. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # Catch it if we add other types that we should + # handle specially here. + assert _FieldDescriptor.MAX_CPPTYPE == 10 + + constant_name = field.name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, field.number) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + _AddPropertiesForRepeatedField(field, cls) + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + _AddPropertiesForNonRepeatedCompositeField(field, cls) + else: + _AddPropertiesForNonRepeatedScalarField(field, cls) + + +class _FieldProperty(property): + __slots__ = ('DESCRIPTOR',) + + def __init__(self, descriptor, getter, setter, doc): + property.__init__(self, getter, setter, doc=doc) + self.DESCRIPTOR = descriptor + + +def _AddPropertiesForRepeatedField(field, cls): + """Adds a public property for a "repeated" protocol message field. Clients + can use this property to get the value of the field, which will be either a + RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see + below). + + Note that when clients add values to these containers, we perform + type-checking in the case of repeated scalar fields, and we also set any + necessary "has" bits as a side-effect. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to repeated field ' + '"%s" in protocol message object.' % proto_field_name) + + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedScalarField(field, cls): + """Adds a public property for a nonrepeated, scalar protocol message field. + Clients can use this property to get and directly set the value of the field. + Note that when the client sets the value of a field by using this property, + all necessary "has" bits are set as a side-effect, and we also perform + type-checking. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + type_checker = type_checkers.GetTypeChecker(field) + default_value = field.default_value + is_proto3 = field.containing_type.syntax == 'proto3' + + def getter(self): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return self._fields.get(field, default_value) + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + clear_when_set_to_default = is_proto3 and not field.containing_oneof + + def field_setter(self, new_value): + # pylint: disable=protected-access + # Testing the value for truthiness captures all of the proto3 defaults + # (0, 0.0, enum 0, and False). + try: + new_value = type_checker.CheckValue(new_value) + except TypeError as e: + raise TypeError( + 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) + if clear_when_set_to_default and not new_value: + self._fields.pop(field, None) + else: + self._fields[field] = new_value + # Check _cached_byte_size_dirty inline to improve performance, since scalar + # setters are called frequently. + if not self._cached_byte_size_dirty: + self._Modified() + + if field.containing_oneof: + def setter(self, new_value): + field_setter(self, new_value) + self._UpdateOneofState(field) + else: + setter = field_setter + + setter.__module__ = None + setter.__doc__ = 'Setter for %s.' % proto_field_name + + # Add a property to encapsulate the getter/setter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedCompositeField(field, cls): + """Adds a public property for a nonrepeated, composite protocol message field. + A composite field is a "group" or "message" field. + + Clients can use this property to get the value of the field, but cannot + assign to the property directly. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # TODO(robinson): Remove duplication with similar method + # for non-repeated scalars. + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to composite field ' + '"%s" in protocol message object.' % proto_field_name) + + # Add a property to encapsulate the getter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForExtensions(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + constant_name = extension_name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, extension_field.number) + + # TODO(amauryfa): Migrate all users of these attributes to functions like + # pool.FindExtensionByNumber(descriptor). + if descriptor.file is not None: + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + pool = descriptor.file.pool + cls._extensions_by_number = pool._extensions_by_number[descriptor] + cls._extensions_by_name = pool._extensions_by_name[descriptor] + +def _AddStaticMethods(cls): + # TODO(robinson): This probably needs to be thread-safe(?) + def RegisterExtension(extension_handle): + extension_handle.containing_type = cls.DESCRIPTOR + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + # pylint: disable=protected-access + cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle) + _AttachFieldHelpers(cls, extension_handle) + cls.RegisterExtension = staticmethod(RegisterExtension) + + def FromString(s): + message = cls() + message.MergeFromString(s) + return message + cls.FromString = staticmethod(FromString) + + +def _IsPresent(item): + """Given a (FieldDescriptor, value) tuple from _fields, return true if the + value should be included in the list returned by ListFields().""" + + if item[0].label == _FieldDescriptor.LABEL_REPEATED: + return bool(item[1]) + elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + return item[1]._is_present_in_parent + else: + return True + + +def _AddListFieldsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ListFields(self): + all_fields = [item for item in self._fields.items() if _IsPresent(item)] + all_fields.sort(key = lambda item: item[0].number) + return all_fields + + cls.ListFields = ListFields + +_PROTO3_ERROR_TEMPLATE = \ + ('Protocol message %s has no non-repeated submessage field "%s" ' + 'nor marked as optional') +_PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field "%s"' + +def _AddHasFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + is_proto3 = (message_descriptor.syntax == "proto3") + error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE + + hassable_fields = {} + for field in message_descriptor.fields: + if field.label == _FieldDescriptor.LABEL_REPEATED: + continue + # For proto3, only submessages and fields inside a oneof have presence. + if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and + not field.containing_oneof): + continue + hassable_fields[field.name] = field + + # Has methods are supported for oneof descriptors. + for oneof in message_descriptor.oneofs: + hassable_fields[oneof.name] = oneof + + def HasField(self, field_name): + try: + field = hassable_fields[field_name] + except KeyError: + raise ValueError(error_msg % (message_descriptor.full_name, field_name)) + + if isinstance(field, descriptor_mod.OneofDescriptor): + try: + return HasField(self, self._oneofs[field].name) + except KeyError: + return False + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(field) + return value is not None and value._is_present_in_parent + else: + return field in self._fields + + cls.HasField = HasField + + +def _AddClearFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def ClearField(self, field_name): + try: + field = message_descriptor.fields_by_name[field_name] + except KeyError: + try: + field = message_descriptor.oneofs_by_name[field_name] + if field in self._oneofs: + field = self._oneofs[field] + else: + return + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + if field in self._fields: + # To match the C++ implementation, we need to invalidate iterators + # for map fields when ClearField() happens. + if hasattr(self._fields[field], 'InvalidateIterators'): + self._fields[field].InvalidateIterators() + + # Note: If the field is a sub-message, its listener will still point + # at us. That's fine, because the worst than can happen is that it + # will call _Modified() and invalidate our byte size. Big deal. + del self._fields[field] + + if self._oneofs.get(field.containing_oneof, None) is field: + del self._oneofs[field.containing_oneof] + + # Always call _Modified() -- even if nothing was changed, this is + # a mutating method, and thus calling it should cause the field to become + # present in the parent message. + self._Modified() + + cls.ClearField = ClearField + + +def _AddClearExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def ClearExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + + # Similar to ClearField(), above. + if extension_handle in self._fields: + del self._fields[extension_handle] + self._Modified() + cls.ClearExtension = ClearExtension + + +def _AddHasExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def HasExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + if extension_handle.label == _FieldDescriptor.LABEL_REPEATED: + raise KeyError('"%s" is repeated.' % extension_handle.full_name) + + if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(extension_handle) + return value is not None and value._is_present_in_parent + else: + return extension_handle in self._fields + cls.HasExtension = HasExtension + +def _InternalUnpackAny(msg): + """Unpacks Any message and returns the unpacked message. + + This internal method is different from public Any Unpack method which takes + the target message as argument. _InternalUnpackAny method does not have + target message type and need to find the message type in descriptor pool. + + Args: + msg: An Any message to be unpacked. + + Returns: + The unpacked message. + """ + # TODO(amauryfa): Don't use the factory of generated messages. + # To make Any work with custom factories, use the message factory of the + # parent message. + # pylint: disable=g-import-not-at-top + from google.protobuf import symbol_database + factory = symbol_database.Default() + + type_url = msg.type_url + + if not type_url: + return None + + # TODO(haberman): For now we just strip the hostname. Better logic will be + # required. + type_name = type_url.split('/')[-1] + descriptor = factory.pool.FindMessageTypeByName(type_name) + + if descriptor is None: + return None + + message_class = factory.GetPrototype(descriptor) + message = message_class() + + message.ParseFromString(msg.value) + return message + + +def _AddEqualsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __eq__(self, other): + if (not isinstance(other, message_mod.Message) or + other.DESCRIPTOR != self.DESCRIPTOR): + return False + + if self is other: + return True + + if self.DESCRIPTOR.full_name == _AnyFullTypeName: + any_a = _InternalUnpackAny(self) + any_b = _InternalUnpackAny(other) + if any_a and any_b: + return any_a == any_b + + if not self.ListFields() == other.ListFields(): + return False + + # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, + # then use it for the comparison. + unknown_fields = list(self._unknown_fields) + unknown_fields.sort() + other_unknown_fields = list(other._unknown_fields) + other_unknown_fields.sort() + return unknown_fields == other_unknown_fields + + cls.__eq__ = __eq__ + + +def _AddStrMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __str__(self): + return text_format.MessageToString(self) + cls.__str__ = __str__ + + +def _AddReprMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __repr__(self): + return text_format.MessageToString(self) + cls.__repr__ = __repr__ + + +def _AddUnicodeMethod(unused_message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def __unicode__(self): + return text_format.MessageToString(self, as_utf8=True).decode('utf-8') + cls.__unicode__ = __unicode__ + + +def _BytesForNonRepeatedElement(value, field_number, field_type): + """Returns the number of bytes needed to serialize a non-repeated element. + The returned byte count includes space for tag information and any + other additional space associated with serializing value. + + Args: + value: Value we're serializing. + field_number: Field number of this value. (Since the field number + is stored as part of a varint-encoded tag, this has an impact + on the total bytes required to serialize the value). + field_type: The type of the field. One of the TYPE_* constants + within FieldDescriptor. + """ + try: + fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] + return fn(field_number, value) + except KeyError: + raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) + + +def _AddByteSizeMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ByteSize(self): + if not self._cached_byte_size_dirty: + return self._cached_byte_size + + size = 0 + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + size = descriptor.fields_by_name['key']._sizer(self.key) + size += descriptor.fields_by_name['value']._sizer(self.value) + else: + for field_descriptor, field_value in self.ListFields(): + size += field_descriptor._sizer(field_value) + for tag_bytes, value_bytes in self._unknown_fields: + size += len(tag_bytes) + len(value_bytes) + + self._cached_byte_size = size + self._cached_byte_size_dirty = False + self._listener_for_children.dirty = False + return size + + cls.ByteSize = ByteSize + + +def _AddSerializeToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializeToString(self, **kwargs): + # Check if the message has all of its required fields set. + if not self.IsInitialized(): + raise message_mod.EncodeError( + 'Message %s is missing required fields: %s' % ( + self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) + return self.SerializePartialToString(**kwargs) + cls.SerializeToString = SerializeToString + + +def _AddSerializePartialToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializePartialToString(self, **kwargs): + out = BytesIO() + self._InternalSerialize(out.write, **kwargs) + return out.getvalue() + cls.SerializePartialToString = SerializePartialToString + + def InternalSerialize(self, write_bytes, deterministic=None): + if deterministic is None: + deterministic = ( + api_implementation.IsPythonDefaultSerializationDeterministic()) + else: + deterministic = bool(deterministic) + + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + descriptor.fields_by_name['key']._encoder( + write_bytes, self.key, deterministic) + descriptor.fields_by_name['value']._encoder( + write_bytes, self.value, deterministic) + else: + for field_descriptor, field_value in self.ListFields(): + field_descriptor._encoder(write_bytes, field_value, deterministic) + for tag_bytes, value_bytes in self._unknown_fields: + write_bytes(tag_bytes) + write_bytes(value_bytes) + cls._InternalSerialize = InternalSerialize + + +def _AddMergeFromStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def MergeFromString(self, serialized): + serialized = memoryview(serialized) + length = len(serialized) + try: + if self._InternalParse(serialized, 0, length) != length: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise message_mod.DecodeError('Unexpected end-group tag.') + except (IndexError, TypeError): + # Now ord(buf[p:p+1]) == ord('') gets TypeError. + raise message_mod.DecodeError('Truncated message.') + except struct.error as e: + raise message_mod.DecodeError(e) + return length # Return this for legacy reasons. + cls.MergeFromString = MergeFromString + + local_ReadTag = decoder.ReadTag + local_SkipField = decoder.SkipField + decoders_by_tag = cls._decoders_by_tag + + def InternalParse(self, buffer, pos, end): + """Create a message from serialized bytes. + + Args: + self: Message, instance of the proto message object. + buffer: memoryview of the serialized data. + pos: int, position to start in the serialized data. + end: int, end position of the serialized data. + + Returns: + Message object. + """ + # Guard against internal misuse, since this function is called internally + # quite extensively, and its easy to accidentally pass bytes. + assert isinstance(buffer, memoryview) + self._Modified() + field_dict = self._fields + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + while pos != end: + (tag_bytes, new_pos) = local_ReadTag(buffer, pos) + field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) + if field_decoder is None: + if not self._unknown_fields: # pylint: disable=protected-access + self._unknown_fields = [] # pylint: disable=protected-access + if unknown_field_set is None: + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + # pylint: disable=protected-access + (tag, _) = decoder._DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if field_number == 0: + raise message_mod.DecodeError('Field number 0 is illegal.') + # TODO(jieluo): remove old_pos. + old_pos = new_pos + (data, new_pos) = decoder._DecodeUnknownField( + buffer, new_pos, wire_type) # pylint: disable=protected-access + if new_pos == -1: + return pos + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + # TODO(jieluo): remove _unknown_fields. + new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) + if new_pos == -1: + return pos + self._unknown_fields.append( + (tag_bytes, buffer[old_pos:new_pos].tobytes())) + pos = new_pos + else: + pos = field_decoder(buffer, new_pos, end, self, field_dict) + if field_desc: + self._UpdateOneofState(field_desc) + return pos + cls._InternalParse = InternalParse + + +def _AddIsInitializedMethod(message_descriptor, cls): + """Adds the IsInitialized and FindInitializationError methods to the + protocol message class.""" + + required_fields = [field for field in message_descriptor.fields + if field.label == _FieldDescriptor.LABEL_REQUIRED] + + def IsInitialized(self, errors=None): + """Checks if all required fields of a message are set. + + Args: + errors: A list which, if provided, will be populated with the field + paths of all missing required fields. + + Returns: + True iff the specified message has all required fields set. + """ + + # Performance is critical so we avoid HasField() and ListFields(). + + for field in required_fields: + if (field not in self._fields or + (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and + not self._fields[field]._is_present_in_parent)): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + for field, value in list(self._fields.items()): # dict can change size! + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.label == _FieldDescriptor.LABEL_REPEATED: + if (field.message_type.has_options and + field.message_type.GetOptions().map_entry): + continue + for element in value: + if not element.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + elif value._is_present_in_parent and not value.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + return True + + cls.IsInitialized = IsInitialized + + def FindInitializationErrors(self): + """Finds required fields which are not initialized. + + Returns: + A list of strings. Each string is a path to an uninitialized field from + the top-level message, e.g. "foo.bar[5].baz". + """ + + errors = [] # simplify things + + for field in required_fields: + if not self.HasField(field.name): + errors.append(field.name) + + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + name = '(%s)' % field.full_name + else: + name = field.name + + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + element = value[key] + prefix = '%s[%s].' % (name, key) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + # ScalarMaps can't have any initialization errors. + pass + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for i in range(len(value)): + element = value[i] + prefix = '%s[%d].' % (name, i) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + prefix = name + '.' + sub_errors = value.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + + return errors + + cls.FindInitializationErrors = FindInitializationErrors + + +def _FullyQualifiedClassName(klass): + module = klass.__module__ + name = getattr(klass, '__qualname__', klass.__name__) + if module in (None, 'builtins', '__builtin__'): + return name + return module + '.' + name + + +def _AddMergeFromMethod(cls): + LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED + CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE + + def MergeFrom(self, msg): + if not isinstance(msg, cls): + raise TypeError( + 'Parameter to MergeFrom() must be instance of same class: ' + 'expected %s got %s.' % (_FullyQualifiedClassName(cls), + _FullyQualifiedClassName(msg.__class__))) + + assert msg is not self + self._Modified() + + fields = self._fields + + for field, value in msg._fields.items(): + if field.label == LABEL_REPEATED: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + elif field.cpp_type == CPPTYPE_MESSAGE: + if value._is_present_in_parent: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + else: + self._fields[field] = value + if field.containing_oneof: + self._UpdateOneofState(field) + + if msg._unknown_fields: + if not self._unknown_fields: + self._unknown_fields = [] + self._unknown_fields.extend(msg._unknown_fields) + # pylint: disable=protected-access + if self._unknown_field_set is None: + self._unknown_field_set = containers.UnknownFieldSet() + self._unknown_field_set._extend(msg._unknown_field_set) + + cls.MergeFrom = MergeFrom + + +def _AddWhichOneofMethod(message_descriptor, cls): + def WhichOneof(self, oneof_name): + """Returns the name of the currently set field inside a oneof, or None.""" + try: + field = message_descriptor.oneofs_by_name[oneof_name] + except KeyError: + raise ValueError( + 'Protocol message has no oneof "%s" field.' % oneof_name) + + nested_field = self._oneofs.get(field, None) + if nested_field is not None and self.HasField(nested_field.name): + return nested_field.name + else: + return None + + cls.WhichOneof = WhichOneof + + +def _Clear(self): + # Clear fields. + self._fields = {} + self._unknown_fields = () + # pylint: disable=protected-access + if self._unknown_field_set is not None: + self._unknown_field_set._clear() + self._unknown_field_set = None + + self._oneofs = {} + self._Modified() + + +def _UnknownFields(self): + if self._unknown_field_set is None: # pylint: disable=protected-access + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + return self._unknown_field_set # pylint: disable=protected-access + + +def _DiscardUnknownFields(self): + self._unknown_fields = [] + self._unknown_field_set = None # pylint: disable=protected-access + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + value[key].DiscardUnknownFields() + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for sub_message in value: + sub_message.DiscardUnknownFields() + else: + value.DiscardUnknownFields() + + +def _SetListener(self, listener): + if listener is None: + self._listener = message_listener_mod.NullMessageListener() + else: + self._listener = listener + + +def _AddMessageMethods(message_descriptor, cls): + """Adds implementations of all Message methods to cls.""" + _AddListFieldsMethod(message_descriptor, cls) + _AddHasFieldMethod(message_descriptor, cls) + _AddClearFieldMethod(message_descriptor, cls) + if message_descriptor.is_extendable: + _AddClearExtensionMethod(cls) + _AddHasExtensionMethod(cls) + _AddEqualsMethod(message_descriptor, cls) + _AddStrMethod(message_descriptor, cls) + _AddReprMethod(message_descriptor, cls) + _AddUnicodeMethod(message_descriptor, cls) + _AddByteSizeMethod(message_descriptor, cls) + _AddSerializeToStringMethod(message_descriptor, cls) + _AddSerializePartialToStringMethod(message_descriptor, cls) + _AddMergeFromStringMethod(message_descriptor, cls) + _AddIsInitializedMethod(message_descriptor, cls) + _AddMergeFromMethod(cls) + _AddWhichOneofMethod(message_descriptor, cls) + # Adds methods which do not depend on cls. + cls.Clear = _Clear + cls.UnknownFields = _UnknownFields + cls.DiscardUnknownFields = _DiscardUnknownFields + cls._SetListener = _SetListener + + +def _AddPrivateHelperMethods(message_descriptor, cls): + """Adds implementation of private helper methods to cls.""" + + def Modified(self): + """Sets the _cached_byte_size_dirty bit to true, + and propagates this to our listener iff this was a state change. + """ + + # Note: Some callers check _cached_byte_size_dirty before calling + # _Modified() as an extra optimization. So, if this method is ever + # changed such that it does stuff even when _cached_byte_size_dirty is + # already true, the callers need to be updated. + if not self._cached_byte_size_dirty: + self._cached_byte_size_dirty = True + self._listener_for_children.dirty = True + self._is_present_in_parent = True + self._listener.Modified() + + def _UpdateOneofState(self, field): + """Sets field as the active field in its containing oneof. + + Will also delete currently active field in the oneof, if it is different + from the argument. Does not mark the message as modified. + """ + other_field = self._oneofs.setdefault(field.containing_oneof, field) + if other_field is not field: + del self._fields[other_field] + self._oneofs[field.containing_oneof] = field + + cls._Modified = Modified + cls.SetInParent = Modified + cls._UpdateOneofState = _UpdateOneofState + + +class _Listener(object): + + """MessageListener implementation that a parent message registers with its + child message. + + In order to support semantics like: + + foo.bar.baz.qux = 23 + assert foo.HasField('bar') + + ...child objects must have back references to their parents. + This helper class is at the heart of this support. + """ + + def __init__(self, parent_message): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + """ + # This listener establishes a back reference from a child (contained) object + # to its parent (containing) object. We make this a weak reference to avoid + # creating cyclic garbage when the client finishes with the 'parent' object + # in the tree. + if isinstance(parent_message, weakref.ProxyType): + self._parent_message_weakref = parent_message + else: + self._parent_message_weakref = weakref.proxy(parent_message) + + # As an optimization, we also indicate directly on the listener whether + # or not the parent message is dirty. This way we can avoid traversing + # up the tree in the common case. + self.dirty = False + + def Modified(self): + if self.dirty: + return + try: + # Propagate the signal to our parents iff this is the first field set. + self._parent_message_weakref._Modified() + except ReferenceError: + # We can get here if a client has kept a reference to a child object, + # and is now setting a field on it, but the child's parent has been + # garbage-collected. This is not an error. + pass + + +class _OneofListener(_Listener): + """Special listener implementation for setting composite oneof fields.""" + + def __init__(self, parent_message, field): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + field: The descriptor of the field being set in the parent message. + """ + super(_OneofListener, self).__init__(parent_message) + self._field = field + + def Modified(self): + """Also updates the state of the containing oneof in the parent message.""" + try: + self._parent_message_weakref._UpdateOneofState(self._field) + super(_OneofListener, self).Modified() + except ReferenceError: + pass diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py new file mode 100644 index 0000000000..a53e71fe8e --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py @@ -0,0 +1,435 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides type checking routines. + +This module defines type checking utilities in the forms of dictionaries: + +VALUE_CHECKERS: A dictionary of field types and a value validation object. +TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing + function. +TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization + function. +FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their + corresponding wire types. +TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization + function. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import ctypes +import numbers + +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import descriptor + +_FieldDescriptor = descriptor.FieldDescriptor + + +def TruncateToFourByteFloat(original): + return ctypes.c_float(original).value + + +def ToShortestFloat(original): + """Returns the shortest float that has same value in wire.""" + # All 4 byte floats have between 6 and 9 significant digits, so we + # start with 6 as the lower bound. + # It has to be iterative because use '.9g' directly can not get rid + # of the noises for most values. For example if set a float_field=0.9 + # use '.9g' will print 0.899999976. + precision = 6 + rounded = float('{0:.{1}g}'.format(original, precision)) + while TruncateToFourByteFloat(rounded) != original: + precision += 1 + rounded = float('{0:.{1}g}'.format(original, precision)) + return rounded + + +def SupportsOpenEnums(field_descriptor): + return field_descriptor.containing_type.syntax == 'proto3' + + +def GetTypeChecker(field): + """Returns a type checker for a message field of the specified types. + + Args: + field: FieldDescriptor object for this field. + + Returns: + An instance of TypeChecker which can be used to verify the types + of values assigned to a field of the specified type. + """ + if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and + field.type == _FieldDescriptor.TYPE_STRING): + return UnicodeValueChecker() + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + if SupportsOpenEnums(field): + # When open enums are supported, any int32 can be assigned. + return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] + else: + return EnumValueChecker(field.enum_type) + return _VALUE_CHECKERS[field.cpp_type] + + +# None of the typecheckers below make any attempt to guard against people +# subclassing builtin types and doing weird things. We're not trying to +# protect against malicious clients here, just people accidentally shooting +# themselves in the foot in obvious ways. +class TypeChecker(object): + + """Type checker used to catch type errors as early as possible + when the client is setting scalar fields in protocol messages. + """ + + def __init__(self, *acceptable_types): + self._acceptable_types = acceptable_types + + def CheckValue(self, proposed_value): + """Type check the provided value and return it. + + The returned value might have been normalized to another type. + """ + if not isinstance(proposed_value, self._acceptable_types): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), self._acceptable_types)) + raise TypeError(message) + return proposed_value + + +class TypeCheckerWithDefault(TypeChecker): + + def __init__(self, default_value, *acceptable_types): + TypeChecker.__init__(self, *acceptable_types) + self._default_value = default_value + + def DefaultValue(self): + return self._default_value + + +class BoolValueChecker(object): + """Type checker used for bool fields.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bool, int))) + raise TypeError(message) + return bool(proposed_value) + + def DefaultValue(self): + return False + + +# IntValueChecker and its subclasses perform integer type-checks +# and bounds-checks. +class IntValueChecker(object): + + """Checker used for integer fields. Performs type-check and range check.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + + if not self._MIN <= int(proposed_value) <= self._MAX: + raise ValueError('Value out of range: %d' % proposed_value) + # We force all values to int to make alternate implementations where the + # distinction is more significant (e.g. the C++ implementation) simpler. + proposed_value = int(proposed_value) + return proposed_value + + def DefaultValue(self): + return 0 + + +class EnumValueChecker(object): + + """Checker used for enum fields. Performs type-check and range check.""" + + def __init__(self, enum_type): + self._enum_type = enum_type + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, numbers.Integral): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + if int(proposed_value) not in self._enum_type.values_by_number: + raise ValueError('Unknown enum value: %d' % proposed_value) + return proposed_value + + def DefaultValue(self): + return self._enum_type.values[0].number + + +class UnicodeValueChecker(object): + + """Checker used for string fields. + + Always returns a unicode value, even if the input is of type str. + """ + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, (bytes, str)): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bytes, str))) + raise TypeError(message) + + # If the value is of type 'bytes' make sure that it is valid UTF-8 data. + if isinstance(proposed_value, bytes): + try: + proposed_value = proposed_value.decode('utf-8') + except UnicodeDecodeError: + raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' + 'encoding. Non-UTF-8 strings must be converted to ' + 'unicode objects before being added.' % + (proposed_value)) + else: + try: + proposed_value.encode('utf8') + except UnicodeEncodeError: + raise ValueError('%.1024r isn\'t a valid unicode string and ' + 'can\'t be encoded in UTF-8.'% + (proposed_value)) + + return proposed_value + + def DefaultValue(self): + return u"" + + +class Int32ValueChecker(IntValueChecker): + # We're sure to use ints instead of longs here since comparison may be more + # efficient. + _MIN = -2147483648 + _MAX = 2147483647 + + +class Uint32ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 32) - 1 + + +class Int64ValueChecker(IntValueChecker): + _MIN = -(1 << 63) + _MAX = (1 << 63) - 1 + + +class Uint64ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 64) - 1 + + +# The max 4 bytes float is about 3.4028234663852886e+38 +_FLOAT_MAX = float.fromhex('0x1.fffffep+127') +_FLOAT_MIN = -_FLOAT_MAX +_INF = float('inf') +_NEG_INF = float('-inf') + + +class DoubleValueChecker(object): + """Checker used for double fields. + + Performs type-check and range check. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + if (not hasattr(proposed_value, '__float__') and + not hasattr(proposed_value, '__index__')) or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: int, float' % + (proposed_value, type(proposed_value))) + raise TypeError(message) + return float(proposed_value) + + def DefaultValue(self): + return 0.0 + + +class FloatValueChecker(DoubleValueChecker): + """Checker used for float fields. + + Performs type-check and range check. + + Values exceeding a 32-bit float will be converted to inf/-inf. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + converted_value = super().CheckValue(proposed_value) + # This inf rounding matches the C++ proto SafeDoubleToFloat logic. + if converted_value > _FLOAT_MAX: + return _INF + if converted_value < _FLOAT_MIN: + return _NEG_INF + + return TruncateToFourByteFloat(converted_value) + +# Type-checkers for all scalar CPPTYPEs. +_VALUE_CHECKERS = { + _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), + _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), + _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), + _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), + _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), + _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), +} + + +# Map from field type to a function F, such that F(field_num, value) +# gives the total byte size for a value of the given type. This +# byte size includes tag information and any other additional space +# associated with serializing "value". +TYPE_TO_BYTE_SIZE_FN = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, + _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, + _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, + _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, + _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, + _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, + _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, + _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, + _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, + _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, + _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, + _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, + _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, + _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, + _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, + _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, + _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, + _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize + } + + +# Maps from field types to encoder constructors. +TYPE_TO_ENCODER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, + _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, + _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, + _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, + _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, + _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, + _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, + _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, + } + + +# Maps from field types to sizer constructors. +TYPE_TO_SIZER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, + _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, + _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, + _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, + _FieldDescriptor.TYPE_STRING: encoder.StringSizer, + _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, + _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, + _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, + } + + +# Maps from field type to a decoder constructor. +TYPE_TO_DECODER = { + _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, + _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, + _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, + _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, + _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, + _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, + _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, + _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, + _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, + _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, + _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, + _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, + _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, + _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, + _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, + _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, + _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, + _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, + } + +# Maps from field type to expected wiretype. +FIELD_TYPE_TO_WIRE_TYPE = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_STRING: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, + _FieldDescriptor.TYPE_MESSAGE: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_BYTES: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, + } diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py new file mode 100644 index 0000000000..b581ab750a --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py @@ -0,0 +1,878 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains well known classes. + +This files defines well known classes which need extra maintenance including: + - Any + - Duration + - FieldMask + - Struct + - Timestamp +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +import calendar +import collections.abc +import datetime + +from google.protobuf.descriptor import FieldDescriptor + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_NANOS_PER_SECOND = 1000000000 +_NANOS_PER_MILLISECOND = 1000000 +_NANOS_PER_MICROSECOND = 1000 +_MILLIS_PER_SECOND = 1000 +_MICROS_PER_SECOND = 1000000 +_SECONDS_PER_DAY = 24 * 3600 +_DURATION_SECONDS_MAX = 315576000000 + + +class Any(object): + """Class for Any Message type.""" + + __slots__ = () + + def Pack(self, msg, type_url_prefix='type.googleapis.com/', + deterministic=None): + """Packs the specified message into current Any message.""" + if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': + self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + else: + self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + self.value = msg.SerializeToString(deterministic=deterministic) + + def Unpack(self, msg): + """Unpacks the current Any message into specified message.""" + descriptor = msg.DESCRIPTOR + if not self.Is(descriptor): + return False + msg.ParseFromString(self.value) + return True + + def TypeName(self): + """Returns the protobuf type name of the inner message.""" + # Only last part is to be used: b/25630112 + return self.type_url.split('/')[-1] + + def Is(self, descriptor): + """Checks if this Any represents the given protobuf type.""" + return '/' in self.type_url and self.TypeName() == descriptor.full_name + + +_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) +_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( + 0, tz=datetime.timezone.utc) + + +class Timestamp(object): + """Class for Timestamp message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Timestamp to RFC 3339 date string format. + + Returns: + A string converted from timestamp. The string is always Z-normalized + and uses 3, 6 or 9 fractional digits as required to represent the + exact time. Example of the return format: '1972-01-01T10:00:20.021Z' + """ + nanos = self.nanos % _NANOS_PER_SECOND + total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND + seconds = total_sec % _SECONDS_PER_DAY + days = (total_sec - seconds) // _SECONDS_PER_DAY + dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) + + result = dt.isoformat() + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 'Z' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03dZ' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06dZ' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09dZ' % nanos + + def FromJsonString(self, value): + """Parse a RFC 3339 date string format to Timestamp. + + Args: + value: A date string. Any fractional digits (or none) and any offset are + accepted as long as they fit into nano-seconds precision. + Example of accepted format: '1972-01-01T10:00:20.021-05:00' + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) + timezone_offset = value.find('Z') + if timezone_offset == -1: + timezone_offset = value.find('+') + if timezone_offset == -1: + timezone_offset = value.rfind('-') + if timezone_offset == -1: + raise ValueError( + 'Failed to parse timestamp: missing valid timezone offset.') + time_value = value[0:timezone_offset] + # Parse datetime and nanos. + point_position = time_value.find('.') + if point_position == -1: + second_value = time_value + nano_value = '' + else: + second_value = time_value[:point_position] + nano_value = time_value[point_position + 1:] + if 't' in second_value: + raise ValueError( + 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' + 'lowercase \'t\' is not accepted'.format(second_value)) + date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) + td = date_object - datetime.datetime(1970, 1, 1) + seconds = td.seconds + td.days * _SECONDS_PER_DAY + if len(nano_value) > 9: + raise ValueError( + 'Failed to parse Timestamp: nanos {0} more than ' + '9 fractional digits.'.format(nano_value)) + if nano_value: + nanos = round(float('0.' + nano_value) * 1e9) + else: + nanos = 0 + # Parse timezone offsets. + if value[timezone_offset] == 'Z': + if len(value) != timezone_offset + 1: + raise ValueError('Failed to parse timestamp: invalid trailing' + ' data {0}.'.format(value)) + else: + timezone = value[timezone_offset:] + pos = timezone.find(':') + if pos == -1: + raise ValueError( + 'Invalid timezone offset value: {0}.'.format(timezone)) + if timezone[0] == '+': + seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + else: + seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + # Set seconds and nanos + self.seconds = int(seconds) + self.nanos = int(nanos) + + def GetCurrentTime(self): + """Get the current UTC into Timestamp.""" + self.FromDatetime(datetime.datetime.utcnow()) + + def ToNanoseconds(self): + """Converts Timestamp to nanoseconds since epoch.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts Timestamp to microseconds since epoch.""" + return (self.seconds * _MICROS_PER_SECOND + + self.nanos // _NANOS_PER_MICROSECOND) + + def ToMilliseconds(self): + """Converts Timestamp to milliseconds since epoch.""" + return (self.seconds * _MILLIS_PER_SECOND + + self.nanos // _NANOS_PER_MILLISECOND) + + def ToSeconds(self): + """Converts Timestamp to seconds since epoch.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds since epoch to Timestamp.""" + self.seconds = nanos // _NANOS_PER_SECOND + self.nanos = nanos % _NANOS_PER_SECOND + + def FromMicroseconds(self, micros): + """Converts microseconds since epoch to Timestamp.""" + self.seconds = micros // _MICROS_PER_SECOND + self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + + def FromMilliseconds(self, millis): + """Converts milliseconds since epoch to Timestamp.""" + self.seconds = millis // _MILLIS_PER_SECOND + self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + + def FromSeconds(self, seconds): + """Converts seconds since epoch to Timestamp.""" + self.seconds = seconds + self.nanos = 0 + + def ToDatetime(self, tzinfo=None): + """Converts Timestamp to a datetime. + + Args: + tzinfo: A datetime.tzinfo subclass; defaults to None. + + Returns: + If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone + information, i.e. not aware that it's UTC). + + Otherwise, returns a timezone-aware datetime in the input timezone. + """ + delta = datetime.timedelta( + seconds=self.seconds, + microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) + if tzinfo is None: + return _EPOCH_DATETIME_NAIVE + delta + else: + return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta + + def FromDatetime(self, dt): + """Converts datetime to Timestamp. + + Args: + dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. + """ + # Using this guide: http://wiki.python.org/moin/WorkingWithTime + # And this conversion guide: http://docs.python.org/library/time.html + + # Turn the date parameter into a tuple (struct_time) that can then be + # manipulated into a long value of seconds. During the conversion from + # struct_time to long, the source date in UTC, and so it follows that the + # correct transformation is calendar.timegm() + self.seconds = calendar.timegm(dt.utctimetuple()) + self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND + + +class Duration(object): + """Class for Duration message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Duration to string format. + + Returns: + A string converted from self. The string format will contains + 3, 6, or 9 fractional digits depending on the precision required to + represent the exact Duration value. For example: "1s", "1.010s", + "1.000000100s", "-3.100s" + """ + _CheckDurationValid(self.seconds, self.nanos) + if self.seconds < 0 or self.nanos < 0: + result = '-' + seconds = - self.seconds + int((0 - self.nanos) // 1e9) + nanos = (0 - self.nanos) % 1e9 + else: + result = '' + seconds = self.seconds + int(self.nanos // 1e9) + nanos = self.nanos % 1e9 + result += '%d' % seconds + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 's' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03ds' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06ds' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09ds' % nanos + + def FromJsonString(self, value): + """Converts a string to Duration. + + Args: + value: A string to be converted. The string must end with 's'. Any + fractional digits (or none) are accepted as long as they fit into + precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Duration JSON value not a string: {!r}'.format(value)) + if len(value) < 1 or value[-1] != 's': + raise ValueError( + 'Duration must end with letter "s": {0}.'.format(value)) + try: + pos = value.find('.') + if pos == -1: + seconds = int(value[:-1]) + nanos = 0 + else: + seconds = int(value[:pos]) + if value[0] == '-': + nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) + else: + nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) + _CheckDurationValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + except ValueError as e: + raise ValueError( + 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) + + def ToNanoseconds(self): + """Converts a Duration to nanoseconds.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts a Duration to microseconds.""" + micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) + return self.seconds * _MICROS_PER_SECOND + micros + + def ToMilliseconds(self): + """Converts a Duration to milliseconds.""" + millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) + return self.seconds * _MILLIS_PER_SECOND + millis + + def ToSeconds(self): + """Converts a Duration to seconds.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds to Duration.""" + self._NormalizeDuration(nanos // _NANOS_PER_SECOND, + nanos % _NANOS_PER_SECOND) + + def FromMicroseconds(self, micros): + """Converts microseconds to Duration.""" + self._NormalizeDuration( + micros // _MICROS_PER_SECOND, + (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) + + def FromMilliseconds(self, millis): + """Converts milliseconds to Duration.""" + self._NormalizeDuration( + millis // _MILLIS_PER_SECOND, + (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) + + def FromSeconds(self, seconds): + """Converts seconds to Duration.""" + self.seconds = seconds + self.nanos = 0 + + def ToTimedelta(self): + """Converts Duration to timedelta.""" + return datetime.timedelta( + seconds=self.seconds, microseconds=_RoundTowardZero( + self.nanos, _NANOS_PER_MICROSECOND)) + + def FromTimedelta(self, td): + """Converts timedelta to Duration.""" + self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, + td.microseconds * _NANOS_PER_MICROSECOND) + + def _NormalizeDuration(self, seconds, nanos): + """Set Duration by seconds and nanos.""" + # Force nanos to be negative if the duration is negative. + if seconds < 0 and nanos > 0: + seconds += 1 + nanos -= _NANOS_PER_SECOND + self.seconds = seconds + self.nanos = nanos + + +def _CheckDurationValid(seconds, nanos): + if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: + raise ValueError( + 'Duration is not valid: Seconds {0} must be in range ' + '[-315576000000, 315576000000].'.format(seconds)) + if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Duration is not valid: Nanos {0} must be in range ' + '[-999999999, 999999999].'.format(nanos)) + if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): + raise ValueError( + 'Duration is not valid: Sign mismatch.') + + +def _RoundTowardZero(value, divider): + """Truncates the remainder part after division.""" + # For some languages, the sign of the remainder is implementation + # dependent if any of the operands is negative. Here we enforce + # "rounded toward zero" semantics. For example, for (-5) / 2 an + # implementation may give -3 as the result with the remainder being + # 1. This function ensures we always return -2 (closer to zero). + result = value // divider + remainder = value % divider + if result < 0 and remainder > 0: + return result + 1 + else: + return result + + +class FieldMask(object): + """Class for FieldMask message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts FieldMask to string according to proto3 JSON spec.""" + camelcase_paths = [] + for path in self.paths: + camelcase_paths.append(_SnakeCaseToCamelCase(path)) + return ','.join(camelcase_paths) + + def FromJsonString(self, value): + """Converts string to FieldMask according to proto3 JSON spec.""" + if not isinstance(value, str): + raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) + self.Clear() + if value: + for path in value.split(','): + self.paths.append(_CamelCaseToSnakeCase(path)) + + def IsValidForDescriptor(self, message_descriptor): + """Checks whether the FieldMask is valid for Message Descriptor.""" + for path in self.paths: + if not _IsValidPath(message_descriptor, path): + return False + return True + + def AllFieldsFromDescriptor(self, message_descriptor): + """Gets all direct fields of Message Descriptor to FieldMask.""" + self.Clear() + for field in message_descriptor.fields: + self.paths.append(field.name) + + def CanonicalFormFromMask(self, mask): + """Converts a FieldMask to the canonical form. + + Removes paths that are covered by another path. For example, + "foo.bar" is covered by "foo" and will be removed if "foo" + is also in the FieldMask. Then sorts all paths in alphabetical order. + + Args: + mask: The original FieldMask to be converted. + """ + tree = _FieldMaskTree(mask) + tree.ToFieldMask(self) + + def Union(self, mask1, mask2): + """Merges mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + tree.MergeFromFieldMask(mask2) + tree.ToFieldMask(self) + + def Intersect(self, mask1, mask2): + """Intersects mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + intersection = _FieldMaskTree() + for path in mask2.paths: + tree.IntersectPath(path, intersection) + intersection.ToFieldMask(self) + + def MergeMessage( + self, source, destination, + replace_message_field=False, replace_repeated_field=False): + """Merges fields specified in FieldMask from source to destination. + + Args: + source: Source message. + destination: The destination message to be merged into. + replace_message_field: Replace message field if True. Merge message + field if False. + replace_repeated_field: Replace repeated field if True. Append + elements of repeated field if False. + """ + tree = _FieldMaskTree(self) + tree.MergeMessage( + source, destination, replace_message_field, replace_repeated_field) + + +def _IsValidPath(message_descriptor, path): + """Checks whether the path is valid for Message Descriptor.""" + parts = path.split('.') + last = parts.pop() + for name in parts: + field = message_descriptor.fields_by_name.get(name) + if (field is None or + field.label == FieldDescriptor.LABEL_REPEATED or + field.type != FieldDescriptor.TYPE_MESSAGE): + return False + message_descriptor = field.message_type + return last in message_descriptor.fields_by_name + + +def _CheckFieldMaskMessage(message): + """Raises ValueError if message is not a FieldMask.""" + message_descriptor = message.DESCRIPTOR + if (message_descriptor.name != 'FieldMask' or + message_descriptor.file.name != 'google/protobuf/field_mask.proto'): + raise ValueError('Message {0} is not a FieldMask.'.format( + message_descriptor.full_name)) + + +def _SnakeCaseToCamelCase(path_name): + """Converts a path name from snake_case to camelCase.""" + result = [] + after_underscore = False + for c in path_name: + if c.isupper(): + raise ValueError( + 'Fail to print FieldMask to Json string: Path name ' + '{0} must not contain uppercase letters.'.format(path_name)) + if after_underscore: + if c.islower(): + result.append(c.upper()) + after_underscore = False + else: + raise ValueError( + 'Fail to print FieldMask to Json string: The ' + 'character after a "_" must be a lowercase letter ' + 'in path name {0}.'.format(path_name)) + elif c == '_': + after_underscore = True + else: + result += c + + if after_underscore: + raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' + 'in path name {0}.'.format(path_name)) + return ''.join(result) + + +def _CamelCaseToSnakeCase(path_name): + """Converts a field name from camelCase to snake_case.""" + result = [] + for c in path_name: + if c == '_': + raise ValueError('Fail to parse FieldMask: Path name ' + '{0} must not contain "_"s.'.format(path_name)) + if c.isupper(): + result += '_' + result += c.lower() + else: + result += c + return ''.join(result) + + +class _FieldMaskTree(object): + """Represents a FieldMask in a tree structure. + + For example, given a FieldMask "foo.bar,foo.baz,bar.baz", + the FieldMaskTree will be: + [_root] -+- foo -+- bar + | | + | +- baz + | + +- bar --- baz + In the tree, each leaf node represents a field path. + """ + + __slots__ = ('_root',) + + def __init__(self, field_mask=None): + """Initializes the tree by FieldMask.""" + self._root = {} + if field_mask: + self.MergeFromFieldMask(field_mask) + + def MergeFromFieldMask(self, field_mask): + """Merges a FieldMask to the tree.""" + for path in field_mask.paths: + self.AddPath(path) + + def AddPath(self, path): + """Adds a field path into the tree. + + If the field path to add is a sub-path of an existing field path + in the tree (i.e., a leaf node), it means the tree already matches + the given path so nothing will be added to the tree. If the path + matches an existing non-leaf node in the tree, that non-leaf node + will be turned into a leaf node with all its children removed because + the path matches all the node's children. Otherwise, a new path will + be added. + + Args: + path: The field path to add. + """ + node = self._root + for name in path.split('.'): + if name not in node: + node[name] = {} + elif not node[name]: + # Pre-existing empty node implies we already have this entire tree. + return + node = node[name] + # Remove any sub-trees we might have had. + node.clear() + + def ToFieldMask(self, field_mask): + """Converts the tree to a FieldMask.""" + field_mask.Clear() + _AddFieldPaths(self._root, '', field_mask) + + def IntersectPath(self, path, intersection): + """Calculates the intersection part of a field path with this tree. + + Args: + path: The field path to calculates. + intersection: The out tree to record the intersection part. + """ + node = self._root + for name in path.split('.'): + if name not in node: + return + elif not node[name]: + intersection.AddPath(path) + return + node = node[name] + intersection.AddLeafNodes(path, node) + + def AddLeafNodes(self, prefix, node): + """Adds leaf nodes begin with prefix to this tree.""" + if not node: + self.AddPath(prefix) + for name in node: + child_path = prefix + '.' + name + self.AddLeafNodes(child_path, node[name]) + + def MergeMessage( + self, source, destination, + replace_message, replace_repeated): + """Merge all fields specified by this tree from source to destination.""" + _MergeMessage( + self._root, source, destination, replace_message, replace_repeated) + + +def _StrConvert(value): + """Converts value to str if it is not.""" + # This file is imported by c extension and some methods like ClearField + # requires string for the field name. py2/py3 has different text + # type and may use unicode. + if not isinstance(value, str): + return value.encode('utf-8') + return value + + +def _MergeMessage( + node, source, destination, replace_message, replace_repeated): + """Merge all fields specified by a sub-tree from source to destination.""" + source_descriptor = source.DESCRIPTOR + for name in node: + child = node[name] + field = source_descriptor.fields_by_name[name] + if field is None: + raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( + name, source_descriptor.full_name)) + if child: + # Sub-paths are only allowed for singular message fields. + if (field.label == FieldDescriptor.LABEL_REPEATED or + field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): + raise ValueError('Error: Field {0} in message {1} is not a singular ' + 'message field and cannot have sub-fields.'.format( + name, source_descriptor.full_name)) + if source.HasField(name): + _MergeMessage( + child, getattr(source, name), getattr(destination, name), + replace_message, replace_repeated) + continue + if field.label == FieldDescriptor.LABEL_REPEATED: + if replace_repeated: + destination.ClearField(_StrConvert(name)) + repeated_source = getattr(source, name) + repeated_destination = getattr(destination, name) + repeated_destination.MergeFrom(repeated_source) + else: + if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + if replace_message: + destination.ClearField(_StrConvert(name)) + if source.HasField(name): + getattr(destination, name).MergeFrom(getattr(source, name)) + else: + setattr(destination, name, getattr(source, name)) + + +def _AddFieldPaths(node, prefix, field_mask): + """Adds the field paths descended from node to field_mask.""" + if not node and prefix: + field_mask.paths.append(prefix) + return + for name in sorted(node): + if prefix: + child_path = prefix + '.' + name + else: + child_path = name + _AddFieldPaths(node[name], child_path, field_mask) + + +def _SetStructValue(struct_value, value): + if value is None: + struct_value.null_value = 0 + elif isinstance(value, bool): + # Note: this check must come before the number check because in Python + # True and False are also considered numbers. + struct_value.bool_value = value + elif isinstance(value, str): + struct_value.string_value = value + elif isinstance(value, (int, float)): + struct_value.number_value = value + elif isinstance(value, (dict, Struct)): + struct_value.struct_value.Clear() + struct_value.struct_value.update(value) + elif isinstance(value, (list, ListValue)): + struct_value.list_value.Clear() + struct_value.list_value.extend(value) + else: + raise ValueError('Unexpected type') + + +def _GetStructValue(struct_value): + which = struct_value.WhichOneof('kind') + if which == 'struct_value': + return struct_value.struct_value + elif which == 'null_value': + return None + elif which == 'number_value': + return struct_value.number_value + elif which == 'string_value': + return struct_value.string_value + elif which == 'bool_value': + return struct_value.bool_value + elif which == 'list_value': + return struct_value.list_value + elif which is None: + raise ValueError('Value not set') + + +class Struct(object): + """Class for Struct message type.""" + + __slots__ = () + + def __getitem__(self, key): + return _GetStructValue(self.fields[key]) + + def __contains__(self, item): + return item in self.fields + + def __setitem__(self, key, value): + _SetStructValue(self.fields[key], value) + + def __delitem__(self, key): + del self.fields[key] + + def __len__(self): + return len(self.fields) + + def __iter__(self): + return iter(self.fields) + + def keys(self): # pylint: disable=invalid-name + return self.fields.keys() + + def values(self): # pylint: disable=invalid-name + return [self[key] for key in self] + + def items(self): # pylint: disable=invalid-name + return [(key, self[key]) for key in self] + + def get_or_create_list(self, key): + """Returns a list for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('list_value'): + # Clear will mark list_value modified which will indeed create a list. + self.fields[key].list_value.Clear() + return self.fields[key].list_value + + def get_or_create_struct(self, key): + """Returns a struct for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('struct_value'): + # Clear will mark struct_value modified which will indeed create a struct. + self.fields[key].struct_value.Clear() + return self.fields[key].struct_value + + def update(self, dictionary): # pylint: disable=invalid-name + for key, value in dictionary.items(): + _SetStructValue(self.fields[key], value) + +collections.abc.MutableMapping.register(Struct) + + +class ListValue(object): + """Class for ListValue message type.""" + + __slots__ = () + + def __len__(self): + return len(self.values) + + def append(self, value): + _SetStructValue(self.values.add(), value) + + def extend(self, elem_seq): + for value in elem_seq: + self.append(value) + + def __getitem__(self, index): + """Retrieves item by the specified index.""" + return _GetStructValue(self.values.__getitem__(index)) + + def __setitem__(self, index, value): + _SetStructValue(self.values.__getitem__(index), value) + + def __delitem__(self, key): + del self.values[key] + + def items(self): + for i in range(len(self)): + yield self[i] + + def add_struct(self): + """Appends and returns a struct value as the next value in the list.""" + struct_value = self.values.add().struct_value + # Clear will mark struct_value modified which will indeed create a struct. + struct_value.Clear() + return struct_value + + def add_list(self): + """Appends and returns a list value as the next value in the list.""" + list_value = self.values.add().list_value + # Clear will mark list_value modified which will indeed create a list. + list_value.Clear() + return list_value + +collections.abc.MutableSequence.register(ListValue) + + +WKTBASES = { + 'google.protobuf.Any': Any, + 'google.protobuf.Duration': Duration, + 'google.protobuf.FieldMask': FieldMask, + 'google.protobuf.ListValue': ListValue, + 'google.protobuf.Struct': Struct, + 'google.protobuf.Timestamp': Timestamp, +} diff --git a/openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py b/openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py new file mode 100644 index 0000000000..883f525585 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py @@ -0,0 +1,268 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Constants and static functions to support protocol buffer wire format.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import struct +from google.protobuf import descriptor +from google.protobuf import message + + +TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. +TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 + +# These numbers identify the wire type of a protocol buffer value. +# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded +# tag-and-type to store one of these WIRETYPE_* constants. +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_VARINT = 0 +WIRETYPE_FIXED64 = 1 +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 +WIRETYPE_END_GROUP = 4 +WIRETYPE_FIXED32 = 5 +_WIRETYPE_MAX = 5 + + +# Bounds for various integer types. +INT32_MAX = int((1 << 31) - 1) +INT32_MIN = int(-(1 << 31)) +UINT32_MAX = (1 << 32) - 1 + +INT64_MAX = (1 << 63) - 1 +INT64_MIN = -(1 << 63) +UINT64_MAX = (1 << 64) - 1 + +# "struct" format strings that will encode/decode the specified formats. +FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) + + +def ZigZagEncode(value): + """ZigZag Transform: Encodes signed integers so that they can be + effectively used with varint encoding. See wire_format.h for + more details. + """ + if value >= 0: + return value << 1 + return (value << 1) ^ (~0) + + +def ZigZagDecode(value): + """Inverse of ZigZagEncode().""" + if not value & 0x1: + return value >> 1 + return (value >> 1) ^ (~0) + + + +# The *ByteSize() functions below return the number of bytes required to +# serialize "field number + type" information and then serialize the value. + + +def Int32ByteSize(field_number, int32): + return Int64ByteSize(field_number, int32) + + +def Int32ByteSizeNoTag(int32): + return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) + + +def Int64ByteSize(field_number, int64): + # Have to convert to uint before calling UInt64ByteSize(). + return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) + + +def UInt32ByteSize(field_number, uint32): + return UInt64ByteSize(field_number, uint32) + + +def UInt64ByteSize(field_number, uint64): + return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) + + +def SInt32ByteSize(field_number, int32): + return UInt32ByteSize(field_number, ZigZagEncode(int32)) + + +def SInt64ByteSize(field_number, int64): + return UInt64ByteSize(field_number, ZigZagEncode(int64)) + + +def Fixed32ByteSize(field_number, fixed32): + return TagByteSize(field_number) + 4 + + +def Fixed64ByteSize(field_number, fixed64): + return TagByteSize(field_number) + 8 + + +def SFixed32ByteSize(field_number, sfixed32): + return TagByteSize(field_number) + 4 + + +def SFixed64ByteSize(field_number, sfixed64): + return TagByteSize(field_number) + 8 + + +def FloatByteSize(field_number, flt): + return TagByteSize(field_number) + 4 + + +def DoubleByteSize(field_number, double): + return TagByteSize(field_number) + 8 + + +def BoolByteSize(field_number, b): + return TagByteSize(field_number) + 1 + + +def EnumByteSize(field_number, enum): + return UInt32ByteSize(field_number, enum) + + +def StringByteSize(field_number, string): + return BytesByteSize(field_number, string.encode('utf-8')) + + +def BytesByteSize(field_number, b): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(len(b)) + + len(b)) + + +def GroupByteSize(field_number, message): + return (2 * TagByteSize(field_number) # START and END group. + + message.ByteSize()) + + +def MessageByteSize(field_number, message): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(message.ByteSize()) + + message.ByteSize()) + + +def MessageSetItemByteSize(field_number, msg): + # First compute the sizes of the tags. + # There are 2 tags for the beginning and ending of the repeated group, that + # is field number 1, one with field number 2 (type_id) and one with field + # number 3 (message). + total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) + + # Add the number of bytes for type_id. + total_size += _VarUInt64ByteSizeNoTag(field_number) + + message_size = msg.ByteSize() + + # The number of bytes for encoding the length of the message. + total_size += _VarUInt64ByteSizeNoTag(message_size) + + # The size of the message. + total_size += message_size + return total_size + + +def TagByteSize(field_number): + """Returns the bytes required to serialize a tag with this field number.""" + # Just pass in type 0, since the type won't affect the tag+type size. + return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) + + +# Private helper function for the *ByteSize() functions above. + +def _VarUInt64ByteSizeNoTag(uint64): + """Returns the number of bytes required to serialize a single varint + using boundary value comparisons. (unrolled loop optimization -WPierce) + uint64 must be unsigned. + """ + if uint64 <= 0x7f: return 1 + if uint64 <= 0x3fff: return 2 + if uint64 <= 0x1fffff: return 3 + if uint64 <= 0xfffffff: return 4 + if uint64 <= 0x7ffffffff: return 5 + if uint64 <= 0x3ffffffffff: return 6 + if uint64 <= 0x1ffffffffffff: return 7 + if uint64 <= 0xffffffffffffff: return 8 + if uint64 <= 0x7fffffffffffffff: return 9 + if uint64 > UINT64_MAX: + raise message.EncodeError('Value out of range: %d' % uint64) + return 10 + + +NON_PACKABLE_TYPES = ( + descriptor.FieldDescriptor.TYPE_STRING, + descriptor.FieldDescriptor.TYPE_GROUP, + descriptor.FieldDescriptor.TYPE_MESSAGE, + descriptor.FieldDescriptor.TYPE_BYTES +) + + +def IsTypePackable(field_type): + """Return true iff packable = true is valid for fields of this type. + + Args: + field_type: a FieldDescriptor::Type value. + + Returns: + True iff fields of this type are packable. + """ + return field_type not in NON_PACKABLE_TYPES diff --git a/openpype/hosts/nuke/vendor/google/protobuf/json_format.py b/openpype/hosts/nuke/vendor/google/protobuf/json_format.py new file mode 100644 index 0000000000..5024ed89d7 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/json_format.py @@ -0,0 +1,912 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in JSON format. + +Simple usage example: + + # Create a proto object and serialize it to a json format string. + message = my_proto_pb2.MyMessage(foo='bar') + json_string = json_format.MessageToJson(message) + + # Parse a json format string to proto object. + message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + + +import base64 +from collections import OrderedDict +import json +import math +from operator import methodcaller +import re +import sys + +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import symbol_database + + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, + descriptor.FieldDescriptor.CPPTYPE_UINT32, + descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, + descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) +_INFINITY = 'Infinity' +_NEG_INFINITY = '-Infinity' +_NAN = 'NaN' + +_UNPAIRED_SURROGATE_PATTERN = re.compile( + u'[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: + raise ParseError('Message too deep. Max recursion depth is {0}'.format( + self.max_recursion_depth)) + message_descriptor = message.DESCRIPTOR + full_name = message_descriptor.full_name + if not path: + path = message_descriptor.name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value, message, path) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) + else: + self._ConvertFieldValuePair(value, message, path) + self.recursion_depth -= 1 + + def _ConvertFieldValuePair(self, js, message, path): + """Convert field value pairs into regular message. + + Args: + js: A JSON object to convert the field value pairs. + message: A regular protocol message to record the data. + path: parent path to log parse error info. + + Raises: + ParseError: In case of problems converting. + """ + names = [] + message_descriptor = message.DESCRIPTOR + fields_by_json_name = dict((f.json_name, f) + for f in message_descriptor.fields) + for name in js: + try: + field = fields_by_json_name.get(name, None) + if not field: + field = message_descriptor.fields_by_name.get(name, None) + if not field and _VALID_EXTENSION_NAME.match(name): + if not message_descriptor.is_extendable: + raise ParseError( + 'Message type {0} does not have extensions at {1}'.format( + message_descriptor.full_name, path)) + identifier = name[1:-1] # strip [] brackets + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + # Try looking for extension by the message type name, dropping the + # field name following the final . separator in full_name. + identifier = '.'.join(identifier.split('.')[:-1]) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + if self.ignore_unknown_fields: + continue + raise ParseError( + ('Message type "{0}" has no field named "{1}" at "{2}".\n' + ' Available Fields(except extensions): "{3}"').format( + message_descriptor.full_name, name, path, + [f.json_name for f in message_descriptor.fields])) + if name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" fields at "{2}".'.format( + message.DESCRIPTOR.full_name, name, path)) + names.append(name) + value = js[name] + # Check no other oneof field is parsed. + if field.containing_oneof is not None and value is not None: + oneof_name = field.containing_oneof.name + if oneof_name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" oneof fields at "{2}".'.format( + message.DESCRIPTOR.full_name, oneof_name, + path)) + names.append(oneof_name) + + if value is None: + if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.message_type.full_name == 'google.protobuf.Value'): + sub_message = getattr(message, field.name) + sub_message.null_value = 0 + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM + and field.enum_type.full_name == 'google.protobuf.NullValue'): + setattr(message, field.name, 0) + else: + message.ClearField(field.name) + continue + + # Parse field value. + if _IsMapEntry(field): + message.ClearField(field.name) + self._ConvertMapFieldValue(value, message, field, + '{0}.{1}'.format(path, name)) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + message.ClearField(field.name) + if not isinstance(value, list): + raise ParseError('repeated field {0} must be in [] which is ' + '{1} at {2}'.format(name, value, path)) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + # Repeated message field. + for index, item in enumerate(value): + sub_message = getattr(message, field.name).add() + # None is a null_value in Value. + if (item is None and + sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + self.ConvertMessage(item, sub_message, + '{0}.{1}[{2}]'.format(path, name, index)) + else: + # Repeated scalar field. + for index, item in enumerate(value): + if item is None: + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + getattr(message, field.name).append( + _ConvertScalarFieldValue( + item, field, '{0}.{1}[{2}]'.format(path, name, index))) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + sub_message = message.Extensions[field] + else: + sub_message = getattr(message, field.name) + sub_message.SetInParent() + self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) + else: + if field.is_extension: + message.Extensions[field] = _ConvertScalarFieldValue( + value, field, '{0}.{1}'.format(path, name)) + else: + setattr( + message, field.name, + _ConvertScalarFieldValue(value, field, + '{0}.{1}'.format(path, name))) + except ParseError as e: + if field and field.containing_oneof is None: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + else: + raise ParseError(str(e)) + except ValueError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + except TypeError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + + def _ConvertAnyMessage(self, value, message, path): + """Convert a JSON representation into Any message.""" + if isinstance(value, dict) and not value: + return + try: + type_url = value['@type'] + except KeyError: + raise ParseError( + '@type is missing when parsing any message at {0}'.format(path)) + + try: + sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) + except TypeError as e: + raise ParseError('{0} at {1}'.format(e, path)) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value['value'], sub_message, + '{0}.value'.format(path)) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, + '{0}.value'.format(path))( + self) + else: + del value['@type'] + self._ConvertFieldValuePair(value, sub_message, path) + value['@type'] = type_url + # Sets Any message + message.value = sub_message.SerializeToString() + message.type_url = type_url + + def _ConvertGenericMessage(self, value, message, path): + """Convert a JSON representation into message with FromJsonString.""" + # Duration, Timestamp, FieldMask have a FromJsonString method to do the + # conversion. Users can also call the method directly. + try: + message.FromJsonString(value) + except ValueError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + def _ConvertValueMessage(self, value, message, path): + """Convert a JSON representation into Value message.""" + if isinstance(value, dict): + self._ConvertStructMessage(value, message.struct_value, path) + elif isinstance(value, list): + self._ConvertListValueMessage(value, message.list_value, path) + elif value is None: + message.null_value = 0 + elif isinstance(value, bool): + message.bool_value = value + elif isinstance(value, str): + message.string_value = value + elif isinstance(value, _INT_OR_FLOAT): + message.number_value = value + else: + raise ParseError('Value {0} has unexpected type {1} at {2}'.format( + value, type(value), path)) + + def _ConvertListValueMessage(self, value, message, path): + """Convert a JSON representation into ListValue message.""" + if not isinstance(value, list): + raise ParseError('ListValue must be in [] which is {0} at {1}'.format( + value, path)) + message.ClearField('values') + for index, item in enumerate(value): + self._ConvertValueMessage(item, message.values.add(), + '{0}[{1}]'.format(path, index)) + + def _ConvertStructMessage(self, value, message, path): + """Convert a JSON representation into Struct message.""" + if not isinstance(value, dict): + raise ParseError('Struct must be in a dict which is {0} at {1}'.format( + value, path)) + # Clear will mark the struct as modified so it will be created even if + # there are no values. + message.Clear() + for key in value: + self._ConvertValueMessage(value[key], message.fields[key], + '{0}.{1}'.format(path, key)) + return + + def _ConvertWrapperMessage(self, value, message, path): + """Convert a JSON representation into Wrapper message.""" + field = message.DESCRIPTOR.fields_by_name['value'] + setattr( + message, 'value', + _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) + + def _ConvertMapFieldValue(self, value, message, field, path): + """Convert map field value for a message map field. + + Args: + value: A JSON object to convert the map field value. + message: A protocol message to record the converted data. + field: The descriptor of the map field to be converted. + path: parent path to log parse error info. + + Raises: + ParseError: In case of convert problems. + """ + if not isinstance(value, dict): + raise ParseError( + 'Map field {0} must be in a dict which is {1} at {2}'.format( + field.name, value, path)) + key_field = field.message_type.fields_by_name['key'] + value_field = field.message_type.fields_by_name['value'] + for key in value: + key_value = _ConvertScalarFieldValue(key, key_field, + '{0}.key'.format(path), True) + if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self.ConvertMessage(value[key], + getattr(message, field.name)[key_value], + '{0}[{1}]'.format(path, key_value)) + else: + getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( + value[key], value_field, path='{0}[{1}]'.format(path, key_value)) + + +def _ConvertScalarFieldValue(value, field, path, require_str=False): + """Convert a single scalar field value. + + Args: + value: A scalar value to convert the scalar field value. + field: The descriptor of the field to convert. + path: parent path to log parse error info. + require_str: If True, the field value must be a str. + + Returns: + The converted scalar field value + + Raises: + ParseError: In case of convert problems. + """ + try: + if field.cpp_type in _INT_TYPES: + return _ConvertInteger(value) + elif field.cpp_type in _FLOAT_TYPES: + return _ConvertFloat(value, field) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + return _ConvertBool(value, require_str) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + if isinstance(value, str): + encoded = value.encode('utf-8') + else: + encoded = value + # Add extra padding '=' + padded_value = encoded + b'=' * (4 - len(encoded) % 4) + return base64.urlsafe_b64decode(padded_value) + else: + # Checking for unpaired surrogates appears to be unreliable, + # depending on the specific Python version, so we check manually. + if _UNPAIRED_SURROGATE_PATTERN.search(value): + raise ParseError('Unpaired surrogate') + return value + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + # Convert an enum value. + enum_value = field.enum_type.values_by_name.get(value, None) + if enum_value is None: + try: + number = int(value) + enum_value = field.enum_type.values_by_number.get(number, None) + except ValueError: + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + if enum_value is None: + if field.file.syntax == 'proto3': + # Proto3 accepts unknown enums. + return number + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + return enum_value.number + except ParseError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + +def _ConvertInteger(value): + """Convert an integer. + + Args: + value: A scalar value to convert. + + Returns: + The integer value. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + if isinstance(value, float) and not value.is_integer(): + raise ParseError('Couldn\'t parse integer: {0}'.format(value)) + + if isinstance(value, str) and value.find(' ') != -1: + raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) + + if isinstance(value, bool): + raise ParseError('Bool value {0} is not acceptable for ' + 'integer field'.format(value)) + + return int(value) + + +def _ConvertFloat(value, field): + """Convert an floating point number.""" + if isinstance(value, float): + if math.isnan(value): + raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') + if math.isinf(value): + if value > 0: + raise ParseError('Couldn\'t parse Infinity or value too large, ' + 'use quoted "Infinity" instead') + else: + raise ParseError('Couldn\'t parse -Infinity or value too small, ' + 'use quoted "-Infinity" instead') + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + # pylint: disable=protected-access + if value > type_checkers._FLOAT_MAX: + raise ParseError('Float value too large') + # pylint: disable=protected-access + if value < type_checkers._FLOAT_MIN: + raise ParseError('Float value too small') + if value == 'nan': + raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') + try: + # Assume Python compatible syntax. + return float(value) + except ValueError: + # Check alternative spellings. + if value == _NEG_INFINITY: + return float('-inf') + elif value == _INFINITY: + return float('inf') + elif value == _NAN: + return float('nan') + else: + raise ParseError('Couldn\'t parse float: {0}'.format(value)) + + +def _ConvertBool(value, require_str): + """Convert a boolean value. + + Args: + value: A scalar value to convert. + require_str: If True, value must be a str. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + if require_str: + if value == 'true': + return True + elif value == 'false': + return False + else: + raise ParseError('Expected "true" or "false", not {0}'.format(value)) + + if not isinstance(value, bool): + raise ParseError('Expected true or false without quotes') + return value + +_WKTJSONMETHODS = { + 'google.protobuf.Any': ['_AnyMessageToJsonObject', + '_ConvertAnyMessage'], + 'google.protobuf.Duration': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', + '_ConvertListValueMessage'], + 'google.protobuf.Struct': ['_StructMessageToJsonObject', + '_ConvertStructMessage'], + 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.Value': ['_ValueMessageToJsonObject', + '_ConvertValueMessage'] +} diff --git a/openpype/hosts/nuke/vendor/google/protobuf/message.py b/openpype/hosts/nuke/vendor/google/protobuf/message.py new file mode 100644 index 0000000000..76c6802f70 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/message.py @@ -0,0 +1,424 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# TODO(robinson): We should just make these methods all "pure-virtual" and move +# all implementation out, into reflection.py for now. + + +"""Contains an abstract base class for protocol messages.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +class Error(Exception): + """Base error type for this module.""" + pass + + +class DecodeError(Error): + """Exception raised when deserializing messages.""" + pass + + +class EncodeError(Error): + """Exception raised when serializing messages.""" + pass + + +class Message(object): + + """Abstract base class for protocol messages. + + Protocol message classes are almost always generated by the protocol + compiler. These generated types subclass Message and implement the methods + shown below. + """ + + # TODO(robinson): Link to an HTML document here. + + # TODO(robinson): Document that instances of this class will also + # have an Extensions attribute with __getitem__ and __setitem__. + # Again, not sure how to best convey this. + + # TODO(robinson): Document that the class must also have a static + # RegisterExtension(extension_field) method. + # Not sure how to best express at this point. + + # TODO(robinson): Document these fields and methods. + + __slots__ = [] + + #: The :class:`google.protobuf.descriptor.Descriptor` for this message type. + DESCRIPTOR = None + + def __deepcopy__(self, memo=None): + clone = type(self)() + clone.MergeFrom(self) + return clone + + def __eq__(self, other_msg): + """Recursively compares two messages by value and structure.""" + raise NotImplementedError + + def __ne__(self, other_msg): + # Can't just say self != other_msg, since that would infinitely recurse. :) + return not self == other_msg + + def __hash__(self): + raise TypeError('unhashable object') + + def __str__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def __unicode__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def MergeFrom(self, other_msg): + """Merges the contents of the specified message into current message. + + This method merges the contents of the specified message into the current + message. Singular fields that are set in the specified message overwrite + the corresponding fields in the current message. Repeated fields are + appended. Singular sub-messages and groups are recursively merged. + + Args: + other_msg (Message): A message to merge into the current message. + """ + raise NotImplementedError + + def CopyFrom(self, other_msg): + """Copies the content of the specified message into the current message. + + The method clears the current message and then merges the specified + message using MergeFrom. + + Args: + other_msg (Message): A message to copy into the current one. + """ + if self is other_msg: + return + self.Clear() + self.MergeFrom(other_msg) + + def Clear(self): + """Clears all data that was set in the message.""" + raise NotImplementedError + + def SetInParent(self): + """Mark this as present in the parent. + + This normally happens automatically when you assign a field of a + sub-message, but sometimes you want to make the sub-message + present while keeping it empty. If you find yourself using this, + you may want to reconsider your design. + """ + raise NotImplementedError + + def IsInitialized(self): + """Checks if the message is initialized. + + Returns: + bool: The method returns True if the message is initialized (i.e. all of + its required fields are set). + """ + raise NotImplementedError + + # TODO(robinson): MergeFromString() should probably return None and be + # implemented in terms of a helper that returns the # of bytes read. Our + # deserialization routines would use the helper when recursively + # deserializing, but the end user would almost always just want the no-return + # MergeFromString(). + + def MergeFromString(self, serialized): + """Merges serialized protocol buffer data into this message. + + When we find a field in `serialized` that is already present + in this message: + + - If it's a "repeated" field, we append to the end of our list. + - Else, if it's a scalar, we overwrite our field. + - Else, (it's a nonrepeated composite), we recursively merge + into the existing composite. + + Args: + serialized (bytes): Any object that allows us to call + ``memoryview(serialized)`` to access a string of bytes using the + buffer interface. + + Returns: + int: The number of bytes read from `serialized`. + For non-group messages, this will always be `len(serialized)`, + but for messages which are actually groups, this will + generally be less than `len(serialized)`, since we must + stop when we reach an ``END_GROUP`` tag. Note that if + we *do* stop because of an ``END_GROUP`` tag, the number + of bytes returned does not include the bytes + for the ``END_GROUP`` tag information. + + Raises: + DecodeError: if the input cannot be parsed. + """ + # TODO(robinson): Document handling of unknown fields. + # TODO(robinson): When we switch to a helper, this will return None. + raise NotImplementedError + + def ParseFromString(self, serialized): + """Parse serialized protocol buffer data into this message. + + Like :func:`MergeFromString()`, except we clear the object first. + + Raises: + message.DecodeError if the input cannot be parsed. + """ + self.Clear() + return self.MergeFromString(serialized) + + def SerializeToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + A binary string representation of the message if all of the required + fields in the message are set (i.e. the message is initialized). + + Raises: + EncodeError: if the message isn't initialized (see :func:`IsInitialized`). + """ + raise NotImplementedError + + def SerializePartialToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + This method is similar to SerializeToString but doesn't check if the + message is initialized. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + bytes: A serialized representation of the partial message. + """ + raise NotImplementedError + + # TODO(robinson): Decide whether we like these better + # than auto-generated has_foo() and clear_foo() methods + # on the instances themselves. This way is less consistent + # with C++, but it makes reflection-type access easier and + # reduces the number of magically autogenerated things. + # + # TODO(robinson): Be sure to document (and test) exactly + # which field names are accepted here. Are we case-sensitive? + # What do we do with fields that share names with Python keywords + # like 'lambda' and 'yield'? + # + # nnorwitz says: + # """ + # Typically (in python), an underscore is appended to names that are + # keywords. So they would become lambda_ or yield_. + # """ + def ListFields(self): + """Returns a list of (FieldDescriptor, value) tuples for present fields. + + A message field is non-empty if HasField() would return true. A singular + primitive field is non-empty if HasField() would return true in proto2 or it + is non zero in proto3. A repeated field is non-empty if it contains at least + one element. The fields are ordered by field number. + + Returns: + list[tuple(FieldDescriptor, value)]: field descriptors and values + for all fields in the message which are not empty. The values vary by + field type. + """ + raise NotImplementedError + + def HasField(self, field_name): + """Checks if a certain field is set for the message. + + For a oneof group, checks if any field inside is set. Note that if the + field_name is not defined in the message descriptor, :exc:`ValueError` will + be raised. + + Args: + field_name (str): The name of the field to check for presence. + + Returns: + bool: Whether a value has been set for the named field. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def ClearField(self, field_name): + """Clears the contents of a given field. + + Inside a oneof group, clears the field set. If the name neither refers to a + defined field or oneof group, :exc:`ValueError` is raised. + + Args: + field_name (str): The name of the field to check for presence. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def WhichOneof(self, oneof_group): + """Returns the name of the field that is set inside a oneof group. + + If no field is set, returns None. + + Args: + oneof_group (str): the name of the oneof group to check. + + Returns: + str or None: The name of the group that is set, or None. + + Raises: + ValueError: no group with the given name exists + """ + raise NotImplementedError + + def HasExtension(self, extension_handle): + """Checks if a certain extension is present for this message. + + Extensions are retrieved using the :attr:`Extensions` mapping (if present). + + Args: + extension_handle: The handle for the extension to check. + + Returns: + bool: Whether the extension is present for this message. + + Raises: + KeyError: if the extension is repeated. Similar to repeated fields, + there is no separate notion of presence: a "not present" repeated + extension is an empty list. + """ + raise NotImplementedError + + def ClearExtension(self, extension_handle): + """Clears the contents of a given extension. + + Args: + extension_handle: The handle for the extension to clear. + """ + raise NotImplementedError + + def UnknownFields(self): + """Returns the UnknownFieldSet. + + Returns: + UnknownFieldSet: The unknown fields stored in this message. + """ + raise NotImplementedError + + def DiscardUnknownFields(self): + """Clears all fields in the :class:`UnknownFieldSet`. + + This operation is recursive for nested message. + """ + raise NotImplementedError + + def ByteSize(self): + """Returns the serialized size of this message. + + Recursively calls ByteSize() on all contained messages. + + Returns: + int: The number of bytes required to serialize this message. + """ + raise NotImplementedError + + @classmethod + def FromString(cls, s): + raise NotImplementedError + + @staticmethod + def RegisterExtension(extension_handle): + raise NotImplementedError + + def _SetListener(self, message_listener): + """Internal method used by the protocol message implementation. + Clients should not call this directly. + + Sets a listener that this message will call on certain state transitions. + + The purpose of this method is to register back-edges from children to + parents at runtime, for the purpose of setting "has" bits and + byte-size-dirty bits in the parent and ancestor objects whenever a child or + descendant object is modified. + + If the client wants to disconnect this Message from the object tree, she + explicitly sets callback to None. + + If message_listener is None, unregisters any existing listener. Otherwise, + message_listener must implement the MessageListener interface in + internal/message_listener.py, and we discard any listener registered + via a previous _SetListener() call. + """ + raise NotImplementedError + + def __getstate__(self): + """Support the pickle protocol.""" + return dict(serialized=self.SerializePartialToString()) + + def __setstate__(self, state): + """Support the pickle protocol.""" + self.__init__() + serialized = state['serialized'] + # On Python 3, using encoding='latin1' is required for unpickling + # protos pickled by Python 2. + if not isinstance(serialized, bytes): + serialized = serialized.encode('latin1') + self.ParseFromString(serialized) + + def __reduce__(self): + message_descriptor = self.DESCRIPTOR + if message_descriptor.containing_type is None: + return type(self), (), self.__getstate__() + # the message type must be nested. + # Python does not pickle nested classes; use the symbol_database on the + # receiving end. + container = message_descriptor + return (_InternalConstructMessage, (container.full_name,), + self.__getstate__()) + + +def _InternalConstructMessage(full_name): + """Constructs a nested message.""" + from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top + + return symbol_database.Default().GetSymbol(full_name)() diff --git a/openpype/hosts/nuke/vendor/google/protobuf/message_factory.py b/openpype/hosts/nuke/vendor/google/protobuf/message_factory.py new file mode 100644 index 0000000000..3656fa6874 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/message_factory.py @@ -0,0 +1,185 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a factory class for generating dynamic messages. + +The easiest way to use this class is if you have access to the FileDescriptor +protos containing the messages you want to create you can just do the following: + +message_classes = message_factory.GetMessages(iterable_of_file_descriptors) +my_proto_instance = message_classes['some.proto.package.MessageName']() +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message + +if api_implementation.Type() == 'cpp': + from google.protobuf.pyext import cpp_message as message_impl +else: + from google.protobuf.internal import python_message as message_impl + + +# The type of all Message classes. +_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType + + +class MessageFactory(object): + """Factory for creating Proto2 messages from descriptors in a pool.""" + + def __init__(self, pool=None): + """Initializes a new factory.""" + self.pool = pool or descriptor_pool.DescriptorPool() + + # local cache of all classes built from protobuf descriptors + self._classes = {} + + def GetPrototype(self, descriptor): + """Obtains a proto2 message class based on the passed in descriptor. + + Passing a descriptor with a fully qualified name matching a previous + invocation will cause the same class to be returned. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + if descriptor not in self._classes: + result_class = self.CreatePrototype(descriptor) + # The assignment to _classes is redundant for the base implementation, but + # might avoid confusion in cases where CreatePrototype gets overridden and + # does not call the base implementation. + self._classes[descriptor] = result_class + return result_class + return self._classes[descriptor] + + def CreatePrototype(self, descriptor): + """Builds a proto2 message class based on the passed in descriptor. + + Don't call this function directly, it always creates a new class. Call + GetPrototype() instead. This method is meant to be overridden in subblasses + to perform additional operations on the newly constructed class. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + descriptor_name = descriptor.name + result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( + descriptor_name, + (message.Message,), + { + 'DESCRIPTOR': descriptor, + # If module not set, it wrongly points to message_factory module. + '__module__': None, + }) + result_class._FACTORY = self # pylint: disable=protected-access + # Assign in _classes before doing recursive calls to avoid infinite + # recursion. + self._classes[descriptor] = result_class + for field in descriptor.fields: + if field.message_type: + self.GetPrototype(field.message_type) + for extension in result_class.DESCRIPTOR.extensions: + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result_class + + def GetMessages(self, files): + """Gets all the messages from a specified file. + + This will find and resolve dependencies, failing if the descriptor + pool cannot satisfy them. + + Args: + files: The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for desc in file_desc.message_types_by_name.values(): + result[desc.full_name] = self.GetPrototype(desc) + + # While the extension FieldDescriptors are created by the descriptor pool, + # the python classes created in the factory need them to be registered + # explicitly, which is done below. + # + # The call to RegisterExtension will specifically check if the + # extension was already registered on the object and either + # ignore the registration if the original was the same, or raise + # an error if they were different. + + for extension in file_desc.extensions_by_name.values(): + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result + + +_FACTORY = MessageFactory() + + +def GetMessages(file_protos): + """Builds a dictionary of all the messages available in a set of files. + + Args: + file_protos: Iterable of FileDescriptorProto to build messages out of. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + # The cpp implementation of the protocol buffer library requires to add the + # message in topological order of the dependency graph. + file_by_name = {file_proto.name: file_proto for file_proto in file_protos} + def _AddFile(file_proto): + for dependency in file_proto.dependency: + if dependency in file_by_name: + # Remove from elements to be visited, in order to cut cycles. + _AddFile(file_by_name.pop(dependency)) + _FACTORY.pool.Add(file_proto) + while file_by_name: + _AddFile(file_by_name.popitem()[1]) + return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos]) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py b/openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py new file mode 100644 index 0000000000..a4667ce63e --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py @@ -0,0 +1,134 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Dynamic Protobuf class creator.""" + +from collections import OrderedDict +import hashlib +import os + +from google.protobuf import descriptor_pb2 +from google.protobuf import descriptor +from google.protobuf import message_factory + + +def _GetMessageFromFactory(factory, full_name): + """Get a proto class from the MessageFactory by name. + + Args: + factory: a MessageFactory instance. + full_name: str, the fully qualified name of the proto type. + Returns: + A class, for the type identified by full_name. + Raises: + KeyError, if the proto is not found in the factory's descriptor pool. + """ + proto_descriptor = factory.pool.FindMessageTypeByName(full_name) + proto_cls = factory.GetPrototype(proto_descriptor) + return proto_cls + + +def MakeSimpleProtoClass(fields, full_name=None, pool=None): + """Create a Protobuf class whose fields are basic types. + + Note: this doesn't validate field names! + + Args: + fields: dict of {name: field_type} mappings for each field in the proto. If + this is an OrderedDict the order will be maintained, otherwise the + fields will be sorted by name. + full_name: optional str, the fully-qualified name of the proto type. + pool: optional DescriptorPool instance. + Returns: + a class, the new protobuf class with a FileDescriptor. + """ + factory = message_factory.MessageFactory(pool=pool) + + if full_name is not None: + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # Get a list of (name, field_type) tuples from the fields dict. If fields was + # an OrderedDict we keep the order, but otherwise we sort the field to ensure + # consistent ordering. + field_items = fields.items() + if not isinstance(fields, OrderedDict): + field_items = sorted(field_items) + + # Use a consistent file name that is unlikely to conflict with any imported + # proto files. + fields_hash = hashlib.sha1() + for f_name, f_type in field_items: + fields_hash.update(f_name.encode('utf-8')) + fields_hash.update(str(f_type).encode('utf-8')) + proto_file_name = fields_hash.hexdigest() + '.proto' + + # If the proto is anonymous, use the same hash to name it. + if full_name is None: + full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + + fields_hash.hexdigest()) + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # This is the first time we see this proto: add a new descriptor to the pool. + factory.pool.Add( + _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) + return _GetMessageFromFactory(factory, full_name) + + +def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): + """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" + package, name = full_name.rsplit('.', 1) + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) + file_proto.package = package + desc_proto = file_proto.message_type.add() + desc_proto.name = name + for f_number, (f_name, f_type) in enumerate(field_items, 1): + field_proto = desc_proto.field.add() + field_proto.name = f_name + # # If the number falls in the reserved range, reassign it to the correct + # # number after the range. + if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: + f_number += ( + descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - + descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) + field_proto.number = f_number + field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + field_proto.type = f_type + return file_proto diff --git a/openpype/hosts/nuke/vendor/google/protobuf/pyext/__init__.py b/openpype/hosts/nuke/vendor/google/protobuf/pyext/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py b/openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py new file mode 100644 index 0000000000..fc8eb32d79 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py @@ -0,0 +1,65 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Protocol message implementation hooks for C++ implementation. + +Contains helper functions used to create protocol message classes from +Descriptor objects at runtime backed by the protocol buffer C++ API. +""" + +__author__ = 'tibell@google.com (Johan Tibell)' + +from google.protobuf.pyext import _message + + +class GeneratedProtocolMessageType(_message.MessageMeta): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + + The above example will not work for nested types. If you wish to include them, + use reflection.MakeClass() instead of manually instantiating the class in + order to create the appropriate class structure. + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' diff --git a/openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py new file mode 100644 index 0000000000..2c6ecf4c98 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/pyext/python.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"google/protobuf/pyext/python.proto\x12\x1fgoogle.protobuf.python.internal\"\xbc\x02\n\x0cTestAllTypes\x12\\\n\x17repeated_nested_message\x18\x01 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\\\n\x17optional_nested_message\x18\x02 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\x16\n\x0eoptional_int32\x18\x03 \x01(\x05\x1aX\n\rNestedMessage\x12\n\n\x02\x62\x62\x18\x01 \x01(\x05\x12;\n\x02\x63\x63\x18\x02 \x01(\x0b\x32/.google.protobuf.python.internal.ForeignMessage\"&\n\x0e\x46oreignMessage\x12\t\n\x01\x63\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x03(\x05\"\x1d\n\x11TestAllExtensions*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x9a\x01\n!optional_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x01 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\x9a\x01\n!repeated_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x02 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\x02H\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestAllExtensions.RegisterExtension(optional_nested_message_extension) + TestAllExtensions.RegisterExtension(repeated_nested_message_extension) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'H\001' + _TESTALLTYPES._serialized_start=72 + _TESTALLTYPES._serialized_end=388 + _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300 + _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388 + _FOREIGNMESSAGE._serialized_start=390 + _FOREIGNMESSAGE._serialized_end=428 + _TESTALLEXTENSIONS._serialized_start=430 + _TESTALLEXTENSIONS._serialized_end=459 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/reflection.py b/openpype/hosts/nuke/vendor/google/protobuf/reflection.py new file mode 100644 index 0000000000..81e18859a8 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/reflection.py @@ -0,0 +1,95 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +from google.protobuf import message_factory +from google.protobuf import symbol_database + +# The type of all Message classes. +# Part of the public interface, but normally only used by message factories. +GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE + +MESSAGE_CLASS_CACHE = {} + + +# Deprecated. Please NEVER use reflection.ParseMessage(). +def ParseMessage(descriptor, byte_str): + """Generate a new Message instance from this Descriptor and a byte string. + + DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). + Please use MessageFactory.GetPrototype() instead. + + Args: + descriptor: Protobuf Descriptor object + byte_str: Serialized protocol buffer byte string + + Returns: + Newly created protobuf Message object. + """ + result_class = MakeClass(descriptor) + new_msg = result_class() + new_msg.ParseFromString(byte_str) + return new_msg + + +# Deprecated. Please NEVER use reflection.MakeClass(). +def MakeClass(descriptor): + """Construct a class object for a protobuf described by descriptor. + + DEPRECATED: use MessageFactory.GetPrototype() instead. + + Args: + descriptor: A descriptor.Descriptor object describing the protobuf. + Returns: + The Message class object described by the descriptor. + """ + # Original implementation leads to duplicate message classes, which won't play + # well with extensions. Message factory info is also missing. + # Redirect to message_factory. + return symbol_database.Default().GetPrototype(descriptor) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/service.py b/openpype/hosts/nuke/vendor/google/protobuf/service.py new file mode 100644 index 0000000000..5625246324 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/service.py @@ -0,0 +1,228 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""DEPRECATED: Declares the RPC service interfaces. + +This module declares the abstract interfaces underlying proto2 RPC +services. These are intended to be independent of any particular RPC +implementation, so that proto2 services can be used on top of a variety +of implementations. Starting with version 2.3.0, RPC implementations should +not try to build on these, but should instead provide code generator plugins +which generate code specific to the particular RPC implementation. This way +the generated code can be more appropriate for the implementation in use +and can avoid unnecessary layers of indirection. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class RpcException(Exception): + """Exception raised on failed blocking RPC method call.""" + pass + + +class Service(object): + + """Abstract base interface for protocol-buffer-based RPC services. + + Services themselves are abstract classes (implemented either by servers or as + stubs), but they subclass this base interface. The methods of this + interface can be used to call the methods of the service without knowing + its exact type at compile time (analogous to the Message interface). + """ + + def GetDescriptor(): + """Retrieves this service's descriptor.""" + raise NotImplementedError + + def CallMethod(self, method_descriptor, rpc_controller, + request, done): + """Calls a method of the service specified by method_descriptor. + + If "done" is None then the call is blocking and the response + message will be returned directly. Otherwise the call is asynchronous + and "done" will later be called with the response value. + + In the blocking case, RpcException will be raised on error. + + Preconditions: + + * method_descriptor.service == GetDescriptor + * request is of the exact same classes as returned by + GetRequestClass(method). + * After the call has started, the request must not be modified. + * "rpc_controller" is of the correct type for the RPC implementation being + used by this Service. For stubs, the "correct type" depends on the + RpcChannel which the stub is using. + + Postconditions: + + * "done" will be called when the method is complete. This may be + before CallMethod() returns or it may be at some point in the future. + * If the RPC failed, the response value passed to "done" will be None. + Further details about the failure can be found by querying the + RpcController. + """ + raise NotImplementedError + + def GetRequestClass(self, method_descriptor): + """Returns the class of the request message for the specified method. + + CallMethod() requires that the request is of a particular subclass of + Message. GetRequestClass() gets the default instance of this required + type. + + Example: + method = service.GetDescriptor().FindMethodByName("Foo") + request = stub.GetRequestClass(method)() + request.ParseFromString(input) + service.CallMethod(method, request, callback) + """ + raise NotImplementedError + + def GetResponseClass(self, method_descriptor): + """Returns the class of the response message for the specified method. + + This method isn't really needed, as the RpcChannel's CallMethod constructs + the response protocol message. It's provided anyway in case it is useful + for the caller to know the response type in advance. + """ + raise NotImplementedError + + +class RpcController(object): + + """An RpcController mediates a single method call. + + The primary purpose of the controller is to provide a way to manipulate + settings specific to the RPC implementation and to find out about RPC-level + errors. The methods provided by the RpcController interface are intended + to be a "least common denominator" set of features which we expect all + implementations to support. Specific implementations may provide more + advanced features (e.g. deadline propagation). + """ + + # Client-side methods below + + def Reset(self): + """Resets the RpcController to its initial state. + + After the RpcController has been reset, it may be reused in + a new call. Must not be called while an RPC is in progress. + """ + raise NotImplementedError + + def Failed(self): + """Returns true if the call failed. + + After a call has finished, returns true if the call failed. The possible + reasons for failure depend on the RPC implementation. Failed() must not + be called before a call has finished. If Failed() returns true, the + contents of the response message are undefined. + """ + raise NotImplementedError + + def ErrorText(self): + """If Failed is true, returns a human-readable description of the error.""" + raise NotImplementedError + + def StartCancel(self): + """Initiate cancellation. + + Advises the RPC system that the caller desires that the RPC call be + canceled. The RPC system may cancel it immediately, may wait awhile and + then cancel it, or may not even cancel the call at all. If the call is + canceled, the "done" callback will still be called and the RpcController + will indicate that the call failed at that time. + """ + raise NotImplementedError + + # Server-side methods below + + def SetFailed(self, reason): + """Sets a failure reason. + + Causes Failed() to return true on the client side. "reason" will be + incorporated into the message returned by ErrorText(). If you find + you need to return machine-readable information about failures, you + should incorporate it into your response protocol buffer and should + NOT call SetFailed(). + """ + raise NotImplementedError + + def IsCanceled(self): + """Checks if the client cancelled the RPC. + + If true, indicates that the client canceled the RPC, so the server may + as well give up on replying to it. The server should still call the + final "done" callback. + """ + raise NotImplementedError + + def NotifyOnCancel(self, callback): + """Sets a callback to invoke on cancel. + + Asks that the given callback be called when the RPC is canceled. The + callback will always be called exactly once. If the RPC completes without + being canceled, the callback will be called after completion. If the RPC + has already been canceled when NotifyOnCancel() is called, the callback + will be called immediately. + + NotifyOnCancel() must be called no more than once per request. + """ + raise NotImplementedError + + +class RpcChannel(object): + + """Abstract interface for an RPC channel. + + An RpcChannel represents a communication line to a service which can be used + to call that service's methods. The service may be running on another + machine. Normally, you should not use an RpcChannel directly, but instead + construct a stub {@link Service} wrapping it. Example: + + Example: + RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") + RpcController controller = rpcImpl.Controller() + MyService service = MyService_Stub(channel) + service.MyMethod(controller, request, callback) + """ + + def CallMethod(self, method_descriptor, rpc_controller, + request, response_class, done): + """Calls the method identified by the descriptor. + + Call the given method of the remote service. The signature of this + procedure looks the same as Service.CallMethod(), but the requirements + are less strict in one important way: the request object doesn't have to + be of any specific class as long as its descriptor is method.input_type. + """ + raise NotImplementedError diff --git a/openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py b/openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py new file mode 100644 index 0000000000..f82ab7145a --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py @@ -0,0 +1,295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains metaclasses used to create protocol service and service stub +classes from ServiceDescriptor objects at runtime. + +The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to +inject all useful functionality into the classes output by the protocol +compiler at compile-time. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class GeneratedServiceType(type): + + """Metaclass for service classes created at runtime from ServiceDescriptors. + + Implementations for all methods described in the Service class are added here + by this class. We also create properties to allow getting/setting all fields + in the protocol message. + + The protocol compiler currently uses this metaclass to create protocol service + classes at runtime. Clients can also manually create their own classes at + runtime, as in this example:: + + mydescriptor = ServiceDescriptor(.....) + class MyProtoService(service.Service): + __metaclass__ = GeneratedServiceType + DESCRIPTOR = mydescriptor + myservice_instance = MyProtoService() + # ... + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service class. + + Args: + name: Name of the class (ignored, but required by the metaclass + protocol). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service class is subclassed. + if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] + service_builder = _ServiceBuilder(descriptor) + service_builder.BuildService(cls) + cls.DESCRIPTOR = descriptor + + +class GeneratedServiceStubType(GeneratedServiceType): + + """Metaclass for service stubs created at runtime from ServiceDescriptors. + + This class has similar responsibilities as GeneratedServiceType, except that + it creates the service stub classes. + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service stub class. + + Args: + name: Name of the class (ignored, here). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service stub is subclassed. + if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] + service_stub_builder = _ServiceStubBuilder(descriptor) + service_stub_builder.BuildServiceStub(cls) + + +class _ServiceBuilder(object): + + """This class constructs a protocol service class using a service descriptor. + + Given a service descriptor, this class constructs a class that represents + the specified service descriptor. One service builder instance constructs + exactly one service class. That means all instances of that class share the + same builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + service class. + """ + self.descriptor = service_descriptor + + def BuildService(builder, cls): + """Constructs the service class. + + Args: + cls: The class that will be constructed. + """ + + # CallMethod needs to operate with an instance of the Service class. This + # internal wrapper function exists only to be able to pass the service + # instance to the method that does the real CallMethod work. + # Making sure to use exact argument names from the abstract interface in + # service.py to match the type signature + def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): + return builder._CallMethod(self, method_descriptor, rpc_controller, + request, done) + + def _WrapGetRequestClass(self, method_descriptor): + return builder._GetRequestClass(method_descriptor) + + def _WrapGetResponseClass(self, method_descriptor): + return builder._GetResponseClass(method_descriptor) + + builder.cls = cls + cls.CallMethod = _WrapCallMethod + cls.GetDescriptor = staticmethod(lambda: builder.descriptor) + cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' + cls.GetRequestClass = _WrapGetRequestClass + cls.GetResponseClass = _WrapGetResponseClass + for method in builder.descriptor.methods: + setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) + + def _CallMethod(self, srvc, method_descriptor, + rpc_controller, request, callback): + """Calls the method described by a given method descriptor. + + Args: + srvc: Instance of the service for which this method is called. + method_descriptor: Descriptor that represent the method to call. + rpc_controller: RPC controller to use for this method's execution. + request: Request protocol message. + callback: A callback to invoke after the method has completed. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'CallMethod() given method descriptor for wrong service type.') + method = getattr(srvc, method_descriptor.name) + return method(rpc_controller, request, callback) + + def _GetRequestClass(self, method_descriptor): + """Returns the class of the request protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + request protocol message class. + + Returns: + A class that represents the input protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetRequestClass() given method descriptor for wrong service type.') + return method_descriptor.input_type._concrete_class + + def _GetResponseClass(self, method_descriptor): + """Returns the class of the response protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + response protocol message class. + + Returns: + A class that represents the output protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetResponseClass() given method descriptor for wrong service type.') + return method_descriptor.output_type._concrete_class + + def _GenerateNonImplementedMethod(self, method): + """Generates and returns a method that can be set for a service methods. + + Args: + method: Descriptor of the service method for which a method is to be + generated. + + Returns: + A method that can be added to the service class. + """ + return lambda inst, rpc_controller, request, callback: ( + self._NonImplementedMethod(method.name, rpc_controller, callback)) + + def _NonImplementedMethod(self, method_name, rpc_controller, callback): + """The body of all methods in the generated service class. + + Args: + method_name: Name of the method being executed. + rpc_controller: RPC controller used to execute this method. + callback: A callback which will be invoked when the method finishes. + """ + rpc_controller.SetFailed('Method %s not implemented.' % method_name) + callback(None) + + +class _ServiceStubBuilder(object): + + """Constructs a protocol service stub class using a service descriptor. + + Given a service descriptor, this class constructs a suitable stub class. + A stub is just a type-safe wrapper around an RpcChannel which emulates a + local implementation of the service. + + One service stub builder instance constructs exactly one class. It means all + instances of that class share the same service stub builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service stub class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + stub class. + """ + self.descriptor = service_descriptor + + def BuildServiceStub(self, cls): + """Constructs the stub class. + + Args: + cls: The class that will be constructed. + """ + + def _ServiceStubInit(stub, rpc_channel): + stub.rpc_channel = rpc_channel + self.cls = cls + cls.__init__ = _ServiceStubInit + for method in self.descriptor.methods: + setattr(cls, method.name, self._GenerateStubMethod(method)) + + def _GenerateStubMethod(self, method): + return (lambda inst, rpc_controller, request, callback=None: + self._StubMethod(inst, method, rpc_controller, request, callback)) + + def _StubMethod(self, stub, method_descriptor, + rpc_controller, request, callback): + """The body of all service methods in the generated stub class. + + Args: + stub: Stub instance. + method_descriptor: Descriptor of the invoked method. + rpc_controller: Rpc controller to execute the method. + request: Request protocol message. + callback: A callback to execute when the method finishes. + Returns: + Response message (in case of blocking call). + """ + return stub.rpc_channel.CallMethod( + method_descriptor, rpc_controller, request, + method_descriptor.output_type._concrete_class, callback) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py new file mode 100644 index 0000000000..30cca2e06e --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/source_context.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\"\"\n\rSourceContext\x12\x11\n\tfile_name\x18\x01 \x01(\tB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SOURCECONTEXT._serialized_start=57 + _SOURCECONTEXT._serialized_end=91 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py new file mode 100644 index 0000000000..149728ca08 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/struct.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x84\x01\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntry\x1a\x45\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xea\x01\n\x05Value\x12\x30\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x12\x16\n\x0cnumber_value\x18\x02 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12/\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00\x42\x06\n\x04kind\"3\n\tListValue\x12&\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.Value*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _STRUCT_FIELDSENTRY._options = None + _STRUCT_FIELDSENTRY._serialized_options = b'8\001' + _NULLVALUE._serialized_start=474 + _NULLVALUE._serialized_end=501 + _STRUCT._serialized_start=50 + _STRUCT._serialized_end=182 + _STRUCT_FIELDSENTRY._serialized_start=113 + _STRUCT_FIELDSENTRY._serialized_end=182 + _VALUE._serialized_start=185 + _VALUE._serialized_end=419 + _LISTVALUE._serialized_start=421 + _LISTVALUE._serialized_end=472 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py b/openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py new file mode 100644 index 0000000000..fdcf8cf06c --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py @@ -0,0 +1,194 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""A database of Python protocol buffer generated symbols. + +SymbolDatabase is the MessageFactory for messages generated at compile time, +and makes it easy to create new instances of a registered type, given only the +type's protocol buffer symbol name. + +Example usage:: + + db = symbol_database.SymbolDatabase() + + # Register symbols of interest, from one or multiple files. + db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) + db.RegisterMessage(my_proto_pb2.MyMessage) + db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) + + # The database can be used as a MessageFactory, to generate types based on + # their name: + types = db.GetMessages(['my_proto.proto']) + my_message_instance = types['MyMessage']() + + # The database's underlying descriptor pool can be queried, so it's not + # necessary to know a type's filename to be able to generate it: + filename = db.pool.FindFileContainingSymbol('MyMessage') + my_message_instance = db.GetMessages([filename])['MyMessage']() + + # This functionality is also provided directly via a convenience method: + my_message_instance = db.GetSymbol('MyMessage')() +""" + + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message_factory + + +class SymbolDatabase(message_factory.MessageFactory): + """A database of Python generated symbols.""" + + def RegisterMessage(self, message): + """Registers the given message type in the local database. + + Calls to GetSymbol() and GetMessages() will return messages registered here. + + Args: + message: A :class:`google.protobuf.message.Message` subclass (or + instance); its descriptor will be registered. + + Returns: + The provided message. + """ + + desc = message.DESCRIPTOR + self._classes[desc] = message + self.RegisterMessageDescriptor(desc) + return message + + def RegisterMessageDescriptor(self, message_descriptor): + """Registers the given message descriptor in the local database. + + Args: + message_descriptor (Descriptor): the message descriptor to add. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddDescriptor(message_descriptor) + + def RegisterEnumDescriptor(self, enum_descriptor): + """Registers the given enum descriptor in the local database. + + Args: + enum_descriptor (EnumDescriptor): The enum descriptor to register. + + Returns: + EnumDescriptor: The provided descriptor. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddEnumDescriptor(enum_descriptor) + return enum_descriptor + + def RegisterServiceDescriptor(self, service_descriptor): + """Registers the given service descriptor in the local database. + + Args: + service_descriptor (ServiceDescriptor): the service descriptor to + register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddServiceDescriptor(service_descriptor) + + def RegisterFileDescriptor(self, file_descriptor): + """Registers the given file descriptor in the local database. + + Args: + file_descriptor (FileDescriptor): The file descriptor to register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._InternalAddFileDescriptor(file_descriptor) + + def GetSymbol(self, symbol): + """Tries to find a symbol in the local database. + + Currently, this method only returns message.Message instances, however, if + may be extended in future to support other symbol types. + + Args: + symbol (str): a protocol buffer symbol. + + Returns: + A Python class corresponding to the symbol. + + Raises: + KeyError: if the symbol could not be found. + """ + + return self._classes[self.pool.FindMessageTypeByName(symbol)] + + def GetMessages(self, files): + # TODO(amauryfa): Fix the differences with MessageFactory. + """Gets all registered messages from a specified file. + + Only messages already created and registered will be returned; (this is the + case for imported _pb2 modules) + But unlike MessageFactory, this version also returns already defined nested + messages, but does not register any message extensions. + + Args: + files (list[str]): The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. + + Raises: + KeyError: if a file could not be found. + """ + + def _GetAllMessages(desc): + """Walk a message Descriptor and recursively yields all message names.""" + yield desc + for msg_desc in desc.nested_types: + for nested_desc in _GetAllMessages(msg_desc): + yield nested_desc + + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for msg_desc in file_desc.message_types_by_name.values(): + for desc in _GetAllMessages(msg_desc): + try: + result[desc.full_name] = self._classes[desc] + except KeyError: + # This descriptor has no registered class, skip it. + pass + return result + + +_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) + + +def Default(): + """Returns the default SymbolDatabase.""" + return _DEFAULT diff --git a/openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py b/openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py new file mode 100644 index 0000000000..759cf11f62 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py @@ -0,0 +1,110 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Encoding related utilities.""" +import re + +_cescape_chr_to_symbol_map = {} +_cescape_chr_to_symbol_map[9] = r'\t' # optional escape +_cescape_chr_to_symbol_map[10] = r'\n' # optional escape +_cescape_chr_to_symbol_map[13] = r'\r' # optional escape +_cescape_chr_to_symbol_map[34] = r'\"' # necessary escape +_cescape_chr_to_symbol_map[39] = r"\'" # optional escape +_cescape_chr_to_symbol_map[92] = r'\\' # necessary escape + +# Lookup table for unicode +_cescape_unicode_to_str = [chr(i) for i in range(0, 256)] +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_unicode_to_str[byte] = string + +# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) +_cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + + [chr(i) for i in range(32, 127)] + + [r'\%03o' % i for i in range(127, 256)]) +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_byte_to_str[byte] = string +del byte, string + + +def CEscape(text, as_utf8): + # type: (...) -> str + """Escape a bytes string for use in an text protocol buffer. + + Args: + text: A byte string to be escaped. + as_utf8: Specifies if result may contain non-ASCII characters. + In Python 3 this allows unescaped non-ASCII Unicode characters. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + Returns: + Escaped string (str). + """ + # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not + # satisfy our needs; they encodes unprintable characters using two-digit hex + # escapes whereas our C++ unescaping function allows hex escapes to be any + # length. So, "\0011".encode('string_escape') ends up being "\\x011", which + # will be decoded in C++ as a single-character string with char code 0x11. + text_is_unicode = isinstance(text, str) + if as_utf8 and text_is_unicode: + # We're already unicode, no processing beyond control char escapes. + return text.translate(_cescape_chr_to_symbol_map) + ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. + if as_utf8: + return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) + return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) + + +_CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') + + +def CUnescape(text): + # type: (str) -> bytes + """Unescape a text string with C-style escape sequences to UTF-8 bytes. + + Args: + text: The data to parse in a str. + Returns: + A byte string. + """ + + def ReplaceHex(m): + # Only replace the match if the number of leading back slashes is odd. i.e. + # the slash itself is not escaped. + if len(m.group(1)) & 1: + return m.group(1) + 'x0' + m.group(2) + return m.group(0) + + # This is required because the 'string_escape' encoding doesn't + # allow single-digit hex escapes (like '\xf'). + result = _CUNESCAPE_HEX.sub(ReplaceHex, text) + + return (result.encode('utf-8') # Make it bytes to allow decode. + .decode('unicode_escape') + # Make it bytes again to return the proper type. + .encode('raw_unicode_escape')) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/text_format.py b/openpype/hosts/nuke/vendor/google/protobuf/text_format.py new file mode 100644 index 0000000000..412385c26f --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/text_format.py @@ -0,0 +1,1795 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in text format. + +Simple usage example:: + + # Create a proto object and serialize it to a text proto string. + message = my_proto_pb2.MyMessage(foo='bar') + text_proto = text_format.MessageToString(message) + + # Parse a text proto string. + message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +# TODO(b/129989314) Import thread contention leads to test failures. +import encodings.raw_unicode_escape # pylint: disable=unused-import +import encodings.unicode_escape # pylint: disable=unused-import +import io +import math +import re + +from google.protobuf.internal import decoder +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import text_encoding + +# pylint: disable=g-import-not-at-top +__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', + 'PrintFieldValue', 'Merge', 'MessageToBytes'] + +_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), + type_checkers.Int32ValueChecker(), + type_checkers.Uint64ValueChecker(), + type_checkers.Int64ValueChecker()) +_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) +_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) +_QUOTES = frozenset(("'", '"')) +_ANY_FULL_TYPE_NAME = 'google.protobuf.Any' + + +class Error(Exception): + """Top-level module error for text_format.""" + + +class ParseError(Error): + """Thrown in case of text parsing or tokenizing error.""" + + def __init__(self, message=None, line=None, column=None): + if message is not None and line is not None: + loc = str(line) + if column is not None: + loc += ':{0}'.format(column) + message = '{0} : {1}'.format(loc, message) + if message is not None: + super(ParseError, self).__init__(message) + else: + super(ParseError, self).__init__() + self._line = line + self._column = column + + def GetLine(self): + return self._line + + def GetColumn(self): + return self._column + + +class TextWriter(object): + + def __init__(self, as_utf8): + self._writer = io.StringIO() + + def write(self, val): + return self._writer.write(val) + + def close(self): + return self._writer.close() + + def getvalue(self): + return self._writer.getvalue() + + +def MessageToString( + message, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + indent=0, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + # type: (...) -> str + """Convert protobuf message to text format. + + Double values can be formatted compactly with 15 digits of + precision (which is the most that IEEE 754 "double" can guarantee) + using double_format='.15g'. To ensure that converting to text and back to a + proto will result in an identical value, double_format='.17g' should be used. + + Args: + message: The protocol buffers message. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, fields of a proto message will be printed using + the order defined in source code instead of the field number, extensions + will be printed at the end of the message and their relative order is + determined by the extension number. By default, use the field number + order. + float_format (str): If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest float + that has same value in wire will be printed. Also affect double field + if double_format is not set but float_format is set. + double_format (str): If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, use ``str()`` + use_field_number: If True, print field numbers instead of names. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + indent (int): The initial indent level, in terms of spaces, for pretty + print. + message_formatter (function(message, indent, as_one_line) -> unicode|None): + Custom formatter for selected sub-messages (usually based on message + type). Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if the + field is a proto message. + + Returns: + str: A string of the text formatted protocol buffer message. + """ + out = TextWriter(as_utf8) + printer = _Printer( + out, + indent, + as_utf8, + as_one_line, + use_short_repeated_primitives, + pointy_brackets, + use_index_order, + float_format, + double_format, + use_field_number, + descriptor_pool, + message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + result = out.getvalue() + out.close() + if as_one_line: + return result.rstrip() + return result + + +def MessageToBytes(message, **kwargs): + # type: (...) -> bytes + """Convert protobuf message to encoded text format. See MessageToString.""" + text = MessageToString(message, **kwargs) + if isinstance(text, bytes): + return text + codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' + return text.encode(codec) + + +def _IsMapEntry(field): + return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def PrintMessage(message, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + printer = _Printer( + out=out, indent=indent, as_utf8=as_utf8, + as_one_line=as_one_line, + use_short_repeated_primitives=use_short_repeated_primitives, + pointy_brackets=pointy_brackets, + use_index_order=use_index_order, + float_format=float_format, + double_format=double_format, + use_field_number=use_field_number, + descriptor_pool=descriptor_pool, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + + +def PrintField(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field name/value pair.""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintField(field, value) + + +def PrintFieldValue(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field value (not including name).""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintFieldValue(field, value) + + +def _BuildMessageFromTypeName(type_name, descriptor_pool): + """Returns a protobuf message instance. + + Args: + type_name: Fully-qualified protobuf message type name string. + descriptor_pool: DescriptorPool instance. + + Returns: + A Message instance of type matching type_name, or None if the a Descriptor + wasn't found matching type_name. + """ + # pylint: disable=g-import-not-at-top + if descriptor_pool is None: + from google.protobuf import descriptor_pool as pool_mod + descriptor_pool = pool_mod.Default() + from google.protobuf import symbol_database + database = symbol_database.Default() + try: + message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) + except KeyError: + return None + message_type = database.GetPrototype(message_descriptor) + return message_type() + + +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 + + +class _Printer(object): + """Text format printer for protocol message.""" + + def __init__( + self, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Initialize the Printer. + + Double values can be formatted compactly with 15 digits of precision + (which is the most that IEEE 754 "double" can guarantee) using + double_format='.15g'. To ensure that converting to text and back to a proto + will result in an identical value, double_format='.17g' should be used. + + Args: + out: To record the text format result. + indent: The initial indent level for pretty print. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, print fields of a proto message using the order + defined in source code instead of the field number. By default, use the + field number order. + float_format: If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest + float that has same value in wire will be printed. Also affect double + field if double_format is not set but float_format is set. + double_format: If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, str() is used. + use_field_number: If True, print field numbers instead of names. + descriptor_pool: A DescriptorPool used to resolve Any types. + message_formatter: A function(message, indent, as_one_line): unicode|None + to custom format selected sub-messages (usually based on message type). + Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if + the field is a proto message. + """ + self.out = out + self.indent = indent + self.as_utf8 = as_utf8 + self.as_one_line = as_one_line + self.use_short_repeated_primitives = use_short_repeated_primitives + self.pointy_brackets = pointy_brackets + self.use_index_order = use_index_order + self.float_format = float_format + if double_format is not None: + self.double_format = double_format + else: + self.double_format = float_format + self.use_field_number = use_field_number + self.descriptor_pool = descriptor_pool + self.message_formatter = message_formatter + self.print_unknown_fields = print_unknown_fields + self.force_colon = force_colon + + def _TryPrintAsAnyMessage(self, message): + """Serializes if message is a google.protobuf.Any field.""" + if '/' not in message.type_url: + return False + packed_message = _BuildMessageFromTypeName(message.TypeName(), + self.descriptor_pool) + if packed_message: + packed_message.MergeFromString(message.value) + colon = ':' if self.force_colon else '' + self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) + self._PrintMessageFieldValue(packed_message) + self.out.write(' ' if self.as_one_line else '\n') + return True + else: + return False + + def _TryCustomFormatMessage(self, message): + formatted = self.message_formatter(message, self.indent, self.as_one_line) + if formatted is None: + return False + + out = self.out + out.write(' ' * self.indent) + out.write(formatted) + out.write(' ' if self.as_one_line else '\n') + return True + + def PrintMessage(self, message): + """Convert protobuf message to text format. + + Args: + message: The protocol buffers message. + """ + if self.message_formatter and self._TryCustomFormatMessage(message): + return + if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and + self._TryPrintAsAnyMessage(message)): + return + fields = message.ListFields() + if self.use_index_order: + fields.sort( + key=lambda x: x[0].number if x[0].is_extension else x[0].index) + for field, value in fields: + if _IsMapEntry(field): + for key in sorted(value): + # This is slow for maps with submessage entries because it copies the + # entire tree. Unfortunately this would take significant refactoring + # of this file to work around. + # + # TODO(haberman): refactor and optimize if this becomes an issue. + entry_submsg = value.GetEntryClass()(key=key, value=value[key]) + self.PrintField(field, entry_submsg) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if (self.use_short_repeated_primitives + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): + self._PrintShortRepeatedPrimitivesValue(field, value) + else: + for element in value: + self.PrintField(field, element) + else: + self.PrintField(field, value) + + if self.print_unknown_fields: + self._PrintUnknownFields(message.UnknownFields()) + + def _PrintUnknownFields(self, unknown_fields): + """Print unknown fields.""" + out = self.out + for field in unknown_fields: + out.write(' ' * self.indent) + out.write(str(field.field_number)) + if field.wire_type == WIRETYPE_START_GROUP: + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(field.data) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: + try: + # If this field is parseable as a Message, it is probably + # an embedded message. + # pylint: disable=protected-access + (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( + memoryview(field.data), 0, len(field.data)) + except Exception: # pylint: disable=broad-except + pos = 0 + + if pos == len(field.data): + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(embedded_unknown_message) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + else: + # A string or bytes field. self.as_utf8 may not work. + out.write(': \"') + out.write(text_encoding.CEscape(field.data, False)) + out.write('\" ' if self.as_one_line else '\"\n') + else: + # varint, fixed32, fixed64 + out.write(': ') + out.write(str(field.data)) + out.write(' ' if self.as_one_line else '\n') + + def _PrintFieldName(self, field): + """Print field name.""" + out = self.out + out.write(' ' * self.indent) + if self.use_field_number: + out.write(str(field.number)) + else: + if field.is_extension: + out.write('[') + if (field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): + out.write(field.message_type.full_name) + else: + out.write(field.full_name) + out.write(']') + elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: + # For groups, use the capitalized name. + out.write(field.message_type.name) + else: + out.write(field.name) + + if (self.force_colon or + field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): + # The colon is optional in this case, but our cross-language golden files + # don't include it. Here, the colon is only included if force_colon is + # set to True + out.write(':') + + def PrintField(self, field, value): + """Print a single field name/value pair.""" + self._PrintFieldName(field) + self.out.write(' ') + self.PrintFieldValue(field, value) + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintShortRepeatedPrimitivesValue(self, field, value): + """"Prints short repeated primitives value.""" + # Note: this is called only when value has at least one element. + self._PrintFieldName(field) + self.out.write(' [') + for i in range(len(value) - 1): + self.PrintFieldValue(field, value[i]) + self.out.write(', ') + self.PrintFieldValue(field, value[-1]) + self.out.write(']') + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintMessageFieldValue(self, value): + if self.pointy_brackets: + openb = '<' + closeb = '>' + else: + openb = '{' + closeb = '}' + + if self.as_one_line: + self.out.write('%s ' % openb) + self.PrintMessage(value) + self.out.write(closeb) + else: + self.out.write('%s\n' % openb) + self.indent += 2 + self.PrintMessage(value) + self.indent -= 2 + self.out.write(' ' * self.indent + closeb) + + def PrintFieldValue(self, field, value): + """Print a single field value (not including name). + + For repeated fields, the value should be a single element. + + Args: + field: The descriptor of the field to be printed. + value: The value of the field. + """ + out = self.out + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self._PrintMessageFieldValue(value) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + enum_value = field.enum_type.values_by_number.get(value, None) + if enum_value is not None: + out.write(enum_value.name) + else: + out.write(str(value)) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + out.write('\"') + if isinstance(value, str) and not self.as_utf8: + out_value = value.encode('utf-8') + else: + out_value = value + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + # We always need to escape all binary data in TYPE_BYTES fields. + out_as_utf8 = False + else: + out_as_utf8 = self.as_utf8 + out.write(text_encoding.CEscape(out_value, out_as_utf8)) + out.write('\"') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + if value: + out.write('true') + else: + out.write('false') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + if self.float_format is not None: + out.write('{1:{0}}'.format(self.float_format, value)) + else: + if math.isnan(value): + out.write(str(value)) + else: + out.write(str(type_checkers.ToShortestFloat(value))) + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and + self.double_format is not None): + out.write('{1:{0}}'.format(self.double_format, value)) + else: + out.write(str(value)) + + +def Parse(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + NOTE: for historical reasons this function does not clear the input + message. This is different from what the binary msg.ParseFrom(...) does. + If text contains a field already set in message, the value is appended if the + field is repeated. Otherwise, an error is raised. + + Example:: + + a = MyProto() + a.repeated_field.append('test') + b = MyProto() + + # Repeated fields are combined + text_format.Parse(repr(a), b) + text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] + + # Non-repeated fields cannot be overwritten + a.singular_field = 1 + b.singular_field = 2 + text_format.Parse(repr(a), b) # ParseError + + # Binary version: + b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" + + Caller is responsible for clearing the message as needed. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def Merge(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + Like Parse(), but allows repeated values for a non-repeated field, and uses + the last one. This means any non-repeated, top-level fields specified in text + replace those in the message. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return MergeLines( + text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def ParseLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Parse() for caveats. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.ParseLines(lines, message) + + +def MergeLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Merge() for more details. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.MergeLines(lines, message) + + +class _Parser(object): + """Text format parser for protocol message.""" + + def __init__(self, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + self.allow_unknown_extension = allow_unknown_extension + self.allow_field_number = allow_field_number + self.descriptor_pool = descriptor_pool + self.allow_unknown_field = allow_unknown_field + + def ParseLines(self, lines, message): + """Parses a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = False + self._ParseOrMerge(lines, message) + return message + + def MergeLines(self, lines, message): + """Merges a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = True + self._ParseOrMerge(lines, message) + return message + + def _ParseOrMerge(self, lines, message): + """Converts a text representation of a protocol message into a message. + + Args: + lines: Lines of a message's text representation. + message: A protocol buffer message to merge into. + + Raises: + ParseError: On text parsing problems. + """ + # Tokenize expects native str lines. + str_lines = ( + line if isinstance(line, str) else line.decode('utf-8') + for line in lines) + tokenizer = Tokenizer(str_lines) + while not tokenizer.AtEnd(): + self._MergeField(tokenizer, message) + + def _MergeField(self, tokenizer, message): + """Merges a single protocol message field into a message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + message: A protocol message to record the data. + + Raises: + ParseError: In case of text parsing problems. + """ + message_descriptor = message.DESCRIPTOR + if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and + tokenizer.TryConsume('[')): + type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) + tokenizer.Consume(']') + tokenizer.TryConsume(':') + if tokenizer.TryConsume('<'): + expanded_any_end_token = '>' + else: + tokenizer.Consume('{') + expanded_any_end_token = '}' + expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, + self.descriptor_pool) + if not expanded_any_sub_message: + raise ParseError('Type %s not found in descriptor pool' % + packed_type_name) + while not tokenizer.TryConsume(expanded_any_end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % + (expanded_any_end_token,)) + self._MergeField(tokenizer, expanded_any_sub_message) + deterministic = False + + message.Pack(expanded_any_sub_message, + type_url_prefix=type_url_prefix, + deterministic=deterministic) + return + + if tokenizer.TryConsume('['): + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + name = '.'.join(name) + + if not message_descriptor.is_extendable: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" does not have extensions.' % + message_descriptor.full_name) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(name) + # pylint: enable=protected-access + + + if not field: + if self.allow_unknown_extension: + field = None + else: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" not registered. ' + 'Did you import the _pb2 module which defines it? ' + 'If you are trying to place the extension in the MessageSet ' + 'field of another message that is in an Any or MessageSet field, ' + 'that message\'s _pb2 module must be imported as well' % name) + elif message_descriptor != field.containing_type: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" does not extend message type "%s".' % + (name, message_descriptor.full_name)) + + tokenizer.Consume(']') + + else: + name = tokenizer.ConsumeIdentifierOrNumber() + if self.allow_field_number and name.isdigit(): + number = ParseInteger(name, True, True) + field = message_descriptor.fields_by_number.get(number, None) + if not field and message_descriptor.is_extendable: + field = message.Extensions._FindExtensionByNumber(number) + else: + field = message_descriptor.fields_by_name.get(name, None) + + # Group names are expected to be capitalized as they appear in the + # .proto file, which actually matches their type names, not their field + # names. + if not field: + field = message_descriptor.fields_by_name.get(name.lower(), None) + if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: + field = None + + if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and + field.message_type.name != name): + field = None + + if not field and not self.allow_unknown_field: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" has no field named "%s".' % + (message_descriptor.full_name, name)) + + if field: + if not self._allow_multiple_scalars and field.containing_oneof: + # Check if there's a different field set in this oneof. + # Note that we ignore the case if the same field was set before, and we + # apply _allow_multiple_scalars to non-scalar fields as well. + which_oneof = message.WhichOneof(field.containing_oneof.name) + if which_oneof is not None and which_oneof != field.name: + raise tokenizer.ParseErrorPreviousToken( + 'Field "%s" is specified along with field "%s", another member ' + 'of oneof "%s" for message type "%s".' % + (field.name, which_oneof, field.containing_oneof.name, + message_descriptor.full_name)) + + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + tokenizer.TryConsume(':') + merger = self._MergeMessageField + else: + tokenizer.Consume(':') + merger = self._MergeScalarField + + if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and + tokenizer.TryConsume('[')): + # Short repeated format, e.g. "foo: [1, 2, 3]" + if not tokenizer.TryConsume(']'): + while True: + merger(tokenizer, message, field) + if tokenizer.TryConsume(']'): + break + tokenizer.Consume(',') + + else: + merger(tokenizer, message, field) + + else: # Proto field is unknown. + assert (self.allow_unknown_extension or self.allow_unknown_field) + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + + def _ConsumeAnyTypeUrl(self, tokenizer): + """Consumes a google.protobuf.Any type URL and returns the type name.""" + # Consume "type.googleapis.com/". + prefix = [tokenizer.ConsumeIdentifier()] + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('/') + # Consume the fully-qualified type name. + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + return '.'.join(prefix), '.'.join(name) + + def _MergeMessageField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: The message of which field is a member. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + """ + is_map_entry = _IsMapEntry(field) + + if tokenizer.TryConsume('<'): + end_token = '>' + else: + tokenizer.Consume('{') + end_token = '}' + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + sub_message = message.Extensions[field].add() + elif is_map_entry: + sub_message = getattr(message, field.name).GetEntryClass()() + else: + sub_message = getattr(message, field.name).add() + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + sub_message = message.Extensions[field] + else: + # Also apply _allow_multiple_scalars to message field. + # TODO(jieluo): Change to _allow_singular_overwrites. + if (not self._allow_multiple_scalars and + message.HasField(field.name)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + sub_message = getattr(message, field.name) + sub_message.SetInParent() + + while not tokenizer.TryConsume(end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) + self._MergeField(tokenizer, sub_message) + + if is_map_entry: + value_cpptype = field.message_type.fields_by_name['value'].cpp_type + if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + value = getattr(message, field.name)[sub_message.key] + value.CopyFrom(sub_message.value) + else: + getattr(message, field.name)[sub_message.key] = sub_message.value + + @staticmethod + def _IsProto3Syntax(message): + message_descriptor = message.DESCRIPTOR + return (hasattr(message_descriptor, 'syntax') and + message_descriptor.syntax == 'proto3') + + def _MergeScalarField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: A protocol message to record the data. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + RuntimeError: On runtime errors. + """ + _ = self.allow_unknown_extension + value = None + + if field.type in (descriptor.FieldDescriptor.TYPE_INT32, + descriptor.FieldDescriptor.TYPE_SINT32, + descriptor.FieldDescriptor.TYPE_SFIXED32): + value = _ConsumeInt32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, + descriptor.FieldDescriptor.TYPE_SINT64, + descriptor.FieldDescriptor.TYPE_SFIXED64): + value = _ConsumeInt64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, + descriptor.FieldDescriptor.TYPE_FIXED32): + value = _ConsumeUint32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, + descriptor.FieldDescriptor.TYPE_FIXED64): + value = _ConsumeUint64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, + descriptor.FieldDescriptor.TYPE_DOUBLE): + value = tokenizer.ConsumeFloat() + elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: + value = tokenizer.ConsumeBool() + elif field.type == descriptor.FieldDescriptor.TYPE_STRING: + value = tokenizer.ConsumeString() + elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: + value = tokenizer.ConsumeByteString() + elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: + value = tokenizer.ConsumeEnum(field) + else: + raise RuntimeError('Unknown field type %d' % field.type) + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + message.Extensions[field].append(value) + else: + getattr(message, field.name).append(value) + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + not self._IsProto3Syntax(message) and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + else: + message.Extensions[field] = value + else: + duplicate_error = False + if not self._allow_multiple_scalars: + if self._IsProto3Syntax(message): + # Proto3 doesn't represent presence so we try best effort to check + # multiple scalars by compare to default values. + duplicate_error = bool(getattr(message, field.name)) + else: + duplicate_error = message.HasField(field.name) + + if duplicate_error: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + else: + setattr(message, field.name, value) + + +def _SkipFieldContents(tokenizer): + """Skips over contents (value or message) of a field. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + # Try to guess the type of this field. + # If this field is not a message, there should be a ":" between the + # field name and the field value and also the field value should not + # start with "{" or "<" which indicates the beginning of a message body. + # If there is no ":" or there is a "{" or "<" after ":", this field has + # to be a message or the input is ill-formed. + if tokenizer.TryConsume(':') and not tokenizer.LookingAt( + '{') and not tokenizer.LookingAt('<'): + _SkipFieldValue(tokenizer) + else: + _SkipFieldMessage(tokenizer) + + +def _SkipField(tokenizer): + """Skips over a complete field (name and value/message). + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + if tokenizer.TryConsume('['): + # Consume extension name. + tokenizer.ConsumeIdentifier() + while tokenizer.TryConsume('.'): + tokenizer.ConsumeIdentifier() + tokenizer.Consume(']') + else: + tokenizer.ConsumeIdentifierOrNumber() + + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + +def _SkipFieldMessage(tokenizer): + """Skips over a field message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + + if tokenizer.TryConsume('<'): + delimiter = '>' + else: + tokenizer.Consume('{') + delimiter = '}' + + while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): + _SkipField(tokenizer) + + tokenizer.Consume(delimiter) + + +def _SkipFieldValue(tokenizer): + """Skips over a field value. + + Args: + tokenizer: A tokenizer to parse the field name and values. + + Raises: + ParseError: In case an invalid field value is found. + """ + # String/bytes tokens can come in multiple adjacent string literals. + # If we can consume one, consume as many as we can. + if tokenizer.TryConsumeByteString(): + while tokenizer.TryConsumeByteString(): + pass + return + + if (not tokenizer.TryConsumeIdentifier() and + not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and + not tokenizer.TryConsumeFloat()): + raise ParseError('Invalid field value: ' + tokenizer.token) + + +class Tokenizer(object): + """Protocol buffer text representation tokenizer. + + This class handles the lower level string parsing by splitting it into + meaningful tokens. + + It was directly ported from the Java protocol buffer API. + """ + + _WHITESPACE = re.compile(r'\s+') + _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) + _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) + _TOKEN = re.compile('|'.join([ + r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier + r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number + ] + [ # quoted str for each quote mark + # Avoid backtracking! https://stackoverflow.com/a/844267 + r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) + for mark in _QUOTES + ])) + + _IDENTIFIER = re.compile(r'[^\d\W]\w*') + _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') + + def __init__(self, lines, skip_comments=True): + self._position = 0 + self._line = -1 + self._column = 0 + self._token_start = None + self.token = '' + self._lines = iter(lines) + self._current_line = '' + self._previous_line = 0 + self._previous_column = 0 + self._more_lines = True + self._skip_comments = skip_comments + self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT + or self._WHITESPACE) + self._SkipWhitespace() + self.NextToken() + + def LookingAt(self, token): + return self.token == token + + def AtEnd(self): + """Checks the end of the text was reached. + + Returns: + True iff the end was reached. + """ + return not self.token + + def _PopLine(self): + while len(self._current_line) <= self._column: + try: + self._current_line = next(self._lines) + except StopIteration: + self._current_line = '' + self._more_lines = False + return + else: + self._line += 1 + self._column = 0 + + def _SkipWhitespace(self): + while True: + self._PopLine() + match = self._whitespace_pattern.match(self._current_line, self._column) + if not match: + break + length = len(match.group(0)) + self._column += length + + def TryConsume(self, token): + """Tries to consume a given piece of text. + + Args: + token: Text to consume. + + Returns: + True iff the text was consumed. + """ + if self.token == token: + self.NextToken() + return True + return False + + def Consume(self, token): + """Consumes a piece of text. + + Args: + token: Text to consume. + + Raises: + ParseError: If the text couldn't be consumed. + """ + if not self.TryConsume(token): + raise self.ParseError('Expected "%s".' % token) + + def ConsumeComment(self): + result = self.token + if not self._COMMENT.match(result): + raise self.ParseError('Expected comment.') + self.NextToken() + return result + + def ConsumeCommentOrTrailingComment(self): + """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" + + # Tokenizer initializes _previous_line and _previous_column to 0. As the + # tokenizer starts, it looks like there is a previous token on the line. + just_started = self._line == 0 and self._column == 0 + + before_parsing = self._previous_line + comment = self.ConsumeComment() + + # A trailing comment is a comment on the same line than the previous token. + trailing = (self._previous_line == before_parsing + and not just_started) + + return trailing, comment + + def TryConsumeIdentifier(self): + try: + self.ConsumeIdentifier() + return True + except ParseError: + return False + + def ConsumeIdentifier(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER.match(result): + raise self.ParseError('Expected identifier.') + self.NextToken() + return result + + def TryConsumeIdentifierOrNumber(self): + try: + self.ConsumeIdentifierOrNumber() + return True + except ParseError: + return False + + def ConsumeIdentifierOrNumber(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER_OR_NUMBER.match(result): + raise self.ParseError('Expected identifier or number, got %s.' % result) + self.NextToken() + return result + + def TryConsumeInteger(self): + try: + self.ConsumeInteger() + return True + except ParseError: + return False + + def ConsumeInteger(self): + """Consumes an integer number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + try: + result = _ParseAbstractInteger(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeFloat(self): + try: + self.ConsumeFloat() + return True + except ParseError: + return False + + def ConsumeFloat(self): + """Consumes an floating point number. + + Returns: + The number parsed. + + Raises: + ParseError: If a floating point number couldn't be consumed. + """ + try: + result = ParseFloat(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeBool(self): + """Consumes a boolean value. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + try: + result = ParseBool(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeByteString(self): + try: + self.ConsumeByteString() + return True + except ParseError: + return False + + def ConsumeString(self): + """Consumes a string value. + + Returns: + The string parsed. + + Raises: + ParseError: If a string value couldn't be consumed. + """ + the_bytes = self.ConsumeByteString() + try: + return str(the_bytes, 'utf-8') + except UnicodeDecodeError as e: + raise self._StringParseError(e) + + def ConsumeByteString(self): + """Consumes a byte array value. + + Returns: + The array parsed (as a string). + + Raises: + ParseError: If a byte array value couldn't be consumed. + """ + the_list = [self._ConsumeSingleByteString()] + while self.token and self.token[0] in _QUOTES: + the_list.append(self._ConsumeSingleByteString()) + return b''.join(the_list) + + def _ConsumeSingleByteString(self): + """Consume one token of a string literal. + + String literals (whether bytes or text) can come in multiple adjacent + tokens which are automatically concatenated, like in C or Python. This + method only consumes one token. + + Returns: + The token parsed. + Raises: + ParseError: When the wrong format data is found. + """ + text = self.token + if len(text) < 1 or text[0] not in _QUOTES: + raise self.ParseError('Expected string but found: %r' % (text,)) + + if len(text) < 2 or text[-1] != text[0]: + raise self.ParseError('String missing ending quote: %r' % (text,)) + + try: + result = text_encoding.CUnescape(text[1:-1]) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeEnum(self, field): + try: + result = ParseEnum(field, self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ParseErrorPreviousToken(self, message): + """Creates and *returns* a ParseError for the previously read token. + + Args: + message: A message to set for the exception. + + Returns: + A ParseError instance. + """ + return ParseError(message, self._previous_line + 1, + self._previous_column + 1) + + def ParseError(self, message): + """Creates and *returns* a ParseError for the current token.""" + return ParseError('\'' + self._current_line + '\': ' + message, + self._line + 1, self._column + 1) + + def _StringParseError(self, e): + return self.ParseError('Couldn\'t parse string: ' + str(e)) + + def NextToken(self): + """Reads the next meaningful token.""" + self._previous_line = self._line + self._previous_column = self._column + + self._column += len(self.token) + self._SkipWhitespace() + + if not self._more_lines: + self.token = '' + return + + match = self._TOKEN.match(self._current_line, self._column) + if not match and not self._skip_comments: + match = self._COMMENT.match(self._current_line, self._column) + if match: + token = match.group(0) + self.token = token + else: + self.token = self._current_line[self._column] + +# Aliased so it can still be accessed by current visibility violators. +# TODO(dbarnett): Migrate violators to textformat_tokenizer. +_Tokenizer = Tokenizer # pylint: disable=invalid-name + + +def _ConsumeInt32(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) + + +def _ConsumeUint32(tokenizer): + """Consumes an unsigned 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) + + +def _TryConsumeInt64(tokenizer): + try: + _ConsumeInt64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeInt64(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) + + +def _TryConsumeUint64(tokenizer): + try: + _ConsumeUint64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeUint64(tokenizer): + """Consumes an unsigned 64bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 64bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) + + +def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): + """Consumes an integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer with given characteristics couldn't be consumed. + """ + try: + result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) + except ValueError as e: + raise tokenizer.ParseError(str(e)) + tokenizer.NextToken() + return result + + +def ParseInteger(text, is_signed=False, is_long=False): + """Parses an integer. + + Args: + text: The text to parse. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + result = _ParseAbstractInteger(text) + + # Check if the integer is sane. Exceptions handled by callers. + checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] + checker.CheckValue(result) + return result + + +def _ParseAbstractInteger(text): + """Parses an integer without checking size/signedness. + + Args: + text: The text to parse. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + orig_text = text + c_octal_match = re.match(r'(-?)0(\d+)$', text) + if c_octal_match: + # Python 3 no longer supports 0755 octal syntax without the 'o', so + # we always use the '0o' prefix for multi-digit numbers starting with 0. + text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) + try: + return int(text, 0) + except ValueError: + raise ValueError('Couldn\'t parse integer: %s' % orig_text) + + +def ParseFloat(text): + """Parse a floating point number. + + Args: + text: Text to parse. + + Returns: + The number parsed. + + Raises: + ValueError: If a floating point number couldn't be parsed. + """ + try: + # Assume Python compatible syntax. + return float(text) + except ValueError: + # Check alternative spellings. + if _FLOAT_INFINITY.match(text): + if text[0] == '-': + return float('-inf') + else: + return float('inf') + elif _FLOAT_NAN.match(text): + return float('nan') + else: + # assume '1.0f' format + try: + return float(text.rstrip('f')) + except ValueError: + raise ValueError('Couldn\'t parse float: %s' % text) + + +def ParseBool(text): + """Parse a boolean value. + + Args: + text: Text to parse. + + Returns: + Boolean values parsed + + Raises: + ValueError: If text is not a valid boolean. + """ + if text in ('true', 't', '1', 'True'): + return True + elif text in ('false', 'f', '0', 'False'): + return False + else: + raise ValueError('Expected "true" or "false".') + + +def ParseEnum(field, value): + """Parse an enum value. + + The value can be specified by a number (the enum value), or by + a string literal (the enum name). + + Args: + field: Enum field descriptor. + value: String value. + + Returns: + Enum value number. + + Raises: + ValueError: If the enum value could not be parsed. + """ + enum_descriptor = field.enum_type + try: + number = int(value, 0) + except ValueError: + # Identifier. + enum_value = enum_descriptor.values_by_name.get(value, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value named %s.' % + (enum_descriptor.full_name, value)) + else: + # Numeric value. + if hasattr(field.file, 'syntax'): + # Attribute is checked for compatibility. + if field.file.syntax == 'proto3': + # Proto3 accept numeric unknown enums. + return number + enum_value = enum_descriptor.values_by_number.get(number, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value with number %d.' % + (enum_descriptor.full_name, number)) + return enum_value.number diff --git a/openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py new file mode 100644 index 0000000000..558d496941 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/timestamp.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\"+\n\tTimestamp\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _TIMESTAMP._serialized_start=52 + _TIMESTAMP._serialized_end=95 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py new file mode 100644 index 0000000000..19903fb6b4 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/type.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xd7\x01\n\x04Type\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Field\x12\x0e\n\x06oneofs\x18\x03 \x03(\t\x12(\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x05\n\x05\x46ield\x12)\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.Kind\x12\x37\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.Cardinality\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08type_url\x18\x06 \x01(\t\x12\x13\n\x0boneof_index\x18\x07 \x01(\x05\x12\x0e\n\x06packed\x18\x08 \x01(\x08\x12(\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.Option\x12\x11\n\tjson_name\x18\n \x01(\t\x12\x15\n\rdefault_value\x18\x0b \x01(\t\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\xce\x01\n\x04\x45num\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValue\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.Syntax\"S\n\tEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\";\n\x06Option\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any*.\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SYNTAX._serialized_start=1413 + _SYNTAX._serialized_end=1459 + _TYPE._serialized_start=113 + _TYPE._serialized_end=328 + _FIELD._serialized_start=331 + _FIELD._serialized_end=1056 + _FIELD_KIND._serialized_start=610 + _FIELD_KIND._serialized_end=938 + _FIELD_CARDINALITY._serialized_start=940 + _FIELD_CARDINALITY._serialized_end=1056 + _ENUM._serialized_start=1059 + _ENUM._serialized_end=1265 + _ENUMVALUE._serialized_start=1267 + _ENUMVALUE._serialized_end=1350 + _OPTION._serialized_start=1352 + _OPTION._serialized_end=1411 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/util/__init__.py b/openpype/hosts/nuke/vendor/google/protobuf/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py new file mode 100644 index 0000000000..66a5836c82 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&google/protobuf/util/json_format.proto\x12\x11protobuf_unittest\"\x89\x01\n\x13TestFlagsAndStrings\x12\t\n\x01\x41\x18\x01 \x02(\x05\x12K\n\rrepeatedgroup\x18\x02 \x03(\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\x1a\x1a\n\rRepeatedGroup\x12\t\n\x01\x66\x18\x03 \x02(\t\"!\n\x14TestBase64ByteArrays\x12\t\n\x01\x61\x18\x01 \x02(\x0c\"G\n\x12TestJavaScriptJSON\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\r\n\x05\x66inal\x18\x02 \x01(\x02\x12\n\n\x02in\x18\x03 \x01(\t\x12\x0b\n\x03Var\x18\x04 \x01(\t\"Q\n\x18TestJavaScriptOrderJSON1\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\"\x89\x01\n\x18TestJavaScriptOrderJSON2\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\x12\x36\n\x01z\x18\x06 \x03(\x0b\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\"$\n\x0cTestLargeInt\x12\t\n\x01\x61\x18\x01 \x02(\x03\x12\t\n\x01\x62\x18\x02 \x02(\x04\"\xa0\x01\n\x0bTestNumbers\x12\x30\n\x01\x61\x18\x01 \x01(\x0e\x32%.protobuf_unittest.TestNumbers.MyType\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x02\x12\t\n\x01\x64\x18\x04 \x01(\x08\x12\t\n\x01\x65\x18\x05 \x01(\x01\x12\t\n\x01\x66\x18\x06 \x01(\r\"(\n\x06MyType\x12\x06\n\x02OK\x10\x00\x12\x0b\n\x07WARNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\"T\n\rTestCamelCase\x12\x14\n\x0cnormal_field\x18\x01 \x01(\t\x12\x15\n\rCAPITAL_FIELD\x18\x02 \x01(\x05\x12\x16\n\x0e\x43\x61melCaseField\x18\x03 \x01(\x05\"|\n\x0bTestBoolMap\x12=\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"O\n\rTestRecursion\x12\r\n\x05value\x18\x01 \x01(\x05\x12/\n\x05\x63hild\x18\x02 \x01(\x0b\x32 .protobuf_unittest.TestRecursion\"\x86\x01\n\rTestStringMap\x12\x43\n\nstring_map\x18\x01 \x03(\x0b\x32/.protobuf_unittest.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x14TestStringSerializer\x12\x15\n\rscalar_string\x18\x01 \x01(\t\x12\x17\n\x0frepeated_string\x18\x02 \x03(\t\x12J\n\nstring_map\x18\x03 \x03(\x0b\x32\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x18TestMessageWithExtension*\x08\x08\x64\x10\x80\x80\x80\x80\x02\"z\n\rTestExtension\x12\r\n\x05value\x18\x01 \x01(\t2Z\n\x03\x65xt\x12+.protobuf_unittest.TestMessageWithExtension\x18\x64 \x01(\x0b\x32 .protobuf_unittest.TestExtension\"Q\n\x14TestDefaultEnumValue\x12\x39\n\nenum_value\x18\x01 \x01(\x0e\x32\x1c.protobuf_unittest.EnumValue:\x07\x44\x45\x46\x41ULT*2\n\tEnumValue\x12\x0c\n\x08PROTOCOL\x10\x00\x12\n\n\x06\x42UFFER\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext']) + + DESCRIPTOR._options = None + _TESTBOOLMAP_BOOLMAPENTRY._options = None + _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\001' + _ENUMVALUE._serialized_start=1607 + _ENUMVALUE._serialized_end=1657 + _TESTFLAGSANDSTRINGS._serialized_start=62 + _TESTFLAGSANDSTRINGS._serialized_end=199 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199 + _TESTBASE64BYTEARRAYS._serialized_start=201 + _TESTBASE64BYTEARRAYS._serialized_end=234 + _TESTJAVASCRIPTJSON._serialized_start=236 + _TESTJAVASCRIPTJSON._serialized_end=307 + _TESTJAVASCRIPTORDERJSON1._serialized_start=309 + _TESTJAVASCRIPTORDERJSON1._serialized_end=390 + _TESTJAVASCRIPTORDERJSON2._serialized_start=393 + _TESTJAVASCRIPTORDERJSON2._serialized_end=530 + _TESTLARGEINT._serialized_start=532 + _TESTLARGEINT._serialized_end=568 + _TESTNUMBERS._serialized_start=571 + _TESTNUMBERS._serialized_end=731 + _TESTNUMBERS_MYTYPE._serialized_start=691 + _TESTNUMBERS_MYTYPE._serialized_end=731 + _TESTCAMELCASE._serialized_start=733 + _TESTCAMELCASE._serialized_end=817 + _TESTBOOLMAP._serialized_start=819 + _TESTBOOLMAP._serialized_end=943 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943 + _TESTRECURSION._serialized_start=945 + _TESTRECURSION._serialized_end=1024 + _TESTSTRINGMAP._serialized_start=1027 + _TESTSTRINGMAP._serialized_end=1161 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161 + _TESTSTRINGSERIALIZER._serialized_start=1164 + _TESTSTRINGSERIALIZER._serialized_end=1360 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161 + _TESTMESSAGEWITHEXTENSION._serialized_start=1362 + _TESTMESSAGEWITHEXTENSION._serialized_end=1398 + _TESTEXTENSION._serialized_start=1400 + _TESTEXTENSION._serialized_end=1522 + _TESTDEFAULTENUMVALUE._serialized_start=1524 + _TESTDEFAULTENUMVALUE._serialized_end=1605 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py new file mode 100644 index 0000000000..5498deafa9 --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format_proto3.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 +from google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-google/protobuf/util/json_format_proto3.proto\x12\x06proto3\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1egoogle/protobuf/unittest.proto\"\x1c\n\x0bMessageType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x94\x05\n\x0bTestMessage\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x13\n\x0bint32_value\x18\x02 \x01(\x05\x12\x13\n\x0bint64_value\x18\x03 \x01(\x03\x12\x14\n\x0cuint32_value\x18\x04 \x01(\r\x12\x14\n\x0cuint64_value\x18\x05 \x01(\x04\x12\x13\n\x0b\x66loat_value\x18\x06 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x07 \x01(\x01\x12\x14\n\x0cstring_value\x18\x08 \x01(\t\x12\x13\n\x0b\x62ytes_value\x18\t \x01(\x0c\x12$\n\nenum_value\x18\n \x01(\x0e\x32\x10.proto3.EnumType\x12*\n\rmessage_value\x18\x0b \x01(\x0b\x32\x13.proto3.MessageType\x12\x1b\n\x13repeated_bool_value\x18\x15 \x03(\x08\x12\x1c\n\x14repeated_int32_value\x18\x16 \x03(\x05\x12\x1c\n\x14repeated_int64_value\x18\x17 \x03(\x03\x12\x1d\n\x15repeated_uint32_value\x18\x18 \x03(\r\x12\x1d\n\x15repeated_uint64_value\x18\x19 \x03(\x04\x12\x1c\n\x14repeated_float_value\x18\x1a \x03(\x02\x12\x1d\n\x15repeated_double_value\x18\x1b \x03(\x01\x12\x1d\n\x15repeated_string_value\x18\x1c \x03(\t\x12\x1c\n\x14repeated_bytes_value\x18\x1d \x03(\x0c\x12-\n\x13repeated_enum_value\x18\x1e \x03(\x0e\x32\x10.proto3.EnumType\x12\x33\n\x16repeated_message_value\x18\x1f \x03(\x0b\x32\x13.proto3.MessageType\"\x8c\x02\n\tTestOneof\x12\x1b\n\x11oneof_int32_value\x18\x01 \x01(\x05H\x00\x12\x1c\n\x12oneof_string_value\x18\x02 \x01(\tH\x00\x12\x1b\n\x11oneof_bytes_value\x18\x03 \x01(\x0cH\x00\x12,\n\x10oneof_enum_value\x18\x04 \x01(\x0e\x32\x10.proto3.EnumTypeH\x00\x12\x32\n\x13oneof_message_value\x18\x05 \x01(\x0b\x32\x13.proto3.MessageTypeH\x00\x12\x36\n\x10oneof_null_value\x18\x06 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\r\n\x0boneof_value\"\xe1\x04\n\x07TestMap\x12.\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\x1c.proto3.TestMap.BoolMapEntry\x12\x30\n\tint32_map\x18\x02 \x03(\x0b\x32\x1d.proto3.TestMap.Int32MapEntry\x12\x30\n\tint64_map\x18\x03 \x03(\x0b\x32\x1d.proto3.TestMap.Int64MapEntry\x12\x32\n\nuint32_map\x18\x04 \x03(\x0b\x32\x1e.proto3.TestMap.Uint32MapEntry\x12\x32\n\nuint64_map\x18\x05 \x03(\x0b\x32\x1e.proto3.TestMap.Uint64MapEntry\x12\x32\n\nstring_map\x18\x06 \x03(\x0b\x32\x1e.proto3.TestMap.StringMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x85\x06\n\rTestNestedMap\x12\x34\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\".proto3.TestNestedMap.BoolMapEntry\x12\x36\n\tint32_map\x18\x02 \x03(\x0b\x32#.proto3.TestNestedMap.Int32MapEntry\x12\x36\n\tint64_map\x18\x03 \x03(\x0b\x32#.proto3.TestNestedMap.Int64MapEntry\x12\x38\n\nuint32_map\x18\x04 \x03(\x0b\x32$.proto3.TestNestedMap.Uint32MapEntry\x12\x38\n\nuint64_map\x18\x05 \x03(\x0b\x32$.proto3.TestNestedMap.Uint64MapEntry\x12\x38\n\nstring_map\x18\x06 \x03(\x0b\x32$.proto3.TestNestedMap.StringMapEntry\x12\x32\n\x07map_map\x18\x07 \x03(\x0b\x32!.proto3.TestNestedMap.MapMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x44\n\x0bMapMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.proto3.TestNestedMap:\x02\x38\x01\"{\n\rTestStringMap\x12\x38\n\nstring_map\x18\x01 \x03(\x0b\x32$.proto3.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0bTestWrapper\x12.\n\nbool_value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0bint32_value\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x30\n\x0bint64_value\x18\x03 \x01(\x0b\x32\x1b.google.protobuf.Int64Value\x12\x32\n\x0cuint32_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x32\n\x0cuint64_value\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x30\n\x0b\x66loat_value\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.FloatValue\x12\x32\n\x0c\x64ouble_value\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x32\n\x0cstring_value\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\x0b\x62ytes_value\x18\t \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x12\x37\n\x13repeated_bool_value\x18\x0b \x03(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\x14repeated_int32_value\x18\x0c \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x39\n\x14repeated_int64_value\x18\r \x03(\x0b\x32\x1b.google.protobuf.Int64Value\x12;\n\x15repeated_uint32_value\x18\x0e \x03(\x0b\x32\x1c.google.protobuf.UInt32Value\x12;\n\x15repeated_uint64_value\x18\x0f \x03(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x39\n\x14repeated_float_value\x18\x10 \x03(\x0b\x32\x1b.google.protobuf.FloatValue\x12;\n\x15repeated_double_value\x18\x11 \x03(\x0b\x32\x1c.google.protobuf.DoubleValue\x12;\n\x15repeated_string_value\x18\x12 \x03(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\x14repeated_bytes_value\x18\x13 \x03(\x0b\x32\x1b.google.protobuf.BytesValue\"n\n\rTestTimestamp\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"k\n\x0cTestDuration\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x19.google.protobuf.Duration\":\n\rTestFieldMask\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\"e\n\nTestStruct\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12/\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"\\\n\x07TestAny\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\x12,\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x14.google.protobuf.Any\"b\n\tTestValue\x12%\n\x05value\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\x12.\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Value\"n\n\rTestListValue\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.ListValue\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.ListValue\"\x89\x01\n\rTestBoolValue\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x34\n\x08\x62ool_map\x18\x02 \x03(\x0b\x32\".proto3.TestBoolValue.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"+\n\x12TestCustomJsonName\x12\x15\n\x05value\x18\x01 \x01(\x05R\x06@value\"J\n\x0eTestExtensions\x12\x38\n\nextensions\x18\x01 \x01(\x0b\x32$.protobuf_unittest.TestAllExtensions\"\x84\x01\n\rTestEnumValue\x12%\n\x0b\x65num_value1\x18\x01 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value2\x18\x02 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value3\x18\x03 \x01(\x0e\x32\x10.proto3.EnumType*\x1c\n\x08\x45numType\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\x42,\n\x18\x63om.google.protobuf.utilB\x10JsonFormatProto3b\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\030com.google.protobuf.utilB\020JsonFormatProto3' + _TESTMAP_BOOLMAPENTRY._options = None + _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT32MAPENTRY._options = None + _TESTMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT64MAPENTRY._options = None + _TESTMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT32MAPENTRY._options = None + _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT64MAPENTRY._options = None + _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_STRINGMAPENTRY._options = None + _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_BOOLMAPENTRY._options = None + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT32MAPENTRY._options = None + _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT64MAPENTRY._options = None + _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT32MAPENTRY._options = None + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT64MAPENTRY._options = None + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_STRINGMAPENTRY._options = None + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_MAPMAPENTRY._options = None + _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTBOOLVALUE_BOOLMAPENTRY._options = None + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\001' + _ENUMTYPE._serialized_start=4849 + _ENUMTYPE._serialized_end=4877 + _MESSAGETYPE._serialized_start=277 + _MESSAGETYPE._serialized_end=305 + _TESTMESSAGE._serialized_start=308 + _TESTMESSAGE._serialized_end=968 + _TESTONEOF._serialized_start=971 + _TESTONEOF._serialized_end=1239 + _TESTMAP._serialized_start=1242 + _TESTMAP._serialized_end=1851 + _TESTMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTMAP_INT32MAPENTRY._serialized_start=1605 + _TESTMAP_INT32MAPENTRY._serialized_end=1652 + _TESTMAP_INT64MAPENTRY._serialized_start=1654 + _TESTMAP_INT64MAPENTRY._serialized_end=1701 + _TESTMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP._serialized_start=1854 + _TESTNESTEDMAP._serialized_end=2627 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627 + _TESTSTRINGMAP._serialized_start=2629 + _TESTSTRINGMAP._serialized_end=2752 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752 + _TESTWRAPPER._serialized_start=2755 + _TESTWRAPPER._serialized_end=3761 + _TESTTIMESTAMP._serialized_start=3763 + _TESTTIMESTAMP._serialized_end=3873 + _TESTDURATION._serialized_start=3875 + _TESTDURATION._serialized_end=3982 + _TESTFIELDMASK._serialized_start=3984 + _TESTFIELDMASK._serialized_end=4042 + _TESTSTRUCT._serialized_start=4044 + _TESTSTRUCT._serialized_end=4145 + _TESTANY._serialized_start=4147 + _TESTANY._serialized_end=4239 + _TESTVALUE._serialized_start=4241 + _TESTVALUE._serialized_end=4339 + _TESTLISTVALUE._serialized_start=4341 + _TESTLISTVALUE._serialized_end=4451 + _TESTBOOLVALUE._serialized_start=4454 + _TESTBOOLVALUE._serialized_end=4591 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603 + _TESTCUSTOMJSONNAME._serialized_start=4593 + _TESTCUSTOMJSONNAME._serialized_end=4636 + _TESTEXTENSIONS._serialized_start=4638 + _TESTEXTENSIONS._serialized_end=4712 + _TESTENUMVALUE._serialized_start=4715 + _TESTENUMVALUE._serialized_end=4847 +# @@protoc_insertion_point(module_scope) diff --git a/openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py b/openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py new file mode 100644 index 0000000000..e49eb4c15d --- /dev/null +++ b/openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/wrappers.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"\x1c\n\x0b\x44oubleValue\x12\r\n\x05value\x18\x01 \x01(\x01\"\x1b\n\nFloatValue\x12\r\n\x05value\x18\x01 \x01(\x02\"\x1b\n\nInt64Value\x12\r\n\x05value\x18\x01 \x01(\x03\"\x1c\n\x0bUInt64Value\x12\r\n\x05value\x18\x01 \x01(\x04\"\x1b\n\nInt32Value\x12\r\n\x05value\x18\x01 \x01(\x05\"\x1c\n\x0bUInt32Value\x12\r\n\x05value\x18\x01 \x01(\r\"\x1a\n\tBoolValue\x12\r\n\x05value\x18\x01 \x01(\x08\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nBytesValue\x12\r\n\x05value\x18\x01 \x01(\x0c\x42\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DOUBLEVALUE._serialized_start=51 + _DOUBLEVALUE._serialized_end=79 + _FLOATVALUE._serialized_start=81 + _FLOATVALUE._serialized_end=108 + _INT64VALUE._serialized_start=110 + _INT64VALUE._serialized_end=137 + _UINT64VALUE._serialized_start=139 + _UINT64VALUE._serialized_end=167 + _INT32VALUE._serialized_start=169 + _INT32VALUE._serialized_end=196 + _UINT32VALUE._serialized_start=198 + _UINT32VALUE._serialized_end=226 + _BOOLVALUE._serialized_start=228 + _BOOLVALUE._serialized_end=254 + _STRINGVALUE._serialized_start=256 + _STRINGVALUE._serialized_end=284 + _BYTESVALUE._serialized_start=286 + _BYTESVALUE._serialized_end=313 +# @@protoc_insertion_point(module_scope) From 882e00baefda3be849c5b92be897d0b9d27ad3e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:52:47 +0200 Subject: [PATCH 0960/1030] use Logger instead of PypeLogger --- openpype/client/entities.py | 2 +- openpype/hosts/maya/api/plugin.py | 5 +-- openpype/hosts/tvpaint/worker/worker_job.py | 4 +- .../webserver_service/webpublish_routes.py | 6 +-- .../webserver_service/webserver_cli.py | 4 +- openpype/lib/applications.py | 10 ++--- openpype/lib/execute.py | 4 +- openpype/lib/path_templates.py | 5 --- openpype/lib/remote_publish.py | 4 +- openpype/modules/base.py | 12 ++--- openpype/modules/deadline/deadline_module.py | 4 +- .../ftrack/ftrack_server/ftrack_server.py | 21 ++++----- openpype/modules/log_viewer/tray/models.py | 14 +++--- openpype/modules/royalrender/api.py | 12 +++-- openpype/modules/sync_server/sync_server.py | 5 +-- .../modules/sync_server/sync_server_module.py | 4 +- openpype/modules/sync_server/tray/app.py | 3 -- .../modules/sync_server/tray/delegates.py | 5 +-- openpype/modules/sync_server/tray/lib.py | 5 --- openpype/modules/sync_server/tray/models.py | 5 +-- openpype/modules/sync_server/tray/widgets.py | 5 +-- .../modules/timers_manager/idle_threads.py | 4 +- openpype/modules/webserver/server.py | 45 ++++++++++++------- openpype/pipeline/anatomy.py | 4 +- openpype/pype_commands.py | 24 +++++----- openpype/settings/entities/base_entity.py | 4 +- 26 files changed, 109 insertions(+), 111 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index f1f1d30214..3d2730a17c 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1455,7 +1455,7 @@ def get_workfile_info( """ ## Custom data storage: - Settings - OP settings overrides and local settings -- Logging - logs from PypeLogger +- Logging - logs from Logger - Webpublisher - jobs - Ftrack - events - Maya - Shaders diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index e50ebfccad..39d821f620 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -4,6 +4,7 @@ from maya import cmds import qargparse +from openpype.lib import Logger from openpype.pipeline import ( LegacyCreator, LoaderPlugin, @@ -50,9 +51,7 @@ def get_reference_node(members, log=None): # Warn the user when we're taking the highest reference node if len(references) > 1: if not log: - from openpype.lib import PypeLogger - - log = PypeLogger().get_logger(__name__) + log = Logger.get_logger(__name__) log.warning("More than one reference node found in " "container, using highest reference node: " diff --git a/openpype/hosts/tvpaint/worker/worker_job.py b/openpype/hosts/tvpaint/worker/worker_job.py index 1c785ab2ee..95c0a678bc 100644 --- a/openpype/hosts/tvpaint/worker/worker_job.py +++ b/openpype/hosts/tvpaint/worker/worker_job.py @@ -9,7 +9,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty import six -from openpype.api import PypeLogger +from openpype.lib import Logger from openpype.modules import ModulesManager @@ -328,7 +328,7 @@ class TVPaintCommands: def log(self): """Access to logger object.""" if self._log is None: - self._log = PypeLogger.get_logger(self.__class__.__name__) + self._log = Logger.get_logger(self.__class__.__name__) return self._log @property diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 6444a5191d..2e9d460a98 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -12,9 +12,7 @@ from openpype.client import ( get_assets, OpenPypeMongoConnection, ) -from openpype.lib import ( - PypeLogger, -) +from openpype.lib import Logger from openpype.lib.remote_publish import ( get_task_data, ERROR_STATUS, @@ -23,7 +21,7 @@ from openpype.lib.remote_publish import ( from openpype.settings import get_project_settings from openpype_modules.webserver.base_routes import RestApiEndpoint -log = PypeLogger.get_logger("WebpublishRoutes") +log = Logger.get_logger("WebpublishRoutes") class ResourceRestApiEndpoint(RestApiEndpoint): diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py index 6620e5d5cf..936bd9735f 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py @@ -7,7 +7,7 @@ import json import subprocess from openpype.client import OpenPypeMongoConnection -from openpype.lib import PypeLogger +from openpype.lib import Logger from .webpublish_routes import ( RestApiResource, @@ -28,7 +28,7 @@ from openpype.lib.remote_publish import ( ) -log = PypeLogger.get_logger("webserver_gui") +log = Logger.get_logger("webserver_gui") def run_webserver(*args, **kwargs): diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 074e815160..73f9607835 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -24,7 +24,7 @@ from openpype.settings.constants import ( METADATA_KEYS, M_DYNAMIC_KEY_LABEL ) -from . import PypeLogger +from .log import Logger from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username @@ -138,7 +138,7 @@ def get_logger(): """Global lib.applications logger getter.""" global _logger if _logger is None: - _logger = PypeLogger.get_logger(__name__) + _logger = Logger.get_logger(__name__) return _logger @@ -373,7 +373,7 @@ class ApplicationManager: """ def __init__(self, system_settings=None): - self.log = PypeLogger.get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.app_groups = {} self.applications = {} @@ -735,7 +735,7 @@ class LaunchHook: Always should be called """ - self.log = PypeLogger().get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.launch_context = launch_context @@ -877,7 +877,7 @@ class ApplicationLaunchContext: # Logger logger_name = "{}-{}".format(self.__class__.__name__, self.app_name) - self.log = PypeLogger.get_logger(logger_name) + self.log = Logger.get_logger(logger_name) self.executable = executable diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index c3e35772f3..f1f2a4fa0a 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -5,7 +5,7 @@ import platform import json import tempfile -from .log import PypeLogger as Logger +from .log import Logger from .vendor_bin_utils import find_executable # MSDN process creation flag (Windows only) @@ -40,7 +40,7 @@ def execute(args, log_levels = ['DEBUG:', 'INFO:', 'ERROR:', 'WARNING:', 'CRITICAL:'] - log = Logger().get_logger('execute') + log = Logger.get_logger('execute') log.info("Executing ({})".format(" ".join(args))) popen = subprocess.Popen( args, diff --git a/openpype/lib/path_templates.py b/openpype/lib/path_templates.py index e4b18ec258..b160054e38 100644 --- a/openpype/lib/path_templates.py +++ b/openpype/lib/path_templates.py @@ -6,11 +6,6 @@ import collections import six -from .log import PypeLogger - -log = PypeLogger.get_logger(__name__) - - KEY_PATTERN = re.compile(r"(\{.*?[^{0]*\})") KEY_PADDING_PATTERN = re.compile(r"([^:]+)\S+[><]\S+") SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index b4b05c053b..2a901544cc 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -66,7 +66,7 @@ def publish(log, close_plugin_name=None, raise_error=False): """Loops through all plugins, logs to console. Used for tests. Args: - log (OpenPypeLogger) + log (openpype.lib.Logger) close_plugin_name (str): name of plugin with responsibility to close host app """ @@ -98,7 +98,7 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): Args: dbcon (OpenPypeMongoConnection) _id (str) - id of current job in DB - log (OpenPypeLogger) + log (openpype.lib.Logger) batch_id (str) - id sent from frontend close_plugin_name (str): name of plugin with responsibility to close host app diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1316d7f734..7fc848af2d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -26,7 +26,7 @@ from openpype.settings.lib import ( get_studio_system_settings_overrides, load_json_file ) -from openpype.lib import PypeLogger +from openpype.lib import Logger # Files that will be always ignored on modules import IGNORED_FILENAMES = ( @@ -93,7 +93,7 @@ class _ModuleClass(object): def log(self): if self._log is None: super(_ModuleClass, self).__setattr__( - "_log", PypeLogger.get_logger(self.name) + "_log", Logger.get_logger(self.name) ) return self._log @@ -290,7 +290,7 @@ def _load_modules(): # Change `sys.modules` sys.modules[modules_key] = openpype_modules = _ModuleClass(modules_key) - log = PypeLogger.get_logger("ModulesLoader") + log = Logger.get_logger("ModulesLoader") # Look for OpenPype modules in paths defined with `get_module_dirs` # - dynamically imported OpenPype modules and addons @@ -440,7 +440,7 @@ class OpenPypeModule: def __init__(self, manager, settings): self.manager = manager - self.log = PypeLogger.get_logger(self.name) + self.log = Logger.get_logger(self.name) self.initialize(settings) @@ -1059,7 +1059,7 @@ class TrayModulesManager(ModulesManager): ) def __init__(self): - self.log = PypeLogger.get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.modules = [] self.modules_by_id = {} @@ -1235,7 +1235,7 @@ def get_module_settings_defs(): settings_defs = [] - log = PypeLogger.get_logger("ModuleSettingsLoad") + log = Logger.get_logger("ModuleSettingsLoad") for raw_module in openpype_modules: for attr_name in dir(raw_module): diff --git a/openpype/modules/deadline/deadline_module.py b/openpype/modules/deadline/deadline_module.py index c30db75188..bbd0f74e8a 100644 --- a/openpype/modules/deadline/deadline_module.py +++ b/openpype/modules/deadline/deadline_module.py @@ -3,7 +3,7 @@ import requests import six import sys -from openpype.lib import requests_get, PypeLogger +from openpype.lib import requests_get, Logger from openpype.modules import OpenPypeModule from openpype_interfaces import IPluginPaths @@ -58,7 +58,7 @@ class DeadlineModule(OpenPypeModule, IPluginPaths): """ if not log: - log = PypeLogger.get_logger(__name__) + log = Logger.get_logger(__name__) argument = "{}/api/pools?NamesOnly=true".format(webservice) try: diff --git a/openpype/modules/ftrack/ftrack_server/ftrack_server.py b/openpype/modules/ftrack/ftrack_server/ftrack_server.py index 8944591b71..c75b8f7172 100644 --- a/openpype/modules/ftrack/ftrack_server/ftrack_server.py +++ b/openpype/modules/ftrack/ftrack_server/ftrack_server.py @@ -7,12 +7,10 @@ import traceback import ftrack_api from openpype.lib import ( - PypeLogger, + Logger, modules_from_path ) -log = PypeLogger.get_logger(__name__) - """ # Required - Needed for connection to Ftrack FTRACK_SERVER # Ftrack server e.g. "https://myFtrack.ftrackapp.com" @@ -43,10 +41,13 @@ class FtrackServer: server.run_server() .. """ + # set Ftrack logging to Warning only - OPTIONAL ftrack_log = logging.getLogger("ftrack_api") ftrack_log.setLevel(logging.WARNING) + self.log = Logger.get_logger(__name__) + self.stopped = True self.is_running = False @@ -72,7 +73,7 @@ class FtrackServer: # Get all modules with functions modules, crashed = modules_from_path(path) for filepath, exc_info in crashed: - log.warning("Filepath load crashed {}.\n{}".format( + self.log.warning("Filepath load crashed {}.\n{}".format( filepath, traceback.format_exception(*exc_info) )) @@ -87,7 +88,7 @@ class FtrackServer: break if not register_function: - log.warning( + self.log.warning( "\"{}\" - Missing register method".format(filepath) ) continue @@ -97,7 +98,7 @@ class FtrackServer: ) if not register_functions: - log.warning(( + self.log.warning(( "There are no events with `register` function" " in registered paths: \"{}\"" ).format("| ".join(paths))) @@ -106,7 +107,7 @@ class FtrackServer: try: register_func(self.session) except Exception: - log.warning( + self.log.warning( "\"{}\" - register was not successful".format(filepath), exc_info=True ) @@ -141,7 +142,7 @@ class FtrackServer: self.session = session if load_files: if not self.handler_paths: - log.warning(( + self.log.warning(( "Paths to event handlers are not set." " Ftrack server won't launch." )) @@ -151,8 +152,8 @@ class FtrackServer: self.set_files(self.handler_paths) msg = "Registration of event handlers has finished!" - log.info(len(msg) * "*") - log.info(msg) + self.log.info(len(msg) * "*") + self.log.info(msg) # keep event_hub on session running self.session.event_hub.wait() diff --git a/openpype/modules/log_viewer/tray/models.py b/openpype/modules/log_viewer/tray/models.py index aea62c381b..d369ffeb64 100644 --- a/openpype/modules/log_viewer/tray/models.py +++ b/openpype/modules/log_viewer/tray/models.py @@ -1,6 +1,6 @@ import collections from Qt import QtCore, QtGui -from openpype.lib.log import PypeLogger +from openpype.lib import Logger class LogModel(QtGui.QStandardItemModel): @@ -41,14 +41,14 @@ class LogModel(QtGui.QStandardItemModel): self.dbcon = None # Crash if connection is not possible to skip this module - if not PypeLogger.initialized: - PypeLogger.initialize() + if not Logger.initialized: + Logger.initialize() - connection = PypeLogger.get_log_mongo_connection() + connection = Logger.get_log_mongo_connection() if connection: - PypeLogger.bootstrap_mongo_log() - database = connection[PypeLogger.log_database_name] - self.dbcon = database[PypeLogger.log_collection_name] + Logger.bootstrap_mongo_log() + database = connection[Logger.log_database_name] + self.dbcon = database[Logger.log_collection_name] def headerData(self, section, orientation, role): if ( diff --git a/openpype/modules/royalrender/api.py b/openpype/modules/royalrender/api.py index ed9e71f240..de1dba8724 100644 --- a/openpype/modules/royalrender/api.py +++ b/openpype/modules/royalrender/api.py @@ -5,13 +5,10 @@ import os from openpype.settings import get_project_settings from openpype.lib.local_settings import OpenPypeSettingsRegistry -from openpype.lib import PypeLogger, run_subprocess +from openpype.lib import Logger, run_subprocess from .rr_job import RRJob, SubmitFile, SubmitterParameter -log = PypeLogger.get_logger("RoyalRender") - - class Api: _settings = None @@ -19,6 +16,7 @@ class Api: RR_SUBMIT_API = 2 def __init__(self, settings, project=None): + self.log = Logger.get_logger("RoyalRender") self._settings = settings self._initialize_rr(project) @@ -137,7 +135,7 @@ class Api: rr_console += ".exe" args = [rr_console, file] - run_subprocess(" ".join(args), logger=log) + run_subprocess(" ".join(args), logger=self.log) def _submit_using_api(self, file): # type: (SubmitFile) -> None @@ -159,11 +157,11 @@ class Api: rr_server = tcp.getRRServer() if len(rr_server) == 0: - log.info("Got RR IP address {}".format(rr_server)) + self.log.info("Got RR IP address {}".format(rr_server)) # TODO: Port is hardcoded in RR? If not, move it to Settings if not tcp.setServer(rr_server, 7773): - log.error( + self.log.error( "Can not set RR server: {}".format(tcp.errorMessage())) raise RoyalRenderException(tcp.errorMessage()) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 97538fcd4e..d01ab1d3a0 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -6,12 +6,11 @@ import concurrent.futures from concurrent.futures._base import CancelledError from .providers import lib -from openpype.lib import PypeLogger +from openpype.lib import Logger from .utils import SyncStatus, ResumableError - -log = PypeLogger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") async def upload(module, project_name, file, representation, provider_name, diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index c7f9484e55..c72b310907 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -13,7 +13,7 @@ from openpype.settings import ( get_project_settings, get_system_settings, ) -from openpype.lib import PypeLogger, get_local_site_id +from openpype.lib import Logger, get_local_site_id from openpype.pipeline import AvalonMongoDB, Anatomy from openpype.settings.lib import ( get_default_anatomy_settings, @@ -28,7 +28,7 @@ from .utils import time_function, SyncStatus, SiteAlreadyPresentError from openpype.client import get_representations, get_representation_by_id -log = PypeLogger.get_logger("SyncServer") +log = Logger.get_logger("SyncServer") class SyncServerModule(OpenPypeModule, ITrayModule): diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index 96fad6a247..9b9768327e 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -2,7 +2,6 @@ from Qt import QtWidgets, QtCore, QtGui from openpype.tools.settings import style -from openpype.lib import PypeLogger from openpype import resources from .widgets import ( @@ -10,8 +9,6 @@ from .widgets import ( SyncRepresentationSummaryWidget ) -log = PypeLogger().get_logger("SyncServer") - class SyncServerWindow(QtWidgets.QDialog): """ diff --git a/openpype/modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py index 5ab809a816..988eb40d28 100644 --- a/openpype/modules/sync_server/tray/delegates.py +++ b/openpype/modules/sync_server/tray/delegates.py @@ -1,8 +1,7 @@ import os from Qt import QtCore, QtWidgets, QtGui -from openpype.lib import PypeLogger -from . import lib +from openpype.lib import Logger from openpype.tools.utils.constants import ( LOCAL_PROVIDER_ROLE, @@ -16,7 +15,7 @@ from openpype.tools.utils.constants import ( EDIT_ICON_ROLE ) -log = PypeLogger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") class PriorityDelegate(QtWidgets.QStyledItemDelegate): diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 87344be634..ff93815639 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -2,11 +2,6 @@ import attr import abc import six -from openpype.lib import PypeLogger - - -log = PypeLogger().get_logger("SyncServer") - STATUS = { 0: 'In Progress', 1: 'Queued', diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 629c4cbbf1..d63d046508 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -9,8 +9,7 @@ import qtawesome from openpype.tools.utils.delegates import pretty_timestamp -from openpype.lib import PypeLogger -from openpype.api import get_local_site_id +from openpype.lib import Logger, get_local_site_id from openpype.client import get_representation_by_id from . import lib @@ -33,7 +32,7 @@ from openpype.tools.utils.constants import ( ) -log = PypeLogger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") class _SyncRepresentationModel(QtCore.QAbstractTableModel): diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index b4ee447ac4..c40aa98f24 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -9,8 +9,7 @@ import qtawesome from openpype.tools.settings import style -from openpype.api import get_local_site_id -from openpype.lib import PypeLogger +from openpype.lib import Logger, get_local_site_id from openpype.tools.utils.delegates import pretty_timestamp @@ -36,7 +35,7 @@ from openpype.tools.utils.constants import ( TRIES_ROLE ) -log = PypeLogger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") class SyncProjectListWidget(QtWidgets.QWidget): diff --git a/openpype/modules/timers_manager/idle_threads.py b/openpype/modules/timers_manager/idle_threads.py index 9ec27e659b..7242761143 100644 --- a/openpype/modules/timers_manager/idle_threads.py +++ b/openpype/modules/timers_manager/idle_threads.py @@ -2,7 +2,7 @@ import time from Qt import QtCore from pynput import mouse, keyboard -from openpype.lib import PypeLogger +from openpype.lib import Logger class IdleItem: @@ -31,7 +31,7 @@ class IdleManager(QtCore.QThread): def __init__(self): super(IdleManager, self).__init__() - self.log = PypeLogger.get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.signal_reset_timer.connect(self._reset_time) self.idle_item = IdleItem() diff --git a/openpype/modules/webserver/server.py b/openpype/modules/webserver/server.py index 82b681f406..44b14acbb6 100644 --- a/openpype/modules/webserver/server.py +++ b/openpype/modules/webserver/server.py @@ -4,16 +4,16 @@ import asyncio from aiohttp import web -from openpype.lib import PypeLogger +from openpype.lib import Logger from .cors_middleware import cors_middleware -log = PypeLogger.get_logger("WebServer") - class WebServerManager: """Manger that care about web server thread.""" def __init__(self, port=None, host=None): + self._log = None + self.port = port or 8079 self.host = host or "localhost" @@ -33,6 +33,12 @@ class WebServerManager: self.webserver_thread = WebServerThread(self) + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + @property def url(self): return "http://{}:{}".format(self.host, self.port) @@ -51,12 +57,12 @@ class WebServerManager: if not self.is_running: return try: - log.debug("Stopping Web server") + self.log.debug("Stopping Web server") self.webserver_thread.is_running = False self.webserver_thread.stop() except Exception: - log.warning( + self.log.warning( "Error has happened during Killing Web server", exc_info=True ) @@ -74,7 +80,10 @@ class WebServerManager: class WebServerThread(threading.Thread): """ Listener for requests in thread.""" + def __init__(self, manager): + self._log = None + super(WebServerThread, self).__init__() self.is_running = False @@ -84,6 +93,12 @@ class WebServerThread(threading.Thread): self.site = None self.tasks = [] + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + @property def port(self): return self.manager.port @@ -96,13 +111,13 @@ class WebServerThread(threading.Thread): self.is_running = True try: - log.info("Starting WebServer server") + self.log.info("Starting WebServer server") self.loop = asyncio.new_event_loop() # create new loop for thread asyncio.set_event_loop(self.loop) self.loop.run_until_complete(self.start_server()) - log.debug( + self.log.debug( "Running Web server on URL: \"localhost:{}\"".format(self.port) ) @@ -110,7 +125,7 @@ class WebServerThread(threading.Thread): self.loop.run_forever() except Exception: - log.warning( + self.log.warning( "Web Server service has failed", exc_info=True ) finally: @@ -118,7 +133,7 @@ class WebServerThread(threading.Thread): self.is_running = False self.manager.thread_stopped() - log.info("Web server stopped") + self.log.info("Web server stopped") async def start_server(self): """ Starts runner and TCPsite """ @@ -138,17 +153,17 @@ class WebServerThread(threading.Thread): while self.is_running: while self.tasks: task = self.tasks.pop(0) - log.debug("waiting for task {}".format(task)) + self.log.debug("waiting for task {}".format(task)) await task - log.debug("returned value {}".format(task.result)) + self.log.debug("returned value {}".format(task.result)) await asyncio.sleep(0.5) - log.debug("Starting shutdown") + self.log.debug("Starting shutdown") await self.site.stop() - log.debug("Site stopped") + self.log.debug("Site stopped") await self.runner.cleanup() - log.debug("Runner stopped") + self.log.debug("Runner stopped") tasks = [ task for task in asyncio.all_tasks() @@ -156,7 +171,7 @@ class WebServerThread(threading.Thread): ] list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks results = await asyncio.gather(*tasks, return_exceptions=True) - log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + self.log.debug(f'Finished awaiting cancelled tasks, results: {results}...') await self.loop.shutdown_asyncgens() # to really make sure everything else has time to stop await asyncio.sleep(0.07) diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 08db4749b3..cb6e07154b 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -14,9 +14,9 @@ from openpype.lib.path_templates import ( TemplatesDict, FormatObject, ) -from openpype.lib.log import PypeLogger +from openpype.lib.log import Logger -log = PypeLogger.get_logger(__name__) +log = Logger.get_logger(__name__) class ProjectNotSet(Exception): diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 66bf5e9bb4..71fa7fb6c0 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -5,7 +5,6 @@ import sys import json import time -from openpype.lib import PypeLogger from openpype.api import get_app_environments_for_context from openpype.lib.plugin_tools import get_batch_asset_task_info from openpype.lib.remote_publish import ( @@ -27,10 +26,11 @@ class PypeCommands: """ @staticmethod def launch_tray(): - PypeLogger.set_process_name("Tray") - + from openpype.lib import Logger from openpype.tools import tray + Logger.set_process_name("Tray") + tray.main() @staticmethod @@ -47,10 +47,12 @@ class PypeCommands: @staticmethod def add_modules(click_func): """Modules/Addons can add their cli commands dynamically.""" + + from openpype.lib import Logger from openpype.modules import ModulesManager manager = ModulesManager() - log = PypeLogger.get_logger("AddModulesCLI") + log = Logger.get_logger("CLI-AddModules") for module in manager.modules: try: module.cli(click_func) @@ -96,10 +98,10 @@ class PypeCommands: Raises: RuntimeError: When there is no path to process. """ + + from openpype.lib import Logger from openpype.modules import ModulesManager from openpype.pipeline import install_openpype_plugins - - from openpype.api import Logger from openpype.tools.utils.host_tools import show_publish from openpype.tools.utils.lib import qt_app_context @@ -107,7 +109,7 @@ class PypeCommands: import pyblish.api import pyblish.util - log = Logger.get_logger() + log = Logger.get_logger("CLI-publish") install_openpype_plugins() @@ -195,11 +197,12 @@ class PypeCommands: targets (list): Pyblish targets (to choose validator for example) """ + import pyblish.api - from openpype.api import Logger from openpype.lib import ApplicationManager - log = Logger.get_logger() + from openpype.lib import Logger + log = Logger.get_logger("CLI-remotepublishfromapp") log.info("remotepublishphotoshop command") @@ -311,10 +314,11 @@ class PypeCommands: import pyblish.api import pyblish.util + from openpype.lib import Logger from openpype.pipeline import install_host from openpype.hosts.webpublisher import api as webpublisher - log = PypeLogger.get_logger() + log = Logger.get_logger("remotepublish") log.info("remotepublish command") diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 741f13c49b..f28fefdf5a 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -15,7 +15,7 @@ from .exceptions import ( EntitySchemaError ) -from openpype.lib import PypeLogger +from openpype.lib import Logger @six.add_metaclass(ABCMeta) @@ -478,7 +478,7 @@ class BaseItemEntity(BaseEntity): def log(self): """Auto created logger for debugging or warnings.""" if self._log is None: - self._log = PypeLogger.get_logger(self.__class__.__name__) + self._log = Logger.get_logger(self.__class__.__name__) return self._log @abstractproperty From 3cdfc5b350dd1ccc3e940967f13cdda42c987739 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:54:07 +0200 Subject: [PATCH 0961/1030] use class log attribues instead of global loggers --- openpype/modules/sync_server/sync_server.py | 61 ++++++++-------- .../modules/sync_server/sync_server_module.py | 69 +++++++++++-------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d01ab1d3a0..8b11055e65 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -10,8 +10,6 @@ from openpype.lib import Logger from .utils import SyncStatus, ResumableError -log = Logger.get_logger("SyncServer") - async def upload(module, project_name, file, representation, provider_name, remote_site_name, tree=None, preset=None): @@ -237,6 +235,7 @@ class SyncServerThread(threading.Thread): Stopped when tray is closed. """ def __init__(self, module): + self.log = Logger.get_logger(self.__class__.__name__) super(SyncServerThread, self).__init__() self.module = module self.loop = None @@ -248,17 +247,17 @@ class SyncServerThread(threading.Thread): self.is_running = True try: - log.info("Starting Sync Server") + self.log.info("Starting Sync Server") self.loop = asyncio.new_event_loop() # create new loop for thread asyncio.set_event_loop(self.loop) self.loop.set_default_executor(self.executor) asyncio.ensure_future(self.check_shutdown(), loop=self.loop) asyncio.ensure_future(self.sync_loop(), loop=self.loop) - log.info("Sync Server Started") + self.log.info("Sync Server Started") self.loop.run_forever() except Exception: - log.warning( + self.log.warning( "Sync Server service has failed", exc_info=True ) finally: @@ -378,8 +377,9 @@ class SyncServerThread(threading.Thread): )) processed_file_path.add(file_path) - log.debug("Sync tasks count {}". - format(len(task_files_to_process))) + self.log.debug("Sync tasks count {}".format( + len(task_files_to_process) + )) files_created = await asyncio.gather( *task_files_to_process, return_exceptions=True) @@ -398,28 +398,31 @@ class SyncServerThread(threading.Thread): error) duration = time.time() - start_time - log.debug("One loop took {:.2f}s".format(duration)) + self.log.debug("One loop took {:.2f}s".format(duration)) delay = self.module.get_loop_delay(project_name) - log.debug("Waiting for {} seconds to new loop".format(delay)) + self.log.debug( + "Waiting for {} seconds to new loop".format(delay) + ) self.timer = asyncio.create_task(self.run_timer(delay)) await asyncio.gather(self.timer) except ConnectionResetError: - log.warning("ConnectionResetError in sync loop, " - "trying next loop", - exc_info=True) + self.log.warning( + "ConnectionResetError in sync loop, trying next loop", + exc_info=True) except CancelledError: # just stopping server pass except ResumableError: - log.warning("ResumableError in sync loop, " - "trying next loop", - exc_info=True) + self.log.warning( + "ResumableError in sync loop, trying next loop", + exc_info=True) except Exception: self.stop() - log.warning("Unhandled except. in sync loop, stopping server", - exc_info=True) + self.log.warning( + "Unhandled except. in sync loop, stopping server", + exc_info=True) def stop(self): """Sets is_running flag to false, 'check_shutdown' shuts server down""" @@ -432,16 +435,17 @@ class SyncServerThread(threading.Thread): while self.is_running: if self.module.long_running_tasks: task = self.module.long_running_tasks.pop() - log.info("starting long running") + self.log.info("starting long running") await self.loop.run_in_executor(None, task["func"]) - log.info("finished long running") + self.log.info("finished long running") self.module.projects_processed.remove(task["project_name"]) await asyncio.sleep(0.5) tasks = [task for task in asyncio.all_tasks() if task is not asyncio.current_task()] list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks results = await asyncio.gather(*tasks, return_exceptions=True) - log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + self.log.debug( + f'Finished awaiting cancelled tasks, results: {results}...') await self.loop.shutdown_asyncgens() # to really make sure everything else has time to stop self.executor.shutdown(wait=True) @@ -454,29 +458,32 @@ class SyncServerThread(threading.Thread): def reset_timer(self): """Called when waiting for next loop should be skipped""" - log.debug("Resetting timer") + self.log.debug("Resetting timer") if self.timer: self.timer.cancel() self.timer = None def _working_sites(self, project_name): if self.module.is_project_paused(project_name): - log.debug("Both sites same, skipping") + self.log.debug("Both sites same, skipping") return None, None 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)) + self.log.debug("{}-{} sites same, skipping".format( + local_site, remote_site)) return None, None 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, - remote_site) + - "working properly") + self.log.debug( + "Some of the sites {} - {} is not working properly".format( + local_site, remote_site + ) + ) + return None, None return local_site, remote_site diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index c72b310907..3ef680c5a6 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -462,7 +462,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id (string): MongoDB objectId value site_name (string): 'gdrive', 'studio' etc. """ - log.info("Pausing SyncServer for {}".format(representation_id)) + self.log.info("Pausing SyncServer for {}".format(representation_id)) self._paused_representations.add(representation_id) self.reset_site_on_representation(project_name, representation_id, site_name=site_name, pause=True) @@ -479,7 +479,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id (string): MongoDB objectId value site_name (string): 'gdrive', 'studio' etc. """ - log.info("Unpausing SyncServer for {}".format(representation_id)) + self.log.info("Unpausing SyncServer for {}".format(representation_id)) try: self._paused_representations.remove(representation_id) except KeyError: @@ -518,7 +518,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Args: project_name (string): project_name name """ - log.info("Pausing SyncServer for {}".format(project_name)) + self.log.info("Pausing SyncServer for {}".format(project_name)) self._paused_projects.add(project_name) def unpause_project(self, project_name): @@ -530,7 +530,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Args: project_name (string): """ - log.info("Unpausing SyncServer for {}".format(project_name)) + self.log.info("Unpausing SyncServer for {}".format(project_name)) try: self._paused_projects.remove(project_name) except KeyError: @@ -558,14 +558,14 @@ class SyncServerModule(OpenPypeModule, ITrayModule): It won't check anything, not uploading/downloading... """ - log.info("Pausing SyncServer") + self.log.info("Pausing SyncServer") self._paused = True def unpause_server(self): """ Unpause server """ - log.info("Unpausing SyncServer") + self.log.info("Unpausing SyncServer") self._paused = False def is_paused(self): @@ -876,7 +876,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # val = val[platform.system().lower()] # except KeyError: # st = "{}'s field value {} should be".format(key, val) # noqa: E501 - # log.error(st + " multiplatform dict") + # self.log.error(st + " multiplatform dict") # # item["namespace"] = item["namespace"].replace('{site}', # site_name) @@ -1148,7 +1148,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if self.enabled: self.sync_server_thread.start() else: - log.info("No presets or active providers. " + + self.log.info("No presets or active providers. " + "Synchronization not possible.") def tray_exit(self): @@ -1166,12 +1166,12 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if not self.is_running: return try: - log.info("Stopping sync server server") + self.log.info("Stopping sync server server") self.sync_server_thread.is_running = False self.sync_server_thread.stop() - log.info("Sync server stopped") + self.log.info("Sync server stopped") except Exception: - log.warning( + self.log.warning( "Error has happened during Killing sync server", exc_info=True ) @@ -1256,7 +1256,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): sync_project_settings[project_name] = proj_settings if not sync_project_settings: - log.info("No enabled and configured projects for sync.") + self.log.info("No enabled and configured projects for sync.") return sync_project_settings def get_sync_project_setting(self, project_name, exclude_locals=False, @@ -1387,7 +1387,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Returns: (list) of dictionaries """ - log.debug("Check representations for : {}".format(project_name)) + self.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(project_name) @@ -1466,9 +1466,10 @@ class SyncServerModule(OpenPypeModule, ITrayModule): }}, {"$sort": {'priority': -1, '_id': 1}}, ] - log.debug("active_site:{} - remote_site:{}".format(active_site, - remote_site)) - log.debug("query: {}".format(aggr)) + self.log.debug("active_site:{} - remote_site:{}".format( + active_site, remote_site + )) + self.log.debug("query: {}".format(aggr)) representations = self.connection.aggregate(aggr) return representations @@ -1503,7 +1504,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if get_local_site_id() not in (local_site, remote_site): # don't do upload/download for studio sites - log.debug("No local site {} - {}".format(local_site, remote_site)) + self.log.debug("No local site {} - {}".format(local_site, remote_site)) return SyncStatus.DO_NOTHING _, remote_rec = self._get_site_rec(sites, remote_site) or {} @@ -1594,11 +1595,16 @@ class SyncServerModule(OpenPypeModule, ITrayModule): error_str = '' source_file = file.get("path", "") - log.debug("File for {} - {source_file} process {status} {error_str}". - format(representation_id, - status=status, - source_file=source_file, - error_str=error_str)) + self.log.debug( + ( + "File for {} - {source_file} process {status} {error_str}" + ).format( + representation_id, + status=status, + source_file=source_file, + error_str=error_str + ) + ) def _get_file_info(self, files, _id): """ @@ -1772,7 +1778,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): break if not found: msg = "Site {} not found".format(site_name) - log.info(msg) + self.log.info(msg) raise ValueError(msg) update = { @@ -1799,7 +1805,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): break if not found: msg = "Site {} not found".format(site_name) - log.info(msg) + self.log.info(msg) raise ValueError(msg) if pause: @@ -1834,7 +1840,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): reset_existing = False files = representation.get("files", []) if not files: - log.debug("No files for {}".format(representation_id)) + self.log.debug("No files for {}".format(representation_id)) return for repre_file in files: @@ -1851,7 +1857,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): reset_existing = True else: msg = "Site {} already present".format(site_name) - log.info(msg) + self.log.info(msg) raise SiteAlreadyPresentError(msg) if reset_existing: @@ -1951,16 +1957,19 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.widget = SyncServerWindow(self) no_errors = True except ValueError: - log.info("No system setting for sync. Not syncing.", exc_info=True) + self.log.info( + "No system setting for sync. Not syncing.", exc_info=True + ) except KeyError: - log.info(( + self.log.info(( "There are not set presets for SyncServer OR " "Credentials provided are invalid, " "no syncing possible"). format(str(self.sync_project_settings)), exc_info=True) except: - log.error("Uncaught exception durin start of SyncServer", - exc_info=True) + self.log.error( + "Uncaught exception durin start of SyncServer", + exc_info=True) self.enabled = no_errors self.widget.show() From c352ae5bcc713405794a3234ca38065624b9c119 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 15:57:52 +0200 Subject: [PATCH 0962/1030] add deprecation warning for PypeLogger and added docstring about deprecation --- openpype/lib/log.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index e77edea0e9..26dcd86eec 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -486,12 +486,18 @@ class Logger: class PypeLogger(Logger): + """Duplicate of 'Logger'. + + Deprecated: + Class will be removed after release version 3.16.* + """ + @classmethod def get_logger(cls, *args, **kwargs): logger = Logger.get_logger(*args, **kwargs) # TODO uncomment when replaced most of places - # logger.warning(( - # "'openpype.lib.PypeLogger' is deprecated class." - # " Please use 'openpype.lib.Logger' instead." - # )) + logger.warning(( + "'openpype.lib.PypeLogger' is deprecated class." + " Please use 'openpype.lib.Logger' instead." + )) return logger From 08efc477caa31e3ee064ce755ff7336322a9bc2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 16:21:04 +0200 Subject: [PATCH 0963/1030] small tweaks in usage of Logger --- openpype/hosts/celaction/api/cli.py | 2 +- openpype/hosts/fusion/api/pipeline.py | 4 ++-- .../fusion/utility_scripts/__OpenPype_Menu__.py | 5 ++--- openpype/hosts/hiero/api/events.py | 5 ++--- openpype/hosts/hiero/api/lib.py | 4 ++-- openpype/hosts/hiero/api/pipeline.py | 4 ++-- openpype/hosts/hiero/api/plugin.py | 3 ++- openpype/hosts/nuke/plugins/load/actions.py | 4 ++-- openpype/hosts/nuke/startup/clear_rendered.py | 5 +++-- openpype/hosts/nuke/startup/write_to_read.py | 4 ++-- .../modules/ftrack/ftrack_server/socket_thread.py | 6 +++--- openpype/modules/ftrack/lib/ftrack_base_handler.py | 4 ++-- .../modules/ftrack/scripts/sub_event_processor.py | 11 +++++------ .../modules/ftrack/scripts/sub_legacy_server.py | 4 ++-- openpype/modules/ftrack/scripts/sub_user_server.py | 5 ++--- openpype/modules/ftrack/tray/ftrack_tray.py | 5 +++-- .../sync_server/providers/abstract_provider.py | 4 ++-- openpype/modules/sync_server/utils.py | 6 ++++-- openpype/modules/timers_manager/rest_api.py | 14 +++++++++----- 19 files changed, 52 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index 8c7b3a2e74..eb91def090 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -14,7 +14,7 @@ from openpype.tools.utils import host_tools from openpype.pipeline import install_openpype_plugins -log = Logger().get_logger("Celaction_cli_publisher") +log = Logger.get_logger("Celaction_cli_publisher") publish_host = "celaction" diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 54002f9f51..54a6c94b60 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -8,7 +8,7 @@ import contextlib import pyblish.api -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, @@ -20,7 +20,7 @@ from openpype.pipeline import ( ) import openpype.hosts.fusion -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__)) PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py index de8fc4b3b4..870e74280a 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py @@ -1,14 +1,12 @@ import os import sys -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( install_host, registered_host, ) -log = Logger().get_logger(__name__) - def main(env): from openpype.hosts.fusion import api @@ -17,6 +15,7 @@ def main(env): # activate resolve from pype install_host(api) + log = Logger.get_logger(__name__) log.info(f"Registered host: {registered_host()}") menu.launch_openpype_menu() diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 59fd278a81..862a2607c1 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -1,7 +1,6 @@ import os import hiero.core.events -from openpype.api import Logger -from openpype.lib import register_event_callback +from openpype.lib import Logger, register_event_callback from .lib import ( sync_avalon_data_to_workfile, launch_workfiles_app, @@ -11,7 +10,7 @@ from .lib import ( from .tags import add_tags_to_workfile from .menu import update_menu_task_label -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def startupCompleted(event): diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 2f66f3ddd7..e288cea2b1 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -21,7 +21,7 @@ from openpype.client import ( ) from openpype.settings import get_anatomy_settings from openpype.pipeline import legacy_io, Anatomy -from openpype.api import Logger +from openpype.lib import Logger from . import tags try: @@ -34,7 +34,7 @@ except ImportError: # from opentimelineio import opentime # from pprint import pformat -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) self = sys.modules[__name__] self._has_been_setup = False diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index b243a38b06..dacfd338bb 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -6,7 +6,7 @@ import contextlib from collections import OrderedDict from pyblish import api as pyblish -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( schema, register_creator_plugin_path, @@ -18,7 +18,7 @@ from openpype.pipeline import ( from openpype.tools.utils import host_tools from . import lib, menu, events -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) # plugin paths API_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 28a9dfb492..77fedbbbdc 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -9,11 +9,12 @@ from Qt import QtWidgets, QtCore import qargparse import openpype.api as openpype +from openpype.lib import Logger from openpype.pipeline import LoaderPlugin, LegacyCreator from openpype.pipeline.context_tools import get_current_project_asset from . import lib -log = openpype.Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def load_stylesheet(): diff --git a/openpype/hosts/nuke/plugins/load/actions.py b/openpype/hosts/nuke/plugins/load/actions.py index d364a4f3a1..69f56c7305 100644 --- a/openpype/hosts/nuke/plugins/load/actions.py +++ b/openpype/hosts/nuke/plugins/load/actions.py @@ -2,10 +2,10 @@ """ -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import load -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) class SetFrameRangeLoader(load.LoaderPlugin): diff --git a/openpype/hosts/nuke/startup/clear_rendered.py b/openpype/hosts/nuke/startup/clear_rendered.py index cf1d8ce170..744af71034 100644 --- a/openpype/hosts/nuke/startup/clear_rendered.py +++ b/openpype/hosts/nuke/startup/clear_rendered.py @@ -1,10 +1,11 @@ import os -from openpype.api import Logger -log = Logger().get_logger(__name__) +from openpype.lib import Logger def clear_rendered(dir_path): + log = Logger.get_logger(__name__) + for _f in os.listdir(dir_path): _f_path = os.path.join(dir_path, _f) log.info("Removing: `{}`".format(_f_path)) diff --git a/openpype/hosts/nuke/startup/write_to_read.py b/openpype/hosts/nuke/startup/write_to_read.py index f5cf66b357..b7add40f47 100644 --- a/openpype/hosts/nuke/startup/write_to_read.py +++ b/openpype/hosts/nuke/startup/write_to_read.py @@ -2,8 +2,8 @@ import re import os import glob import nuke -from openpype.api import Logger -log = Logger().get_logger(__name__) +from openpype.lib import Logger +log = Logger.get_logger(__name__) SINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v', 'm2v'] diff --git a/openpype/modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/ftrack/ftrack_server/socket_thread.py index f49ca5557e..3ef55f8daa 100644 --- a/openpype/modules/ftrack/ftrack_server/socket_thread.py +++ b/openpype/modules/ftrack/ftrack_server/socket_thread.py @@ -5,8 +5,8 @@ import socket import threading import traceback import subprocess -from openpype.api import Logger -from openpype.lib import get_openpype_execute_args + +from openpype.lib import get_openpype_execute_args, Logger class SocketThread(threading.Thread): @@ -16,7 +16,7 @@ class SocketThread(threading.Thread): def __init__(self, name, port, filepath, additional_args=[]): super(SocketThread, self).__init__() - self.log = Logger().get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.setName(name) self.name = name self.port = port diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index c0fad6aadc..c0b03f8a41 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -6,7 +6,7 @@ import uuid import datetime import traceback import time -from openpype.api import Logger +from openpype.lib import Logger from openpype.settings import get_project_settings import ftrack_api @@ -52,7 +52,7 @@ class BaseHandler(object): def __init__(self, session): '''Expects a ftrack_api.Session instance''' - self.log = Logger().get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) if not( isinstance(session, ftrack_api.session.Session) or isinstance(session, ftrack_server.lib.SocketSession) diff --git a/openpype/modules/ftrack/scripts/sub_event_processor.py b/openpype/modules/ftrack/scripts/sub_event_processor.py index d1e2e3aaeb..a5ce0511b8 100644 --- a/openpype/modules/ftrack/scripts/sub_event_processor.py +++ b/openpype/modules/ftrack/scripts/sub_event_processor.py @@ -4,6 +4,8 @@ import signal import socket import datetime +import ftrack_api + from openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer from openpype_modules.ftrack.ftrack_server.lib import ( SocketSession, @@ -12,17 +14,12 @@ from openpype_modules.ftrack.ftrack_server.lib import ( ) from openpype.modules import ModulesManager -from openpype.api import Logger from openpype.lib import ( + Logger, get_openpype_version, get_build_version ) - -import ftrack_api - -log = Logger().get_logger("Event processor") - subprocess_started = datetime.datetime.now() @@ -68,6 +65,8 @@ def register(session): def main(args): + log = Logger.get_logger("Event processor") + port = int(args[-1]) # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/openpype/modules/ftrack/scripts/sub_legacy_server.py b/openpype/modules/ftrack/scripts/sub_legacy_server.py index e3a623c376..1f0fc1b369 100644 --- a/openpype/modules/ftrack/scripts/sub_legacy_server.py +++ b/openpype/modules/ftrack/scripts/sub_legacy_server.py @@ -5,11 +5,11 @@ import signal import threading import ftrack_api -from openpype.api import Logger +from openpype.lib import Logger from openpype.modules import ModulesManager from openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer -log = Logger().get_logger("Event Server Legacy") +log = Logger.get_logger("Event Server Legacy") class TimerChecker(threading.Thread): diff --git a/openpype/modules/ftrack/scripts/sub_user_server.py b/openpype/modules/ftrack/scripts/sub_user_server.py index a3701a0950..930a2d51e2 100644 --- a/openpype/modules/ftrack/scripts/sub_user_server.py +++ b/openpype/modules/ftrack/scripts/sub_user_server.py @@ -2,6 +2,7 @@ import sys import signal import socket +from openpype.lib import Logger from openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer from openpype_modules.ftrack.ftrack_server.lib import ( SocketSession, @@ -9,9 +10,7 @@ from openpype_modules.ftrack.ftrack_server.lib import ( ) from openpype.modules import ModulesManager -from openpype.api import Logger - -log = Logger().get_logger("FtrackUserServer") +log = Logger.get_logger("FtrackUserServer") def main(args): diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 2919ae22fb..501d837a4c 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -12,10 +12,11 @@ from ..lib import credentials from ..ftrack_module import FTRACK_MODULE_DIR from . import login_dialog -from openpype.api import Logger, resources +from openpype import resources +from openpype.lib import Logger -log = Logger().get_logger("FtrackModule") +log = Logger.get_logger("FtrackModule") class FtrackTrayWrapper: diff --git a/openpype/modules/sync_server/providers/abstract_provider.py b/openpype/modules/sync_server/providers/abstract_provider.py index 8c2fe1cad9..9c808dc80e 100644 --- a/openpype/modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/sync_server/providers/abstract_provider.py @@ -1,8 +1,8 @@ import abc import six -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") @six.add_metaclass(abc.ABCMeta) diff --git a/openpype/modules/sync_server/utils.py b/openpype/modules/sync_server/utils.py index 03f362202f..4caa01e9d7 100644 --- a/openpype/modules/sync_server/utils.py +++ b/openpype/modules/sync_server/utils.py @@ -1,6 +1,8 @@ import time -from openpype.api import Logger -log = Logger().get_logger("SyncServer") + +from openpype.lib import Logger + +log = Logger.get_logger("SyncServer") class ResumableError(Exception): diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index f16cb316c3..9bde19aec9 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -1,9 +1,7 @@ import json from aiohttp.web_response import Response -from openpype.api import Logger - -log = Logger().get_logger("Event processor") +from openpype.lib import Logger class TimersManagerModuleRestApi: @@ -12,6 +10,7 @@ class TimersManagerModuleRestApi: happens in Workfile app. """ def __init__(self, user_module, server_manager): + self.log = None self.module = user_module self.server_manager = server_manager @@ -19,6 +18,11 @@ class TimersManagerModuleRestApi: self.register() + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__ckass__.__name__) + return self._log + def register(self): self.server_manager.add_route( "POST", @@ -47,7 +51,7 @@ class TimersManagerModuleRestApi: "Payload must contain fields 'project_name," " 'asset_name' and 'task_name'" ) - log.error(msg) + self.log.error(msg) return Response(status=400, message=msg) self.module.stop_timers() @@ -73,7 +77,7 @@ class TimersManagerModuleRestApi: "Payload must contain fields 'project_name, 'asset_name'," " 'task_name'" ) - log.warning(message) + self.log.warning(message) return Response(text=message, status=404) time = self.module.get_task_time(project_name, asset_name, task_name) From 840fbaa38086b110f9ad72584aa3e37fa1bf1178 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 16:21:17 +0200 Subject: [PATCH 0964/1030] cleanup imports in modules --- openpype/modules/base.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 7fc848af2d..8ac4e7ddac 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -13,7 +13,6 @@ from uuid import uuid4 from abc import ABCMeta, abstractmethod import six -import openpype from openpype.settings import ( get_system_settings, SYSTEM_SETTINGS_KEY, @@ -26,7 +25,12 @@ from openpype.settings.lib import ( get_studio_system_settings_overrides, load_json_file ) -from openpype.lib import Logger + +from openpype.lib import ( + Logger, + import_filepath, + import_module_from_dirpath +) # Files that will be always ignored on modules import IGNORED_FILENAMES = ( @@ -278,12 +282,6 @@ def load_modules(force=False): def _load_modules(): - # Import helper functions from lib - from openpype.lib import ( - import_filepath, - import_module_from_dirpath - ) - # Key under which will be modules imported in `sys.modules` modules_key = "openpype_modules" From bf8e2207e07dd18f1fc8e2d8026ec719886666ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 16:58:38 +0200 Subject: [PATCH 0965/1030] fix property --- openpype/modules/timers_manager/rest_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 9bde19aec9..6686407350 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -18,6 +18,7 @@ class TimersManagerModuleRestApi: self.register() + @property def log(self): if self._log is None: self._log = Logger.get_logger(self.__ckass__.__name__) From 310e9bf50f59a3f39adf54d18047eb7a422c5843 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 17:19:01 +0200 Subject: [PATCH 0966/1030] fix line lengths --- openpype/modules/sync_server/sync_server_module.py | 4 +++- openpype/modules/webserver/server.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 3ef680c5a6..634b68c55f 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1504,7 +1504,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if get_local_site_id() not in (local_site, remote_site): # don't do upload/download for studio sites - self.log.debug("No local site {} - {}".format(local_site, remote_site)) + self.log.debug( + "No local site {} - {}".format(local_site, remote_site) + ) return SyncStatus.DO_NOTHING _, remote_rec = self._get_site_rec(sites, remote_site) or {} diff --git a/openpype/modules/webserver/server.py b/openpype/modules/webserver/server.py index 44b14acbb6..120925a362 100644 --- a/openpype/modules/webserver/server.py +++ b/openpype/modules/webserver/server.py @@ -171,7 +171,9 @@ class WebServerThread(threading.Thread): ] list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks results = await asyncio.gather(*tasks, return_exceptions=True) - self.log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + self.log.debug( + f'Finished awaiting cancelled tasks, results: {results}...' + ) await self.loop.shutdown_asyncgens() # to really make sure everything else has time to stop await asyncio.sleep(0.07) From 8539c03d72c246c125334f059522b14073cd6ed8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 17:53:21 +0200 Subject: [PATCH 0967/1030] remove getattrs on instance and context --- openpype/tools/publisher/widgets/publish_widget.py | 2 -- openpype/tools/pyblish_pype/model.py | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/publish_widget.py b/openpype/tools/publisher/widgets/publish_widget.py index 80d0265dd3..b32b5381d1 100644 --- a/openpype/tools/publisher/widgets/publish_widget.py +++ b/openpype/tools/publisher/widgets/publish_widget.py @@ -335,14 +335,12 @@ class PublishFrame(QtWidgets.QFrame): if instance is None: new_name = ( context.data.get("label") - or getattr(context, "label", None) or context.data.get("name") or "Context" ) else: new_name = ( instance.data.get("label") - or getattr(instance, "label", None) or instance.data["name"] ) diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 309126a884..1479d91bb5 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -613,10 +613,7 @@ class InstanceItem(QtGui.QStandardItem): if role == QtCore.Qt.DisplayRole: label = None if settings.UseLabel: - label = ( - self.instance.data.get("label") - or getattr(self.instance, "label", None) - ) + label = self.instance.data.get("label") if not label: if self.is_context: From 6a1979b6b2b852be227ff0b254cc27797ba8b3f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:09:28 +0200 Subject: [PATCH 0968/1030] created aftereffects module --- openpype/hosts/aftereffects/__init__.py | 15 ++++++--------- openpype/hosts/aftereffects/module.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/aftereffects/module.py diff --git a/openpype/hosts/aftereffects/__init__.py b/openpype/hosts/aftereffects/__init__.py index deae48d122..c9ad6aaeeb 100644 --- a/openpype/hosts/aftereffects/__init__.py +++ b/openpype/hosts/aftereffects/__init__.py @@ -1,9 +1,6 @@ -def add_implementation_envs(env, _app): - """Modify environments to contain all required for implementation.""" - defaults = { - "OPENPYPE_LOG_NO_COLORS": "True", - "WEBSOCKET_URL": "ws://localhost:8097/ws/" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value +from .module import AfterEffectsModule + + +__all__ = ( + "AfterEffectsModule", +) diff --git a/openpype/hosts/aftereffects/module.py b/openpype/hosts/aftereffects/module.py new file mode 100644 index 0000000000..33e42b451b --- /dev/null +++ b/openpype/hosts/aftereffects/module.py @@ -0,0 +1,24 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + + +class AfterEffectsModule(OpenPypeModule, IHostModule): + name = "aftereffects" + host_name = "aftereffects" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + """Modify environments to contain all required for implementation.""" + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True", + "WEBSOCKET_URL": "ws://localhost:8097/ws/" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + def get_workfile_extensions(self): + return [".aep"] From 7e1ba966ce2188e5a492255bae7f9216eaac8833 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:09:38 +0200 Subject: [PATCH 0969/1030] workio is not using HOST_WORKFILE_EXTENSIONS --- openpype/hosts/aftereffects/api/workio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/api/workio.py b/openpype/hosts/aftereffects/api/workio.py index d6c732285a..18b40af5dc 100644 --- a/openpype/hosts/aftereffects/api/workio.py +++ b/openpype/hosts/aftereffects/api/workio.py @@ -1,12 +1,11 @@ """Host API required Work Files tool""" import os -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS from .launch_logic import get_stub def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["aftereffects"] + return [".aep"] def has_unsaved_changes(): From 18f22f4d0fa522c50bba4797471ebd5ab4446e43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:12:49 +0200 Subject: [PATCH 0970/1030] removed unused import --- openpype/hosts/aftereffects/module.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/aftereffects/module.py b/openpype/hosts/aftereffects/module.py index 33e42b451b..93d575c186 100644 --- a/openpype/hosts/aftereffects/module.py +++ b/openpype/hosts/aftereffects/module.py @@ -1,4 +1,3 @@ -import os from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostModule From 6282719d9a7d0a773ebb075a801d496083b908be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:16:25 +0200 Subject: [PATCH 0971/1030] added blender module --- openpype/hosts/blender/__init__.py | 54 ++-------------------- openpype/hosts/blender/module.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 openpype/hosts/blender/module.py diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 0f27882c7e..58d7ac656f 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -1,52 +1,6 @@ -import os +from .module import BlenderModule -def add_implementation_envs(env, _app): - """Modify environments to contain all required for implementation.""" - # Prepare path to implementation script - implementation_user_script_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "blender_addon" - ) - - # Add blender implementation script path to PYTHONPATH - python_path = env.get("PYTHONPATH") or "" - python_path_parts = [ - path - for path in python_path.split(os.pathsep) - if path - ] - python_path_parts.insert(0, implementation_user_script_path) - env["PYTHONPATH"] = os.pathsep.join(python_path_parts) - - # Modify Blender user scripts path - previous_user_scripts = set() - # Implementation path is added to set for easier paths check inside loops - # - will be removed at the end - previous_user_scripts.add(implementation_user_script_path) - - openpype_blender_user_scripts = ( - env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or "" - ) - for path in openpype_blender_user_scripts.split(os.pathsep): - if path: - previous_user_scripts.add(os.path.normpath(path)) - - blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" - for path in blender_user_scripts.split(os.pathsep): - if path: - previous_user_scripts.add(os.path.normpath(path)) - - # Remove implementation path from user script paths as is set to - # `BLENDER_USER_SCRIPTS` - previous_user_scripts.remove(implementation_user_script_path) - env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path - - # Set custom user scripts env - env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join( - previous_user_scripts - ) - - # Define Qt binding if not defined - if not env.get("QT_PREFERRED_BINDING"): - env["QT_PREFERRED_BINDING"] = "PySide2" +__all__ = ( + "BlenderModule", +) diff --git a/openpype/hosts/blender/module.py b/openpype/hosts/blender/module.py new file mode 100644 index 0000000000..73865184c0 --- /dev/null +++ b/openpype/hosts/blender/module.py @@ -0,0 +1,73 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class BlenderModule(OpenPypeModule, IHostModule): + name = "nuke" + host_name = "nuke" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + """Modify environments to contain all required for implementation.""" + # Prepare path to implementation script + implementation_user_script_path = os.path.join( + BLENDER_ROOT_DIR, + "blender_addon" + ) + + # Add blender implementation script path to PYTHONPATH + python_path = env.get("PYTHONPATH") or "" + python_path_parts = [ + path + for path in python_path.split(os.pathsep) + if path + ] + python_path_parts.insert(0, implementation_user_script_path) + env["PYTHONPATH"] = os.pathsep.join(python_path_parts) + + # Modify Blender user scripts path + previous_user_scripts = set() + # Implementation path is added to set for easier paths check inside loops + # - will be removed at the end + previous_user_scripts.add(implementation_user_script_path) + + openpype_blender_user_scripts = ( + env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or "" + ) + for path in openpype_blender_user_scripts.split(os.pathsep): + if path: + previous_user_scripts.add(os.path.normpath(path)) + + blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" + for path in blender_user_scripts.split(os.pathsep): + if path: + previous_user_scripts.add(os.path.normpath(path)) + + # Remove implementation path from user script paths as is set to + # `BLENDER_USER_SCRIPTS` + previous_user_scripts.remove(implementation_user_script_path) + env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path + + # Set custom user scripts env + env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join( + previous_user_scripts + ) + + # Define Qt binding if not defined + if not env.get("QT_PREFERRED_BINDING"): + env["QT_PREFERRED_BINDING"] = "PySide2" + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(BLENDER_ROOT_DIR, "hooks") + ] + + def get_workfile_extensions(self): + return [".blend"] From f36b8f49a2202b10e5f17fc4ecb26cfa1ab6e428 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:16:39 +0200 Subject: [PATCH 0972/1030] don't use HOST_WORKFILE_EXTENSIONS in blender workio --- openpype/hosts/blender/api/workio.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/workio.py b/openpype/hosts/blender/api/workio.py index 5eb9f82999..a8f6193abc 100644 --- a/openpype/hosts/blender/api/workio.py +++ b/openpype/hosts/blender/api/workio.py @@ -5,8 +5,6 @@ from typing import List, Optional import bpy -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - class OpenFileCacher: """Store information about opening file. @@ -78,7 +76,7 @@ def has_unsaved_changes() -> bool: def file_extensions() -> List[str]: """Return the supported file extensions for Blender scene files.""" - return HOST_WORKFILE_EXTENSIONS["blender"] + return [".blend"] def work_root(session: dict) -> str: From 78d107f485894726c87b502d2e88eb2ecf0d8e38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 Aug 2022 18:19:08 +0200 Subject: [PATCH 0973/1030] hound fix --- openpype/hosts/blender/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/module.py b/openpype/hosts/blender/module.py index 73865184c0..0cb2f5c44b 100644 --- a/openpype/hosts/blender/module.py +++ b/openpype/hosts/blender/module.py @@ -32,8 +32,8 @@ class BlenderModule(OpenPypeModule, IHostModule): # Modify Blender user scripts path previous_user_scripts = set() - # Implementation path is added to set for easier paths check inside loops - # - will be removed at the end + # Implementation path is added to set for easier paths check inside + # loops - will be removed at the end previous_user_scripts.add(implementation_user_script_path) openpype_blender_user_scripts = ( From 28bdf2f2caa5ce4c80ffa9f3bb2a470cb9769ba7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 10:55:57 +0200 Subject: [PATCH 0974/1030] fix host name --- openpype/hosts/blender/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/module.py b/openpype/hosts/blender/module.py index 0cb2f5c44b..d6ff3b111c 100644 --- a/openpype/hosts/blender/module.py +++ b/openpype/hosts/blender/module.py @@ -6,8 +6,8 @@ BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) class BlenderModule(OpenPypeModule, IHostModule): - name = "nuke" - host_name = "nuke" + name = "blender" + host_name = "blender" def initialize(self, module_settings): self.enabled = True From 2f2cbd41465c882519ef6831e5ad81373f7a3615 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Aug 2022 11:24:53 +0200 Subject: [PATCH 0975/1030] OP-3722 - added check for empty context to basic publish --- openpype/pype_commands.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index b266479bb1..391244d185 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -230,7 +230,6 @@ class PypeCommands: 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") if not task_data["context"]: msg = "Batch manifest must contain context data" @@ -351,6 +350,12 @@ class PypeCommands: dbcon = get_webpublish_conn() _id = start_webpublish_log(dbcon, batch_id, user_email) + task_data = get_task_data(batch_path) + if not task_data["context"]: + msg = "Batch manifest must contain context data" + msg += "Create new batch and set context properly." + fail_batch(_id, dbcon, msg) + publish_and_log(dbcon, _id, log, batch_id=batch_id) log.info("Publish finished.") From 382ec674a8d044f2f4f1650773b78192062618d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:02:22 +0200 Subject: [PATCH 0976/1030] copied 'get_unique_layer_name' and 'get_background_layers' into ae lib --- openpype/hosts/aftereffects/api/lib.py | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index ce4cbf09af..dc16aaeac5 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -1,5 +1,7 @@ import os import sys +import re +import json import contextlib import traceback import logging @@ -68,3 +70,57 @@ def get_extension_manifest_path(): "CSXS", "manifest.xml" ) + + +def get_unique_layer_name(layers, name): + """ + Gets all layer names and if 'name' is present in them, increases + suffix by 1 (eg. creates unique layer name - for Loader) + Args: + layers (list): of strings, names only + name (string): checked value + + Returns: + (string): name_00X (without version) + """ + names = {} + for layer in layers: + layer_name = re.sub(r'_\d{3}$', '', layer) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1) + + +def get_background_layers(file_url): + """ + Pulls file name from background json file, enrich with folder url for + AE to be able import files. + + Order is important, follows order in json. + + Args: + file_url (str): abs url of background json + + Returns: + (list): of abs paths to images + """ + with open(file_url) as json_file: + data = json.load(json_file) + + layers = list() + bg_folder = os.path.dirname(file_url) + for child in data['children']: + if child.get("filename"): + layers.append(os.path.join(bg_folder, child.get("filename")). + replace("\\", "/")) + else: + for layer in child['children']: + if layer.get("filename"): + layers.append(os.path.join(bg_folder, + layer.get("filename")). + replace("\\", "/")) + return layers From 5d83a428d9f7fd11fdf8002dc231c3577b7de7a3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:02:37 +0200 Subject: [PATCH 0977/1030] change imports to new location in loaders --- .../hosts/aftereffects/plugins/load/load_background.py | 8 ++++---- openpype/hosts/aftereffects/plugins/load/load_file.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/load/load_background.py b/openpype/hosts/aftereffects/plugins/load/load_background.py index d346df504a..260e780be0 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_background.py +++ b/openpype/hosts/aftereffects/plugins/load/load_background.py @@ -1,14 +1,14 @@ import re -from openpype.lib import ( - get_background_layers, - get_unique_layer_name -) from openpype.pipeline import get_representation_path from openpype.hosts.aftereffects.api import ( AfterEffectsLoader, containerise ) +from openpype.hosts.aftereffects.api.lib import ( + get_background_layers, + get_unique_layer_name, +) class BackgroundLoader(AfterEffectsLoader): diff --git a/openpype/hosts/aftereffects/plugins/load/load_file.py b/openpype/hosts/aftereffects/plugins/load/load_file.py index 6ab69c6bfa..2ddc9825e5 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_file.py +++ b/openpype/hosts/aftereffects/plugins/load/load_file.py @@ -1,12 +1,11 @@ import re -from openpype import lib - from openpype.pipeline import get_representation_path from openpype.hosts.aftereffects.api import ( AfterEffectsLoader, containerise ) +from openpype.hosts.aftereffects.api.lib import get_unique_layer_name class FileLoader(AfterEffectsLoader): @@ -28,7 +27,7 @@ class FileLoader(AfterEffectsLoader): stub = self.get_stub() layers = stub.get_items(comps=True, folders=True, footages=True) existing_layers = [layer.name for layer in layers] - comp_name = lib.get_unique_layer_name( + comp_name = get_unique_layer_name( existing_layers, "{}_{}".format(context["asset"]["name"], name)) import_options = {} @@ -87,7 +86,7 @@ class FileLoader(AfterEffectsLoader): if namespace_from_container != layer_name: layers = stub.get_items(comps=True) existing_layers = [layer.name for layer in layers] - layer_name = lib.get_unique_layer_name( + layer_name = get_unique_layer_name( existing_layers, "{}_{}".format(context["asset"], context["subset"])) else: # switching version - keep same name From d263a8ef9df1aa496a44c08d6c0b7e69810153d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:02:51 +0200 Subject: [PATCH 0978/1030] remove functions from openpype lib --- openpype/lib/__init__.py | 4 --- openpype/lib/plugin_tools.py | 54 ------------------------------------ 2 files changed, 58 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 3d3e425a86..adb857a056 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -189,8 +189,6 @@ from .plugin_tools import ( filter_pyblish_plugins, set_plugin_attributes_from_settings, source_hash, - get_unique_layer_name, - get_background_layers, ) from .path_tools import ( @@ -354,8 +352,6 @@ __all__ = [ "filter_pyblish_plugins", "set_plugin_attributes_from_settings", "source_hash", - "get_unique_layer_name", - "get_background_layers", "create_hard_link", "version_up", diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 060db94ae0..9080918dfa 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -375,60 +375,6 @@ def source_hash(filepath, *args): return "|".join([file_name, time, size] + list(args)).replace(".", ",") -def get_unique_layer_name(layers, name): - """ - Gets all layer names and if 'name' is present in them, increases - suffix by 1 (eg. creates unique layer name - for Loader) - Args: - layers (list): of strings, names only - name (string): checked value - - Returns: - (string): name_00X (without version) - """ - names = {} - for layer in layers: - layer_name = re.sub(r'_\d{3}$', '', layer) - if layer_name in names.keys(): - names[layer_name] = names[layer_name] + 1 - else: - names[layer_name] = 1 - occurrences = names.get(name, 0) - - return "{}_{:0>3d}".format(name, occurrences + 1) - - -def get_background_layers(file_url): - """ - Pulls file name from background json file, enrich with folder url for - AE to be able import files. - - Order is important, follows order in json. - - Args: - file_url (str): abs url of background json - - Returns: - (list): of abs paths to images - """ - with open(file_url) as json_file: - data = json.load(json_file) - - layers = list() - bg_folder = os.path.dirname(file_url) - for child in data['children']: - if child.get("filename"): - layers.append(os.path.join(bg_folder, child.get("filename")). - replace("\\", "/")) - else: - for layer in child['children']: - if layer.get("filename"): - layers.append(os.path.join(bg_folder, - layer.get("filename")). - replace("\\", "/")) - return layers - - def parse_json(path): """Parses json file at 'path' location From 5372c016eadfe2dd09cfc1803a6984cfca24d61b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:19:47 +0200 Subject: [PATCH 0979/1030] moved 'OpenPypeInterface' into interfaces.py --- openpype/modules/__init__.py | 2 -- openpype/modules/base.py | 42 +++++++--------------------------- openpype/modules/interfaces.py | 29 +++++++++++++++++++++-- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 68b5f6c247..02e7dc13ab 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -2,7 +2,6 @@ from .base import ( OpenPypeModule, OpenPypeAddOn, - OpenPypeInterface, load_modules, @@ -20,7 +19,6 @@ from .base import ( __all__ = ( "OpenPypeModule", "OpenPypeAddOn", - "OpenPypeInterface", "load_modules", diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1316d7f734..1b8cf5d769 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -28,6 +28,14 @@ from openpype.settings.lib import ( ) from openpype.lib import PypeLogger +from .interfaces import ( + OpenPypeInterface, + IPluginPaths, + IHostModule, + ITrayModule, + ITrayService +) + # Files that will be always ignored on modules import IGNORED_FILENAMES = ( "__pycache__", @@ -391,29 +399,7 @@ def _load_modules(): log.error(msg, exc_info=True) -class _OpenPypeInterfaceMeta(ABCMeta): - """OpenPypeInterface meta class to print proper string.""" - def __str__(self): - return "<'OpenPypeInterface.{}'>".format(self.__name__) - - def __repr__(self): - return str(self) - - -@six.add_metaclass(_OpenPypeInterfaceMeta) -class OpenPypeInterface: - """Base class of Interface that can be used as Mixin with abstract parts. - - This is way how OpenPype module or addon can tell that has implementation - for specific part or for other module/addon. - - Child classes of OpenPypeInterface may be used as mixin in different - OpenPype modules which means they have to have implemented methods defined - in the interface. By default interface does not have any abstract parts. - """ - - pass @six.add_metaclass(ABCMeta) @@ -749,8 +735,6 @@ class ModulesManager: and "actions" each containing list of paths. """ # Output structure - from openpype_interfaces import IPluginPaths - output = { "publish": [], "create": [], @@ -807,8 +791,6 @@ class ModulesManager: list: List of creator plugin paths. """ # Output structure - from openpype_interfaces import IPluginPaths - output = [] for module in self.get_enabled_modules(): # Skip module that do not inherit from `IPluginPaths` @@ -897,8 +879,6 @@ class ModulesManager: host name set to passed 'host_name'. """ - from openpype_interfaces import IHostModule - for module in self.get_enabled_modules(): if ( isinstance(module, IHostModule) @@ -915,8 +895,6 @@ class ModulesManager: inheriting 'IHostModule'. """ - from openpype_interfaces import IHostModule - host_names = { module.host_name for module in self.get_enabled_modules() @@ -1098,8 +1076,6 @@ class TrayModulesManager(ModulesManager): self.tray_menu(tray_menu) def get_enabled_tray_modules(self): - from openpype_interfaces import ITrayModule - output = [] for module in self.modules: if module.enabled and isinstance(module, ITrayModule): @@ -1175,8 +1151,6 @@ class TrayModulesManager(ModulesManager): self._report["Tray menu"] = report def start_modules(self): - from openpype_interfaces import ITrayService - report = {} time_start = time.time() prev_start_time = time_start diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 14f49204ee..8221db4d05 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,8 +1,33 @@ -from abc import abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod, abstractproperty + +import six from openpype import resources -from openpype.modules import OpenPypeInterface + +class _OpenPypeInterfaceMeta(ABCMeta): + """OpenPypeInterface meta class to print proper string.""" + + def __str__(self): + return "<'OpenPypeInterface.{}'>".format(self.__name__) + + def __repr__(self): + return str(self) + + +@six.add_metaclass(_OpenPypeInterfaceMeta) +class OpenPypeInterface: + """Base class of Interface that can be used as Mixin with abstract parts. + + This is way how OpenPype module or addon can tell OpenPype that contain + implementation for specific functionality. + + Child classes of OpenPypeInterface may be used as mixin in different + OpenPype modules which means they have to have implemented methods defined + in the interface. By default interface does not have any abstract parts. + """ + + pass class IPluginPaths(OpenPypeInterface): From 8cc6086e92a6c5135428898717e6d9057f567e8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:22:19 +0200 Subject: [PATCH 0980/1030] removed usage of 'ILaunchHookPaths' --- openpype/modules/ftrack/ftrack_module.py | 5 ++--- openpype/modules/shotgrid/shotgrid_module.py | 5 +---- openpype/modules/slack/slack_module.py | 10 ++++------ openpype/modules/timers_manager/timers_manager.py | 11 ++++------- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index f99e189082..cb4f204523 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -9,7 +9,6 @@ from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayModule, IPluginPaths, - ILaunchHookPaths, ISettingsChangeListener ) from openpype.settings import SaveWarningExc @@ -21,7 +20,6 @@ class FtrackModule( OpenPypeModule, ITrayModule, IPluginPaths, - ILaunchHookPaths, ISettingsChangeListener ): name = "ftrack" @@ -85,7 +83,8 @@ class FtrackModule( } def get_launch_hook_paths(self): - """Implementation of `ILaunchHookPaths`.""" + """Implementation for applications launch hooks.""" + return os.path.join(FTRACK_MODULE_DIR, "launch_hooks") def modify_application_launch_arguments(self, application, env): diff --git a/openpype/modules/shotgrid/shotgrid_module.py b/openpype/modules/shotgrid/shotgrid_module.py index 5644f0c35f..281c6fdcad 100644 --- a/openpype/modules/shotgrid/shotgrid_module.py +++ b/openpype/modules/shotgrid/shotgrid_module.py @@ -3,7 +3,6 @@ import os from openpype_interfaces import ( ITrayModule, IPluginPaths, - ILaunchHookPaths, ) from openpype.modules import OpenPypeModule @@ -11,9 +10,7 @@ from openpype.modules import OpenPypeModule SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -class ShotgridModule( - OpenPypeModule, ITrayModule, IPluginPaths, ILaunchHookPaths -): +class ShotgridModule(OpenPypeModule, ITrayModule, IPluginPaths): leecher_manager_url = None name = "shotgrid" enabled = False diff --git a/openpype/modules/slack/slack_module.py b/openpype/modules/slack/slack_module.py index 9b2976d766..499c1c19ce 100644 --- a/openpype/modules/slack/slack_module.py +++ b/openpype/modules/slack/slack_module.py @@ -1,14 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - IPluginPaths, - ILaunchHookPaths -) +from openpype.modules.interfaces import IPluginPaths SLACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths): +class SlackIntegrationModule(OpenPypeModule, IPluginPaths): """Allows sending notification to Slack channels during publishing.""" name = "slack" @@ -18,7 +15,8 @@ class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths): self.enabled = slack_settings["enabled"] def get_launch_hook_paths(self): - """Implementation of `ILaunchHookPaths`.""" + """Implementation for applications launch hooks.""" + return os.path.join(SLACK_MODULE_DIR, "launch_hooks") def get_plugin_paths(self): diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 93332ace4f..c168e9534d 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -6,7 +6,6 @@ from openpype.client import get_asset_by_name from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayService, - ILaunchHookPaths, IPluginPaths ) from openpype.lib.events import register_event_callback @@ -79,7 +78,6 @@ class ExampleTimersManagerConnector: class TimersManager( OpenPypeModule, ITrayService, - ILaunchHookPaths, IPluginPaths ): """ Handles about Timers. @@ -185,12 +183,11 @@ class TimersManager( ) def get_launch_hook_paths(self): - """Implementation of `ILaunchHookPaths`.""" + """Implementation for applications launch hooks.""" - return os.path.join( - TIMER_MODULE_DIR, - "launch_hooks" - ) + return [ + os.path.join(TIMER_MODULE_DIR, "launch_hooks") + ] def get_plugin_paths(self): """Implementation of `IPluginPaths`.""" From 8acb96c572a58c779b544dac8b79aab9d71b6663 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:27:02 +0200 Subject: [PATCH 0981/1030] added deprecation warning to 'ILaunchHookPaths' --- openpype/modules/interfaces.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 8221db4d05..13655773dd 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -81,6 +81,13 @@ class ILaunchHookPaths(OpenPypeInterface): Expected result is list of paths. ["path/to/launch_hooks_dir"] + + Deprecated: + This interface is not needed since OpenPype 3.14.*. Addon just have to + implement 'get_launch_hook_paths' which can expect Application object + or nothing as argument. + + Interface class will be removed after 3.16.*. """ @abstractmethod From 7356fc666d7aa4cca7251849f4eaf4d91dfc0e75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 12:31:47 +0200 Subject: [PATCH 0982/1030] moved collection of launch hooks from modules into applications logic --- openpype/lib/applications.py | 61 +++++++++++++++++++++++++++++++++-- openpype/modules/base.py | 62 ------------------------------------ 2 files changed, 58 insertions(+), 65 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 074e815160..b389bc2539 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -950,6 +950,63 @@ class ApplicationLaunchContext: ) self.kwargs["env"] = value + def _collect_addons_launch_hook_paths(self): + """Helper to collect application launch hooks from addons. + + Module have to have implemented 'get_launch_hook_paths' method which + can expect appliction as argument or nothing. + + Returns: + List[str]: Paths to launch hook directories. + """ + + expected_types = (list, tuple, set) + + output = [] + for module in self.modules_manager.get_enabled_modules(): + # Skip module if does not have implemented 'get_launch_hook_paths' + func = getattr(module, "get_launch_hook_paths", None) + if func is None: + continue + + func = module.get_launch_hook_paths + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 + + # Pass application argument if method expect it. + try: + if expect_args: + hook_paths = func(self.application) + else: + hook_paths = func() + except Exception: + self.log.warning( + "Failed to call 'get_launch_hook_paths'", + exc_info=True + ) + continue + + if not hook_paths: + continue + + # Convert string to list + if isinstance(hook_paths, six.string_types): + hook_paths = [hook_paths] + + # Skip invalid types + if not isinstance(hook_paths, expected_types): + self.log.warning(( + "Result of `get_launch_hook_paths`" + " has invalid type {}. Expected {}" + ).format(type(hook_paths), expected_types)) + continue + + output.extend(hook_paths) + return output + def paths_to_launch_hooks(self): """Directory paths where to look for launch hooks.""" # This method has potential to be part of application manager (maybe). @@ -983,9 +1040,7 @@ class ApplicationLaunchContext: paths.append(path) # Load modules paths - paths.extend( - self.modules_manager.collect_launch_hook_paths(self.application) - ) + paths.extend(self._collect_addons_launch_hook_paths()) return paths diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1b8cf5d769..25355cbd9c 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -805,68 +805,6 @@ class ModulesManager: output.extend(paths) return output - def collect_launch_hook_paths(self, app): - """Helper to collect application launch hooks. - - It used to be based on 'ILaunchHookPaths' which is not true anymore. - Module just have to have implemented 'get_launch_hook_paths' method. - - Args: - app (Application): Application object which can be used for - filtering of which launch hook paths are returned. - - Returns: - list: Paths to launch hook directories. - """ - - str_type = type("") - expected_types = (list, tuple, set) - - output = [] - for module in self.get_enabled_modules(): - # Skip module if does not have implemented 'get_launch_hook_paths' - func = getattr(module, "get_launch_hook_paths", None) - if func is None: - continue - - func = module.get_launch_hook_paths - if hasattr(inspect, "signature"): - sig = inspect.signature(func) - expect_args = len(sig.parameters) > 0 - else: - expect_args = len(inspect.getargspec(func)[0]) > 0 - - # Pass application argument if method expect it. - try: - if expect_args: - hook_paths = func(app) - else: - hook_paths = func() - except Exception: - self.log.warning( - "Failed to call 'get_launch_hook_paths'", - exc_info=True - ) - continue - - if not hook_paths: - continue - - # Convert string to list - if isinstance(hook_paths, str_type): - hook_paths = [hook_paths] - - # Skip invalid types - if not isinstance(hook_paths, expected_types): - self.log.warning(( - "Result of `get_launch_hook_paths`" - " has invalid type {}. Expected {}" - ).format(type(hook_paths), expected_types)) - continue - - output.extend(hook_paths) - return output - def get_host_module(self, host_name): """Find host module by host name. From c1268ec253a1c9b4ee7e3d8dc5acb76712a8a035 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 15:55:09 +0200 Subject: [PATCH 0983/1030] implemented hamrony addon --- openpype/hosts/harmony/__init__.py | 17 ++++++++--------- openpype/hosts/harmony/addon.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/harmony/addon.py diff --git a/openpype/hosts/harmony/__init__.py b/openpype/hosts/harmony/__init__.py index d2f710d83d..9177eaa285 100644 --- a/openpype/hosts/harmony/__init__.py +++ b/openpype/hosts/harmony/__init__.py @@ -1,11 +1,10 @@ -import os +from .addon import ( + HARMONY_HOST_DIR, + HarmonyAddon, +) -def add_implementation_envs(env, _app): - """Modify environments to contain all required for implementation.""" - openharmony_path = os.path.join( - os.environ["OPENPYPE_REPOS_ROOT"], "openpype", "hosts", - "harmony", "vendor", "OpenHarmony" - ) - # TODO check if is already set? What to do if is already set? - env["LIB_OPENHARMONY_PATH"] = openharmony_path +__all__ = ( + "HARMONY_HOST_DIR", + "HarmonyAddon", +) diff --git a/openpype/hosts/harmony/addon.py b/openpype/hosts/harmony/addon.py new file mode 100644 index 0000000000..b051d68abb --- /dev/null +++ b/openpype/hosts/harmony/addon.py @@ -0,0 +1,24 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class HarmonyAddon(OpenPypeModule, IHostModule): + name = "harmony" + host_name = "harmony" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + """Modify environments to contain all required for implementation.""" + openharmony_path = os.path.join( + HARMONY_HOST_DIR, "vendor", "OpenHarmony" + ) + # TODO check if is already set? What to do if is already set? + env["LIB_OPENHARMONY_PATH"] = openharmony_path + + def get_workfile_extensions(self): + return [".zip"] From a5ce719e58eadaa9c936086e7fdbd37f4eaba7fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 15:55:19 +0200 Subject: [PATCH 0984/1030] removed usage of HOST_WORKFILE_EXTENSIONS --- openpype/hosts/harmony/api/workio.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/api/workio.py b/openpype/hosts/harmony/api/workio.py index ab1cb9b1a9..8df5ede917 100644 --- a/openpype/hosts/harmony/api/workio.py +++ b/openpype/hosts/harmony/api/workio.py @@ -2,8 +2,6 @@ import os import shutil -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - from .lib import ( ProcessContext, get_local_harmony_path, @@ -16,7 +14,7 @@ save_disabled = False def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["harmony"] + return [".zip"] def has_unsaved_changes(): From bdedea41d67bd72ecca676c066d24a83120b3215 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 15:55:30 +0200 Subject: [PATCH 0985/1030] reuse 'HARMONY_HOST_DIR' from public api --- openpype/hosts/harmony/api/pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 4d71b9380d..4b9849c190 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -14,14 +14,14 @@ from openpype.pipeline import ( ) from openpype.pipeline.load import get_outdated_containers from openpype.pipeline.context_tools import get_current_project_asset -import openpype.hosts.harmony + +from openpype.hosts.harmony import HARMONY_HOST_DIR import openpype.hosts.harmony.api as harmony log = logging.getLogger("openpype.hosts.harmony") -HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.harmony.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(HARMONY_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") From 1810d757856093726e37b9f4eee8eb50ebfb5934 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:23:53 +0200 Subject: [PATCH 0986/1030] implemented photoshop addon --- openpype/hosts/photoshop/__init__.py | 19 ++++++++++--------- openpype/hosts/photoshop/addon.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/photoshop/addon.py diff --git a/openpype/hosts/photoshop/__init__.py b/openpype/hosts/photoshop/__init__.py index a91e0a65ff..b3f66ea35c 100644 --- a/openpype/hosts/photoshop/__init__.py +++ b/openpype/hosts/photoshop/__init__.py @@ -1,9 +1,10 @@ -def add_implementation_envs(env, _app): - """Modify environments to contain all required for implementation.""" - defaults = { - "OPENPYPE_LOG_NO_COLORS": "True", - "WEBSOCKET_URL": "ws://localhost:8099/ws/" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value +from .module import ( + PhotoshopAddon, + PHOTOSHOP_HOST_DIR, +) + + +__all__ = ( + "PhotoshopAddon", + "PHOTOSHOP_HOST_DIR", +) diff --git a/openpype/hosts/photoshop/addon.py b/openpype/hosts/photoshop/addon.py new file mode 100644 index 0000000000..18899d4de8 --- /dev/null +++ b/openpype/hosts/photoshop/addon.py @@ -0,0 +1,26 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class PhotoshopAddon(OpenPypeModule, IHostModule): + name = "photoshop" + host_name = "photoshop" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + """Modify environments to contain all required for implementation.""" + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True", + "WEBSOCKET_URL": "ws://localhost:8099/ws/" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + + def get_workfile_extensions(self): + return [".psd", ".psb"] From 355edb24f55cc9dc9ff9cc9c7d9c6aaf612efadb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:24:04 +0200 Subject: [PATCH 0987/1030] reuse PHOTOSHOP_HOST_DIR from public api --- openpype/hosts/photoshop/api/pipeline.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index ee150d1808..f660096630 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -14,14 +14,13 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers -import openpype.hosts.photoshop +from openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR from . import lib log = Logger.get_logger(__name__) -HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") From f0af027faaa4a1c015ee91de0cb98a5c22152e05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:24:12 +0200 Subject: [PATCH 0988/1030] removed usage of HOST_WORKFILE_EXTENSIONS --- openpype/hosts/photoshop/api/workio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py index 951c5dbfff..35b44d6070 100644 --- a/openpype/hosts/photoshop/api/workio.py +++ b/openpype/hosts/photoshop/api/workio.py @@ -1,7 +1,6 @@ """Host API required Work Files tool""" import os -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS from . import lib @@ -14,7 +13,7 @@ def _active_document(): def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["photoshop"] + return [".psd", ".psb"] def has_unsaved_changes(): From caf9e014bdb8150ac2abcc8dd23cbe5cb88ab09d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:32:46 +0200 Subject: [PATCH 0989/1030] implemented webpublish addon --- openpype/hosts/webpublisher/__init__.py | 10 ++++++++++ openpype/hosts/webpublisher/addon.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 openpype/hosts/webpublisher/addon.py diff --git a/openpype/hosts/webpublisher/__init__.py b/openpype/hosts/webpublisher/__init__.py index e69de29bb2..4e918c5d7d 100644 --- a/openpype/hosts/webpublisher/__init__.py +++ b/openpype/hosts/webpublisher/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + WebpublisherAddon, + WEBPUBLISHER_ROOT_DIR, +) + + +__all__ = ( + "WebpublisherAddon", + "WEBPUBLISHER_ROOT_DIR", +) diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py new file mode 100644 index 0000000000..3d76115df1 --- /dev/null +++ b/openpype/hosts/webpublisher/addon.py @@ -0,0 +1,13 @@ +import os +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + +WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class WebpublisherAddon(OpenPypeModule, IHostModule): + name = "webpublisher" + host_name = "webpublisher" + + def initialize(self, module_settings): + self.enabled = True From b188afe97569a7141f6a3c3e14dc7966b1e3b853 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:39:19 +0200 Subject: [PATCH 0990/1030] reorganized imports in pype commands --- openpype/pype_commands.py | 43 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 67b0b8ad76..cb84fac3c7 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -5,19 +5,6 @@ import sys import json import time -from openpype.api import get_app_environments_for_context -from openpype.lib.plugin_tools import get_batch_asset_task_info -from openpype.lib.remote_publish import ( - get_webpublish_conn, - start_webpublish_log, - publish_and_log, - fail_batch, - find_variant_key, - get_task_data, - get_timeout, - IN_PROGRESS_STATUS -) - class PypeCommands: """Class implementing commands used by Pype. @@ -100,6 +87,7 @@ class PypeCommands: """ from openpype.lib import Logger + from openpype.lib.applications import get_app_environments_for_context from openpype.modules import ModulesManager from openpype.pipeline import install_openpype_plugins from openpype.tools.utils.host_tools import show_publish @@ -199,9 +187,23 @@ class PypeCommands: """ import pyblish.api - from openpype.lib import ApplicationManager from openpype.lib import Logger + from openpype.lib.applications import ( + ApplicationManager, + get_app_environments_for_context, + ) + from openpype.lib.plugin_tools import get_batch_asset_task_info + from openpype.lib.remote_publish import ( + get_webpublish_conn, + start_webpublish_log, + fail_batch, + find_variant_key, + get_task_data, + get_timeout, + IN_PROGRESS_STATUS + ) + log = Logger.get_logger("CLI-remotepublishfromapp") log.info("remotepublishphotoshop command") @@ -318,9 +320,16 @@ class PypeCommands: import pyblish.api import pyblish.util - from openpype.lib import Logger from openpype.pipeline import install_host from openpype.hosts.webpublisher import api as webpublisher + from openpype.lib import Logger + from openpype.lib.remote_publish import ( + get_webpublish_conn, + start_webpublish_log, + publish_and_log, + fail_batch, + get_task_data, + ) log = Logger.get_logger("remotepublish") @@ -366,8 +375,10 @@ class PypeCommands: Called by Deadline plugin to propagate environment into render jobs. """ + + from openpype.lib.applications import get_app_environments_for_context + if all((project, asset, task, app)): - from openpype.api import get_app_environments_for_context env = get_app_environments_for_context( project, asset, task, app, env_group ) From c2332507f49eb863aecd98540b9b71b421c2f1ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:41:56 +0200 Subject: [PATCH 0991/1030] implement webpublisher host with HostBase --- openpype/hosts/webpublisher/api/__init__.py | 36 ++++++++------------- openpype/pype_commands.py | 11 +++---- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index 18e3a16cf5..afea838e2c 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -1,31 +1,23 @@ import os import logging -from pyblish import api as pyblish -import openpype.hosts.webpublisher -from openpype.pipeline import legacy_io +import pyblish.api + +from openpype.host import HostBase +from openpype.hosts.webpublisher import WEBPUBLISHER_ROOT_DIR log = logging.getLogger("openpype.hosts.webpublisher") -HOST_DIR = os.path.dirname(os.path.abspath( - openpype.hosts.webpublisher.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +class WebpublisherHost(HostBase): + name = "webpublisher" -def install(): - print("Installing Pype config...") + def install(self): + print("Installing Pype config...") + pyblish.api.register_host(self.name) - pyblish.register_plugin_path(PUBLISH_PATH) - log.info(PUBLISH_PATH) - - legacy_io.install() - - -def uninstall(): - pyblish.deregister_plugin_path(PUBLISH_PATH) - - -# to have required methods for interface -def ls(): - pass + publish_plugin_dir = os.path.join( + WEBPUBLISHER_ROOT_DIR, "plugins", "publish" + ) + pyblish.api.register_plugin_path(publish_plugin_dir) + self.log.info(publish_plugin_dir) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index cb84fac3c7..6a65b78dfc 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -321,7 +321,7 @@ class PypeCommands: import pyblish.util from openpype.pipeline import install_host - from openpype.hosts.webpublisher import api as webpublisher + from openpype.hosts.webpublisher.api import WebpublisherHost from openpype.lib import Logger from openpype.lib.remote_publish import ( get_webpublish_conn, @@ -335,22 +335,21 @@ class PypeCommands: log.info("remotepublish command") - host_name = "webpublisher" + webpublisher_host = WebpublisherHost() + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path os.environ["AVALON_PROJECT"] = project - os.environ["AVALON_APP"] = host_name + os.environ["AVALON_APP"] = webpublisher_host.name os.environ["USER_EMAIL"] = user_email os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - pyblish.api.register_host(host_name) - if targets: if isinstance(targets, str): targets = [targets] for target in targets: pyblish.api.register_target(target) - install_host(webpublisher) + install_host(webpublisher_host) log.info("Running publish ...") From 61690d84774268b49904789f0ea5cd8e5171caf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:54:42 +0200 Subject: [PATCH 0992/1030] omved cli functions into webpublisher --- openpype/hosts/webpublisher/addon.py | 50 +++++ openpype/hosts/webpublisher/cli_functions.py | 204 +++++++++++++++++++ openpype/pype_commands.py | 159 +-------------- 3 files changed, 259 insertions(+), 154 deletions(-) create mode 100644 openpype/hosts/webpublisher/cli_functions.py diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 3d76115df1..1a4370c9a5 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -1,4 +1,7 @@ import os + +import click + from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostModule @@ -11,3 +14,50 @@ class WebpublisherAddon(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group( + WebpublisherAddon.name, + help="Webpublisher related commands.") +def cli_main(): + pass + + +@cli_main.command() +@click.argument("path") +@click.option("-u", "--user", help="User email address") +@click.option("-p", "--project", help="Project") +@click.option("-t", "--targets", help="Targets", default=None, + multiple=True) +def publish(project, path, user=None, targets=None): + """Start CLI publishing. + + Publish collects json from paths provided as an argument. + More than one path is allowed. + """ + + from .cli_functions import publish + + publish(project, path, user, targets) + + +@cli_main.command() +@click.argument("path") +@click.option("-h", "--host", help="Host") +@click.option("-u", "--user", help="User email address") +@click.option("-p", "--project", help="Project") +@click.option("-t", "--targets", help="Targets", default=None, + multiple=True) +def publishfromapp(project, path, user=None, targets=None): + """Start CLI publishing. + + Publish collects json from paths provided as an argument. + More than one path is allowed. + """ + + from .cli_functions import publish_from_app + + publish_from_app(project, path, user, targets) diff --git a/openpype/hosts/webpublisher/cli_functions.py b/openpype/hosts/webpublisher/cli_functions.py new file mode 100644 index 0000000000..cb2e59fac2 --- /dev/null +++ b/openpype/hosts/webpublisher/cli_functions.py @@ -0,0 +1,204 @@ +import os +import time +import pyblish.api +import pyblish.util + +from openpype.lib import Logger +from openpype.lib.remote_publish import ( + get_webpublish_conn, + start_webpublish_log, + publish_and_log, + fail_batch, + find_variant_key, + get_task_data, + get_timeout, + IN_PROGRESS_STATUS +) +from openpype.lib.applications import ( + ApplicationManager, + get_app_environments_for_context, +) +from openpype.lib.plugin_tools import get_batch_asset_task_info +from openpype.pipeline import install_host +from openpype.hosts.webpublisher.api import WebpublisherHost + + +def publish(project_name, batch_path, user_email, targets): + """Start headless publishing. + + Used to publish rendered assets, workfiles etc via Webpublisher. + Eventually should be yanked out to Webpublisher cli. + + Publish use json from passed paths argument. + + Args: + 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.) + user_email (string): email address for webpublisher - used to + find Ftrack user with same email + targets (list): Pyblish targets + (to choose validator for example) + + Raises: + RuntimeError: When there is no path to process. + """ + + if not batch_path: + raise RuntimeError("No publish paths specified") + + log = Logger.get_logger("remotepublish") + log.info("remotepublish command") + + # Register target and host + webpublisher_host = WebpublisherHost() + + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path + os.environ["AVALON_PROJECT"] = project_name + os.environ["AVALON_APP"] = webpublisher_host.name + os.environ["USER_EMAIL"] = user_email + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + + if targets: + if isinstance(targets, str): + targets = [targets] + for target in targets: + pyblish.api.register_target(target) + + install_host(webpublisher_host) + + log.info("Running publish ...") + + _, batch_id = os.path.split(batch_path) + dbcon = get_webpublish_conn() + _id = start_webpublish_log(dbcon, batch_id, user_email) + + task_data = get_task_data(batch_path) + if not task_data["context"]: + msg = "Batch manifest must contain context data" + msg += "Create new batch and set context properly." + fail_batch(_id, dbcon, msg) + + publish_and_log(dbcon, _id, log, batch_id=batch_id) + + log.info("Publish finished.") + + +def publish_from_app( + project_name, batch_path, host_name, user_email, targets +): + """Opens installed variant of 'host' and run remote publish there. + + Eventually should be yanked out to Webpublisher cli. + + Currently implemented and tested for Photoshop where customer + wants to process uploaded .psd file and publish collected layers + from there. Triggered by Webpublisher. + + Checks if no other batches are running (status =='in_progress). If + so, it sleeps for SLEEP (this is separate process), + waits for WAIT_FOR seconds altogether. + + Requires installed host application on the machine. + + Runs publish process as user would, in automatic fashion. + + Args: + 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' + user_email (string): email address for webpublisher - used to + find Ftrack user with same email + targets (list): Pyblish targets + (to choose validator for example) + """ + + log = Logger.get_logger("RemotePublishFromApp") + + log.info("remotepublishphotoshop command") + + task_data = get_task_data(batch_path) + + workfile_path = os.path.join(batch_path, + task_data["task"], + task_data["files"][0]) + + print("workfile_path {}".format(workfile_path)) + + batch_id = task_data["batch"] + dbcon = get_webpublish_conn() + # safer to start logging here, launch might be broken altogether + _id = start_webpublish_log(dbcon, batch_id, user_email) + + batches_in_progress = list(dbcon.find({"status": IN_PROGRESS_STATUS})) + if len(batches_in_progress) > 1: + 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) + + if not task_data["context"]: + msg = "Batch manifest must contain context data" + msg += "Create new batch and set context properly." + fail_batch(_id, dbcon, msg) + + asset_name, 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) + app_name = "{}/{}".format(host_name, found_variant_key) + + # must have for proper launch of app + env = get_app_environments_for_context( + project_name, + asset_name, + task_name, + app_name + ) + print("env:: {}".format(env)) + os.environ.update(env) + + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path + # must pass identifier to update log lines for a batch + os.environ["BATCH_LOG_ID"] = str(_id) + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + os.environ["USER_EMAIL"] = user_email + + pyblish.api.register_host(host_name) + if targets: + if isinstance(targets, str): + targets = [targets] + current_targets = os.environ.get("PYBLISH_TARGETS", "").split( + os.pathsep) + for target in targets: + current_targets.append(target) + + os.environ["PYBLISH_TARGETS"] = os.pathsep.join( + set(current_targets)) + + data = { + "last_workfile_path": workfile_path, + "start_last_workfile": True, + "project_name": project_name, + "asset_name": asset_name, + "task_name": task_name + } + + launched_app = application_manager.launch(app_name, **data) + + timeout = get_timeout(project_name, 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) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 6a65b78dfc..1817724df1 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -186,110 +186,11 @@ class PypeCommands: (to choose validator for example) """ - import pyblish.api + from openpype.hosts.webpublisher.cli_functions import publish_from_app - from openpype.lib import Logger - from openpype.lib.applications import ( - ApplicationManager, - get_app_environments_for_context, + publish_from_app( + project_name, batch_path, host_name, user_email, targets ) - from openpype.lib.plugin_tools import get_batch_asset_task_info - from openpype.lib.remote_publish import ( - get_webpublish_conn, - start_webpublish_log, - fail_batch, - find_variant_key, - get_task_data, - get_timeout, - IN_PROGRESS_STATUS - ) - - log = Logger.get_logger("CLI-remotepublishfromapp") - - log.info("remotepublishphotoshop command") - - task_data = get_task_data(batch_path) - - workfile_path = os.path.join(batch_path, - task_data["task"], - task_data["files"][0]) - - print("workfile_path {}".format(workfile_path)) - - batch_id = task_data["batch"] - dbcon = get_webpublish_conn() - # safer to start logging here, launch might be broken altogether - _id = start_webpublish_log(dbcon, batch_id, user_email) - - batches_in_progress = list(dbcon.find({"status": IN_PROGRESS_STATUS})) - if len(batches_in_progress) > 1: - 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) - - if not task_data["context"]: - msg = "Batch manifest must contain context data" - msg += "Create new batch and set context properly." - fail_batch(_id, dbcon, msg) - - asset_name, 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) - app_name = "{}/{}".format(host_name, found_variant_key) - - # must have for proper launch of app - env = get_app_environments_for_context( - project_name, - asset_name, - task_name, - app_name - ) - print("env:: {}".format(env)) - os.environ.update(env) - - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path - # must pass identifier to update log lines for a batch - os.environ["BATCH_LOG_ID"] = str(_id) - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - os.environ["USER_EMAIL"] = user_email - - pyblish.api.register_host(host_name) - if targets: - if isinstance(targets, str): - targets = [targets] - current_targets = os.environ.get("PYBLISH_TARGETS", "").split( - os.pathsep) - for target in targets: - current_targets.append(target) - - os.environ["PYBLISH_TARGETS"] = os.pathsep.join( - set(current_targets)) - - data = { - "last_workfile_path": workfile_path, - "start_last_workfile": True, - "project_name": project_name, - "asset_name": asset_name, - "task_name": task_name - } - - launched_app = application_manager.launch(app_name, **data) - - timeout = get_timeout(project_name, 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) @staticmethod def remotepublish(project, batch_path, user_email, targets=None): @@ -313,59 +214,10 @@ class PypeCommands: Raises: RuntimeError: When there is no path to process. """ - if not batch_path: - raise RuntimeError("No publish paths specified") - # Register target and host - import pyblish.api - import pyblish.util + from openpype.hosts.webpublisher.cli_functions import publish - from openpype.pipeline import install_host - from openpype.hosts.webpublisher.api import WebpublisherHost - from openpype.lib import Logger - from openpype.lib.remote_publish import ( - get_webpublish_conn, - start_webpublish_log, - publish_and_log, - fail_batch, - get_task_data, - ) - - log = Logger.get_logger("remotepublish") - - log.info("remotepublish command") - - webpublisher_host = WebpublisherHost() - - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path - os.environ["AVALON_PROJECT"] = project - os.environ["AVALON_APP"] = webpublisher_host.name - os.environ["USER_EMAIL"] = user_email - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - - if targets: - if isinstance(targets, str): - targets = [targets] - for target in targets: - pyblish.api.register_target(target) - - install_host(webpublisher_host) - - log.info("Running publish ...") - - _, batch_id = os.path.split(batch_path) - dbcon = get_webpublish_conn() - _id = start_webpublish_log(dbcon, batch_id, user_email) - - task_data = get_task_data(batch_path) - if not task_data["context"]: - msg = "Batch manifest must contain context data" - msg += "Create new batch and set context properly." - fail_batch(_id, dbcon, msg) - - publish_and_log(dbcon, _id, log, batch_id=batch_id) - - log.info("Publish finished.") + publish(project, batch_path, user_email, targets) @staticmethod def extractenvironments(output_json_path, project, asset, task, app, @@ -479,7 +331,6 @@ class PypeCommands: sync_server_module.server_init() sync_server_module.server_start() - import time while True: time.sleep(1.0) From eed9789287adeb1ba000262544368344be353ff9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 16:59:20 +0200 Subject: [PATCH 0993/1030] changed function names --- openpype/hosts/webpublisher/addon.py | 8 ++++---- openpype/hosts/webpublisher/cli_functions.py | 4 ++-- openpype/pype_commands.py | 12 ++++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 1a4370c9a5..9e63030fe2 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -39,9 +39,9 @@ def publish(project, path, user=None, targets=None): More than one path is allowed. """ - from .cli_functions import publish + from .cli_functions import cli_publish - publish(project, path, user, targets) + cli_publish(project, path, user, targets) @cli_main.command() @@ -58,6 +58,6 @@ def publishfromapp(project, path, user=None, targets=None): More than one path is allowed. """ - from .cli_functions import publish_from_app + from .cli_functions import cli_publish_from_app - publish_from_app(project, path, user, targets) + cli_publish_from_app(project, path, user, targets) diff --git a/openpype/hosts/webpublisher/cli_functions.py b/openpype/hosts/webpublisher/cli_functions.py index cb2e59fac2..ad3bb596fb 100644 --- a/openpype/hosts/webpublisher/cli_functions.py +++ b/openpype/hosts/webpublisher/cli_functions.py @@ -23,7 +23,7 @@ from openpype.pipeline import install_host from openpype.hosts.webpublisher.api import WebpublisherHost -def publish(project_name, batch_path, user_email, targets): +def cli_publish(project_name, batch_path, user_email, targets): """Start headless publishing. Used to publish rendered assets, workfiles etc via Webpublisher. @@ -85,7 +85,7 @@ def publish(project_name, batch_path, user_email, targets): log.info("Publish finished.") -def publish_from_app( +def cli_publish_from_app( project_name, batch_path, host_name, user_email, targets ): """Opens installed variant of 'host' and run remote publish there. diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 1817724df1..b6c1228ade 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -186,9 +186,11 @@ class PypeCommands: (to choose validator for example) """ - from openpype.hosts.webpublisher.cli_functions import publish_from_app + from openpype.hosts.webpublisher.cli_functions import ( + cli_publish_from_app + ) - publish_from_app( + cli_publish_from_app( project_name, batch_path, host_name, user_email, targets ) @@ -215,9 +217,11 @@ class PypeCommands: RuntimeError: When there is no path to process. """ - from openpype.hosts.webpublisher.cli_functions import publish + from openpype.hosts.webpublisher.cli_functions import ( + cli_publish + ) - publish(project, batch_path, user_email, targets) + cli_publish(project, batch_path, user_email, targets) @staticmethod def extractenvironments(output_json_path, project, asset, task, app, From dec6335ece01b37e6f0396807526ab16b5db1a6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:41:18 +0200 Subject: [PATCH 0994/1030] move remote_publish funtion into pipeline publish --- openpype/pipeline/publish/lib.py | 40 ++++++++++++++++++++++++++++++ openpype/scripts/remote_publish.py | 7 +++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index d5494cd8a4..9060a0bf4b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -273,3 +273,43 @@ def filter_pyblish_plugins(plugins): option, value, plugin.__name__)) setattr(plugin, option, value) + + +def find_close_plugin(close_plugin_name, log): + if close_plugin_name: + plugins = pyblish.api.discover() + for plugin in plugins: + if plugin.__name__ == close_plugin_name: + return plugin + + log.debug("Close plugin not found, app might not close.") + + +def remote_publish(log, close_plugin_name=None, raise_error=False): + """Loops through all plugins, logs to console. Used for tests. + + Args: + log (openpype.lib.Logger) + close_plugin_name (str): name of plugin with responsibility to + close host app + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + close_plugin = find_close_plugin(close_plugin_name, log) + + for result in pyblish.util.publish_iter(): + for record in result["records"]: + log.info("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + error_message = error_format.format(**result) + log.error(error_message) + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin().process(context) + if raise_error: + # Fatal Error is because of Deadline + error_message = "Fatal Error: " + error_format.format(**result) + raise RuntimeError(error_message) diff --git a/openpype/scripts/remote_publish.py b/openpype/scripts/remote_publish.py index d322f369d1..37df35e36c 100644 --- a/openpype/scripts/remote_publish.py +++ b/openpype/scripts/remote_publish.py @@ -1,11 +1,12 @@ try: - from openpype.api import Logger - import openpype.lib.remote_publish + from openpype.lib import Logger + from openpype.pipeline.publish.lib import remote_publish except ImportError as exc: # Ensure Deadline fails by output an error that contains "Fatal Error:" raise ImportError("Fatal Error: %s" % exc) + if __name__ == "__main__": # Perform remote publish with thorough error checking log = Logger.get_logger(__name__) - openpype.lib.remote_publish.publish(log, raise_error=True) + remote_publish(log, raise_error=True) From c1a7b9aff5024c4df92493169e2741f044558b2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:49:24 +0200 Subject: [PATCH 0995/1030] moved webpublisher specific functions into webpublisher --- openpype/hosts/webpublisher/lib.py | 278 +++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 openpype/hosts/webpublisher/lib.py diff --git a/openpype/hosts/webpublisher/lib.py b/openpype/hosts/webpublisher/lib.py new file mode 100644 index 0000000000..dde875c934 --- /dev/null +++ b/openpype/hosts/webpublisher/lib.py @@ -0,0 +1,278 @@ +import os +from datetime import datetime +import collections +import json + +from bson.objectid import ObjectId + +import pyblish.util +import pyblish.api + +from openpype.client.mongo import OpenPypeMongoConnection +from openpype.settings import get_project_settings +from openpype.lib import Logger +from openpype.lib.profiles_filtering import filter_profiles + +ERROR_STATUS = "error" +IN_PROGRESS_STATUS = "in_progress" +REPROCESS_STATUS = "reprocess" +SENT_REPROCESSING_STATUS = "sent_for_reprocessing" +FINISHED_REPROCESS_STATUS = "republishing_finished" +FINISHED_OK_STATUS = "finished_ok" + +log = Logger.get_logger(__name__) + + +def parse_json(path): + """Parses json file at 'path' location + + Returns: + (dict) or None if unparsable + Raises: + AsssertionError if 'path' doesn't exist + """ + path = path.strip('\"') + assert os.path.isfile(path), ( + "Path to json file doesn't exist. \"{}\"".format(path) + ) + data = None + with open(path, "r") as json_file: + try: + data = json.load(json_file) + except Exception as exc: + log.error( + "Error loading json: {} - Exception: {}".format(path, exc) + ) + return data + + +def get_batch_asset_task_info(ctx): + """Parses context data from webpublisher's batch metadata + + Returns: + (tuple): asset, task_name (Optional), task_type + """ + task_type = "default_task_type" + task_name = None + asset = None + + if ctx["type"] == "task": + items = ctx["path"].split('/') + asset = items[-2] + task_name = ctx["name"] + task_type = ctx["attributes"]["type"] + else: + asset = ctx["name"] + + return asset, task_name, task_type + + +def get_webpublish_conn(): + """Get connection to OP 'webpublishes' collection.""" + mongo_client = OpenPypeMongoConnection.get_mongo_client() + database_name = os.environ["OPENPYPE_DATABASE_NAME"] + return mongo_client[database_name]["webpublishes"] + + +def start_webpublish_log(dbcon, batch_id, user): + """Start new log record for 'batch_id' + + Args: + dbcon (OpenPypeMongoConnection) + batch_id (str) + user (str) + Returns + (ObjectId) from DB + """ + return dbcon.insert_one({ + "batch_id": batch_id, + "start_date": datetime.now(), + "user": user, + "status": IN_PROGRESS_STATUS, + "progress": 0 # integer 0-100, percentage + }).inserted_id + + +def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): + """Loops through all plugins, logs ok and fails into OP DB. + + Args: + dbcon (OpenPypeMongoConnection) + _id (str) - id of current job in DB + log (openpype.lib.Logger) + batch_id (str) - id sent from frontend + close_plugin_name (str): name of plugin with responsibility to + close host app + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}\n" + error_format += "-" * 80 + "\n" + + close_plugin = _get_close_plugin(close_plugin_name, log) + + if isinstance(_id, str): + _id = ObjectId(_id) + + log_lines = [] + processed = 0 + log_every = 5 + for result in pyblish.util.publish_iter(): + for record in result["records"]: + log_lines.append("{}: {}".format( + result["plugin"].label, record.msg)) + processed += 1 + + if result["error"]: + log.error(error_format.format(**result)) + log_lines = [error_format.format(**result)] + log_lines + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "finish_date": datetime.now(), + "status": ERROR_STATUS, + "log": os.linesep.join(log_lines) + + }} + ) + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin().process(context) + return + elif processed % log_every == 0: + # pyblish returns progress in 0.0 - 2.0 + progress = min(round(result["progress"] / 2 * 100), 99) + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "progress": progress, + "log": os.linesep.join(log_lines) + }} + ) + + # final update + if batch_id: + dbcon.update_many( + {"batch_id": batch_id, "status": SENT_REPROCESSING_STATUS}, + { + "$set": + { + "finish_date": datetime.now(), + "status": FINISHED_REPROCESS_STATUS, + } + } + ) + + dbcon.update_one( + {"_id": _id}, + { + "$set": + { + "finish_date": datetime.now(), + "status": FINISHED_OK_STATUS, + "progress": 100, + "log": os.linesep.join(log_lines) + } + } + ) + + +def fail_batch(_id, dbcon, msg): + """Set current batch as failed as there is some problem. + + Raises: + ValueError + """ + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "finish_date": datetime.now(), + "status": ERROR_STATUS, + "log": msg + + }} + ) + raise ValueError(msg) + + +def find_variant_key(application_manager, host): + """Searches for latest installed variant for 'host' + + Args: + application_manager (ApplicationManager) + host (str) + Returns + (string) (optional) + Raises: + (ValueError) if no variant found + """ + app_group = application_manager.app_groups.get(host) + if not app_group or not app_group.enabled: + raise ValueError("No application {} configured".format(host)) + + found_variant_key = None + # finds most up-to-date variant if any installed + sorted_variants = collections.OrderedDict( + sorted(app_group.variants.items())) + for variant_key, variant in sorted_variants.items(): + for executable in variant.executables: + if executable.exists(): + found_variant_key = variant_key + + if not found_variant_key: + raise ValueError("No executable for {} found".format(host)) + + return found_variant_key + + +def _get_close_plugin(close_plugin_name, log): + if close_plugin_name: + plugins = pyblish.api.discover() + for plugin in plugins: + if plugin.__name__ == close_plugin_name: + return plugin + + log.debug("Close plugin not found, app might not close.") + + +def get_task_data(batch_dir): + """Return parsed data from first task manifest.json + + Used for `remotepublishfromapp` command where batch contains only + single task with publishable workfile. + + Returns: + (dict) + Throws: + (ValueError) if batch or task manifest not found or broken + """ + batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) + if not batch_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(batch_dir)) + task_dir_name = batch_data["tasks"][0] + task_data = parse_json(os.path.join(batch_dir, task_dir_name, + "manifest.json")) + if not task_data: + raise ValueError( + "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 = 3600 + if matching_item: + timeout = matching_item["timeout"] + + return timeout From 6c330c48969bffba508a83aa0d98cf93d804142f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:50:54 +0200 Subject: [PATCH 0996/1030] use lib functions from webpublisher --- .../plugins/publish/collect_batch_data.py | 8 ++++---- openpype/hosts/webpublisher/cli_functions.py | 17 +++++++++-------- .../plugins/publish/collect_batch_data.py | 11 ++++++----- .../plugins/publish/collect_published_files.py | 6 ++---- .../publish/collect_tvpaint_workfile_data.py | 2 +- .../webserver_service/webpublish_routes.py | 12 +++++------- .../webserver_service/webserver_cli.py | 14 +++++++------- 7 files changed, 34 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py index 2881ef0ea6..5d50a78914 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -17,11 +17,11 @@ import os import pyblish.api -from openpype.lib.plugin_tools import ( - parse_json, - get_batch_asset_task_info -) from openpype.pipeline import legacy_io +from openpype_modules.webpublisher.lib import ( + get_batch_asset_task_info, + parse_json +) class CollectBatchData(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/cli_functions.py b/openpype/hosts/webpublisher/cli_functions.py index ad3bb596fb..83f53ced68 100644 --- a/openpype/hosts/webpublisher/cli_functions.py +++ b/openpype/hosts/webpublisher/cli_functions.py @@ -4,7 +4,15 @@ import pyblish.api import pyblish.util from openpype.lib import Logger -from openpype.lib.remote_publish import ( +from openpype.lib.applications import ( + ApplicationManager, + get_app_environments_for_context, +) +from openpype.pipeline import install_host +from openpype.hosts.webpublisher.api import WebpublisherHost + +from .lib import ( + get_batch_asset_task_info, get_webpublish_conn, start_webpublish_log, publish_and_log, @@ -14,13 +22,6 @@ from openpype.lib.remote_publish import ( get_timeout, IN_PROGRESS_STATUS ) -from openpype.lib.applications import ( - ApplicationManager, - get_app_environments_for_context, -) -from openpype.lib.plugin_tools import get_batch_asset_task_info -from openpype.pipeline import install_host -from openpype.hosts.webpublisher.api import WebpublisherHost def cli_publish(project_name, batch_path, user_email, targets): diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py index 9ff779636a..eb2737b276 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py @@ -13,12 +13,13 @@ import os import pyblish.api -from openpype.lib.plugin_tools import ( - parse_json, - get_batch_asset_task_info -) -from openpype.lib.remote_publish import get_webpublish_conn, IN_PROGRESS_STATUS from openpype.pipeline import legacy_io +from openpype_modules.webpublisher.lib import ( + parse_json, + get_batch_asset_task_info, + get_webpublish_conn, + IN_PROGRESS_STATUS +) class CollectBatchData(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 20e277d794..454f78ce9d 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -23,10 +23,8 @@ from openpype.lib import ( get_ffprobe_streams, convert_ffprobe_fps_value, ) -from openpype.lib.plugin_tools import ( - parse_json, - get_subset_name_with_asset_doc -) +from openpype.lib.plugin_tools import get_subset_name_with_asset_doc +from openpype_modules.webpublisher.lib import parse_json class CollectPublishedFiles(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_workfile_data.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_workfile_data.py index f0f29260a2..b5f8ed9c8f 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_workfile_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_workfile_data.py @@ -16,11 +16,11 @@ import uuid import json import shutil import pyblish.api -from openpype.lib.plugin_tools import parse_json from openpype.hosts.tvpaint.worker import ( SenderTVPaintCommands, CollectSceneData ) +from openpype_modules.webpublisher.lib import parse_json class CollectTVPaintWorkfileData(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 2e9d460a98..e3de555ace 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -10,16 +10,16 @@ from aiohttp.web_response import Response from openpype.client import ( get_projects, get_assets, - OpenPypeMongoConnection, ) from openpype.lib import Logger -from openpype.lib.remote_publish import ( +from openpype.settings import get_project_settings +from openpype_modules.webserver.base_routes import RestApiEndpoint +from openpype_modules.webpublisher.lib import ( + get_webpublish_conn, get_task_data, ERROR_STATUS, REPROCESS_STATUS ) -from openpype.settings import get_project_settings -from openpype_modules.webserver.base_routes import RestApiEndpoint log = Logger.get_logger("WebpublishRoutes") @@ -77,9 +77,7 @@ class WebpublishRestApiResource(JsonApiResource): """Resource carrying OP DB connection for storing batch info into DB.""" def __init__(self): - mongo_client = OpenPypeMongoConnection.get_mongo_client() - database_name = os.environ["OPENPYPE_DATABASE_NAME"] - self.dbcon = mongo_client[database_name]["webpublishes"] + self.dbcon = get_webpublish_conn() class ProjectsEndpoint(ResourceRestApiEndpoint): diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py index 936bd9735f..47c792a575 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py @@ -7,8 +7,15 @@ import json import subprocess from openpype.client import OpenPypeMongoConnection +from openpype.modules import ModulesManager from openpype.lib import Logger +from openpype_modules.webpublisher.lib import ( + ERROR_STATUS, + REPROCESS_STATUS, + SENT_REPROCESSING_STATUS +) + from .webpublish_routes import ( RestApiResource, WebpublishRestApiResource, @@ -21,19 +28,12 @@ from .webpublish_routes import ( TaskPublishEndpoint, UserReportEndpoint ) -from openpype.lib.remote_publish import ( - ERROR_STATUS, - REPROCESS_STATUS, - SENT_REPROCESSING_STATUS -) - log = Logger.get_logger("webserver_gui") def run_webserver(*args, **kwargs): """Runs webserver in command line, adds routes.""" - from openpype.modules import ModulesManager manager = ModulesManager() webserver_module = manager.modules_by_name["webserver"] From 233d70bdd8d21062842aa88b15841ac1fb61f0a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:54:05 +0200 Subject: [PATCH 0997/1030] headless_publish is a method on webpublisher addon --- openpype/hosts/aftereffects/api/lib.py | 19 ++++++++++++++----- openpype/hosts/photoshop/api/lib.py | 9 +++++---- openpype/hosts/webpublisher/addon.py | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index ce4cbf09af..d5583ee862 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -3,11 +3,12 @@ import sys import contextlib import traceback import logging +from functools import partial from Qt import QtWidgets from openpype.pipeline import install_host -from openpype.lib.remote_publish import headless_publish +from openpype.modules import ModulesManager from openpype.tools.utils import host_tools from .launch_logic import ProcessLauncher, get_stub @@ -35,10 +36,18 @@ def main(*subprocess_args): launcher.start() if os.environ.get("HEADLESS_PUBLISH"): - launcher.execute_in_main_thread(lambda: headless_publish( - log, - "CloseAE", - os.environ.get("IS_TEST"))) + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] + + launcher.execute_in_main_thread( + partial( + webpublisher_addon.headless_publish, + log, + "CloseAE", + os.environ.get("IS_TEST") + ) + ) + elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): save = False if os.getenv("WORKFILES_SAVE_AS"): diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 2f57d64464..73a546604f 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -5,11 +5,10 @@ import traceback from Qt import QtWidgets -from openpype.api import Logger +from openpype.lib import env_value_to_bool, Logger +from openpype.modules import ModulesManager from openpype.pipeline import install_host from openpype.tools.utils import host_tools -from openpype.lib.remote_publish import headless_publish -from openpype.lib import env_value_to_bool from .launch_logic import ProcessLauncher, stub @@ -35,8 +34,10 @@ def main(*subprocess_args): launcher.start() if env_value_to_bool("HEADLESS_PUBLISH"): + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] launcher.execute_in_main_thread( - headless_publish, + webpublisher_addon.headless_publish, log, "ClosePS", os.environ.get("IS_TEST") diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 9e63030fe2..0bba8adc4b 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -15,6 +15,30 @@ class WebpublisherAddon(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True + def headless_publish(self, log, close_plugin_name=None, is_test=False): + """Runs publish in a opened host with a context. + + Close Python process at the end. + """ + + from openpype.pipeline.publish.lib import remote_publish + from .lib import get_webpublish_conn, publish_and_log + + if is_test: + remote_publish(log, close_plugin_name) + return + + dbcon = get_webpublish_conn() + _id = os.environ.get("BATCH_LOG_ID") + if not _id: + log.warning("Unable to store log records, " + "batch will be unfinished!") + return + + publish_and_log( + dbcon, _id, log, close_plugin_name=close_plugin_name + ) + def cli(self, click_group): click_group.add_command(cli_main) From a98c7953aaf4fecf465b7e9b95357de2056e3018 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:54:30 +0200 Subject: [PATCH 0998/1030] use 'find_close_plugin' --- openpype/hosts/webpublisher/lib.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/webpublisher/lib.py b/openpype/hosts/webpublisher/lib.py index dde875c934..4bc3f1db80 100644 --- a/openpype/hosts/webpublisher/lib.py +++ b/openpype/hosts/webpublisher/lib.py @@ -12,6 +12,7 @@ from openpype.client.mongo import OpenPypeMongoConnection from openpype.settings import get_project_settings from openpype.lib import Logger from openpype.lib.profiles_filtering import filter_profiles +from openpype.pipeline.publish.lib import find_close_plugin ERROR_STATUS = "error" IN_PROGRESS_STATUS = "in_progress" @@ -108,7 +109,7 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}\n" error_format += "-" * 80 + "\n" - close_plugin = _get_close_plugin(close_plugin_name, log) + close_plugin = find_close_plugin(close_plugin_name, log) if isinstance(_id, str): _id = ObjectId(_id) @@ -227,16 +228,6 @@ def find_variant_key(application_manager, host): return found_variant_key -def _get_close_plugin(close_plugin_name, log): - if close_plugin_name: - plugins = pyblish.api.discover() - for plugin in plugins: - if plugin.__name__ == close_plugin_name: - return plugin - - log.debug("Close plugin not found, app might not close.") - - def get_task_data(batch_dir): """Return parsed data from first task manifest.json From 9c3e37e3f4ab83e465b71d908bcec439df011385 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:55:04 +0200 Subject: [PATCH 0999/1030] removed unused functions from openpype lib --- openpype/lib/plugin_tools.py | 45 ------ openpype/lib/remote_publish.py | 277 --------------------------------- 2 files changed, 322 deletions(-) delete mode 100644 openpype/lib/remote_publish.py diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 060db94ae0..659210e6e3 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -427,48 +427,3 @@ def get_background_layers(file_url): layer.get("filename")). replace("\\", "/")) return layers - - -def parse_json(path): - """Parses json file at 'path' location - - Returns: - (dict) or None if unparsable - Raises: - AsssertionError if 'path' doesn't exist - """ - path = path.strip('\"') - assert os.path.isfile(path), ( - "Path to json file doesn't exist. \"{}\"".format(path) - ) - data = None - with open(path, "r") as json_file: - try: - data = json.load(json_file) - except Exception as exc: - log.error( - "Error loading json: " - "{} - Exception: {}".format(path, exc) - ) - return data - - -def get_batch_asset_task_info(ctx): - """Parses context data from webpublisher's batch metadata - - Returns: - (tuple): asset, task_name (Optional), task_type - """ - task_type = "default_task_type" - task_name = None - asset = None - - if ctx["type"] == "task": - items = ctx["path"].split('/') - asset = items[-2] - task_name = ctx["name"] - task_type = ctx["attributes"]["type"] - else: - asset = ctx["name"] - - return asset, task_name, task_type diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py deleted file mode 100644 index 2a901544cc..0000000000 --- a/openpype/lib/remote_publish.py +++ /dev/null @@ -1,277 +0,0 @@ -import os -from datetime import datetime -import collections - -from bson.objectid import ObjectId - -import pyblish.util -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" -REPROCESS_STATUS = "reprocess" -SENT_REPROCESSING_STATUS = "sent_for_reprocessing" -FINISHED_REPROCESS_STATUS = "republishing_finished" -FINISHED_OK_STATUS = "finished_ok" - - -def headless_publish(log, close_plugin_name=None, is_test=False): - """Runs publish in a opened host with a context and closes Python process. - """ - if not is_test: - dbcon = get_webpublish_conn() - _id = os.environ.get("BATCH_LOG_ID") - if not _id: - log.warning("Unable to store log records, " - "batch will be unfinished!") - return - - publish_and_log(dbcon, _id, log, close_plugin_name=close_plugin_name) - else: - publish(log, close_plugin_name) - - -def get_webpublish_conn(): - """Get connection to OP 'webpublishes' collection.""" - mongo_client = OpenPypeMongoConnection.get_mongo_client() - database_name = os.environ["OPENPYPE_DATABASE_NAME"] - return mongo_client[database_name]["webpublishes"] - - -def start_webpublish_log(dbcon, batch_id, user): - """Start new log record for 'batch_id' - - Args: - dbcon (OpenPypeMongoConnection) - batch_id (str) - user (str) - Returns - (ObjectId) from DB - """ - return dbcon.insert_one({ - "batch_id": batch_id, - "start_date": datetime.now(), - "user": user, - "status": IN_PROGRESS_STATUS, - "progress": 0 # integer 0-100, percentage - }).inserted_id - - -def publish(log, close_plugin_name=None, raise_error=False): - """Loops through all plugins, logs to console. Used for tests. - - Args: - log (openpype.lib.Logger) - close_plugin_name (str): name of plugin with responsibility to - close host app - """ - # Error exit as soon as any error occurs. - error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" - - close_plugin = _get_close_plugin(close_plugin_name, log) - - for result in pyblish.util.publish_iter(): - for record in result["records"]: - log.info("{}: {}".format( - result["plugin"].label, record.msg)) - - if result["error"]: - error_message = error_format.format(**result) - log.error(error_message) - if close_plugin: # close host app explicitly after error - context = pyblish.api.Context() - close_plugin().process(context) - if raise_error: - # Fatal Error is because of Deadline - error_message = "Fatal Error: " + error_format.format(**result) - raise RuntimeError(error_message) - - -def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): - """Loops through all plugins, logs ok and fails into OP DB. - - Args: - dbcon (OpenPypeMongoConnection) - _id (str) - id of current job in DB - log (openpype.lib.Logger) - batch_id (str) - id sent from frontend - close_plugin_name (str): name of plugin with responsibility to - close host app - """ - # Error exit as soon as any error occurs. - error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}\n" - error_format += "-" * 80 + "\n" - - close_plugin = _get_close_plugin(close_plugin_name, log) - - if isinstance(_id, str): - _id = ObjectId(_id) - - log_lines = [] - processed = 0 - log_every = 5 - for result in pyblish.util.publish_iter(): - for record in result["records"]: - log_lines.append("{}: {}".format( - result["plugin"].label, record.msg)) - processed += 1 - - if result["error"]: - log.error(error_format.format(**result)) - log_lines = [error_format.format(**result)] + log_lines - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "finish_date": datetime.now(), - "status": ERROR_STATUS, - "log": os.linesep.join(log_lines) - - }} - ) - if close_plugin: # close host app explicitly after error - context = pyblish.api.Context() - close_plugin().process(context) - return - elif processed % log_every == 0: - # pyblish returns progress in 0.0 - 2.0 - progress = min(round(result["progress"] / 2 * 100), 99) - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "progress": progress, - "log": os.linesep.join(log_lines) - }} - ) - - # final update - if batch_id: - dbcon.update_many( - {"batch_id": batch_id, "status": SENT_REPROCESSING_STATUS}, - { - "$set": - { - "finish_date": datetime.now(), - "status": FINISHED_REPROCESS_STATUS, - } - } - ) - - dbcon.update_one( - {"_id": _id}, - { - "$set": - { - "finish_date": datetime.now(), - "status": FINISHED_OK_STATUS, - "progress": 100, - "log": os.linesep.join(log_lines) - } - } - ) - - -def fail_batch(_id, dbcon, msg): - """Set current batch as failed as there is some problem. - - Raises: - ValueError - """ - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "finish_date": datetime.now(), - "status": ERROR_STATUS, - "log": msg - - }} - ) - raise ValueError(msg) - - -def find_variant_key(application_manager, host): - """Searches for latest installed variant for 'host' - - Args: - application_manager (ApplicationManager) - host (str) - Returns - (string) (optional) - Raises: - (ValueError) if no variant found - """ - app_group = application_manager.app_groups.get(host) - if not app_group or not app_group.enabled: - raise ValueError("No application {} configured".format(host)) - - found_variant_key = None - # finds most up-to-date variant if any installed - sorted_variants = collections.OrderedDict( - sorted(app_group.variants.items())) - for variant_key, variant in sorted_variants.items(): - for executable in variant.executables: - if executable.exists(): - found_variant_key = variant_key - - if not found_variant_key: - raise ValueError("No executable for {} found".format(host)) - - return found_variant_key - - -def _get_close_plugin(close_plugin_name, log): - if close_plugin_name: - plugins = pyblish.api.discover() - for plugin in plugins: - if plugin.__name__ == close_plugin_name: - return plugin - - log.debug("Close plugin not found, app might not close.") - - -def get_task_data(batch_dir): - """Return parsed data from first task manifest.json - - Used for `remotepublishfromapp` command where batch contains only - single task with publishable workfile. - - Returns: - (dict) - Throws: - (ValueError) if batch or task manifest not found or broken - """ - batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) - if not batch_data: - raise ValueError( - "Cannot parse batch meta in {} folder".format(batch_dir)) - task_dir_name = batch_data["tasks"][0] - task_data = parse_json(os.path.join(batch_dir, task_dir_name, - "manifest.json")) - if not task_data: - raise ValueError( - "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 = 3600 - if matching_item: - timeout = matching_item["timeout"] - - return timeout From d5f6ad9fdc1727cd3c631698ae258ed8485ca479 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 17:58:09 +0200 Subject: [PATCH 1000/1030] renamed 'cli_functions' to 'publish_functions' --- openpype/hosts/webpublisher/addon.py | 4 ++-- .../webpublisher/{cli_functions.py => publish_functions.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename openpype/hosts/webpublisher/{cli_functions.py => publish_functions.py} (100%) diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 0bba8adc4b..cb639db3fa 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -63,7 +63,7 @@ def publish(project, path, user=None, targets=None): More than one path is allowed. """ - from .cli_functions import cli_publish + from .publish_functions import cli_publish cli_publish(project, path, user, targets) @@ -82,6 +82,6 @@ def publishfromapp(project, path, user=None, targets=None): More than one path is allowed. """ - from .cli_functions import cli_publish_from_app + from .publish_functions import cli_publish_from_app cli_publish_from_app(project, path, user, targets) diff --git a/openpype/hosts/webpublisher/cli_functions.py b/openpype/hosts/webpublisher/publish_functions.py similarity index 100% rename from openpype/hosts/webpublisher/cli_functions.py rename to openpype/hosts/webpublisher/publish_functions.py From 338d12e60cd7f2fe9a15efecac07ce7ae8449d57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:03:09 +0200 Subject: [PATCH 1001/1030] added cli command for webserver --- openpype/hosts/webpublisher/addon.py | 19 +++++++++++++++++++ .../webserver_service/__init__.py | 6 ++++++ .../webserver_service/webserver_cli.py | 16 ++++++++++------ 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/webpublisher/webserver_service/__init__.py diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index cb639db3fa..85e16de4a6 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -85,3 +85,22 @@ def publishfromapp(project, path, user=None, targets=None): from .publish_functions import cli_publish_from_app cli_publish_from_app(project, path, user, targets) + + +@cli_main.command() +@click.option("-h", "--host", help="Host", default=None) +@click.option("-p", "--port", help="Port", default=None) +@click.option("-e", "--executable", help="Executable") +@click.option("-u", "--upload_dir", help="Upload dir") +def webserver(executable, upload_dir, host=None, port=None): + """Starts webserver for communication with Webpublish FR via command line + + OP must be congigured on a machine, eg. OPENPYPE_MONGO filled AND + FTRACK_BOT_API_KEY provided with api key from Ftrack. + + Expect "pype.club" user created on Ftrack. + """ + + from .webserver_service import run_webserver + + run_webserver(executable, upload_dir, host, port) diff --git a/openpype/hosts/webpublisher/webserver_service/__init__.py b/openpype/hosts/webpublisher/webserver_service/__init__.py new file mode 100644 index 0000000000..e43f3f063a --- /dev/null +++ b/openpype/hosts/webpublisher/webserver_service/__init__.py @@ -0,0 +1,6 @@ +from .webserver_cli import run_webserver + + +__all__ = ( + "run_webserver", +) diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py index 47c792a575..093b53d9d3 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py @@ -32,21 +32,25 @@ from .webpublish_routes import ( log = Logger.get_logger("webserver_gui") -def run_webserver(*args, **kwargs): +def run_webserver(executable, upload_dir, host=None, port=None): """Runs webserver in command line, adds routes.""" + if not host: + host = "localhost" + if not port: + port = 8079 + manager = ModulesManager() webserver_module = manager.modules_by_name["webserver"] - host = kwargs.get("host") or "localhost" - port = kwargs.get("port") or 8079 + server_manager = webserver_module.create_new_server_manager(port, host) webserver_url = server_manager.url # queue for remotepublishfromapp tasks studio_task_queue = collections.deque() resource = RestApiResource(server_manager, - upload_dir=kwargs["upload_dir"], - executable=kwargs["executable"], + upload_dir=upload_dir, + executable=executable, studio_task_queue=studio_task_queue) projects_endpoint = ProjectsEndpoint(resource) server_manager.add_route( @@ -111,7 +115,7 @@ def run_webserver(*args, **kwargs): last_reprocessed = time.time() while True: if time.time() - last_reprocessed > 20: - reprocess_failed(kwargs["upload_dir"], webserver_url) + reprocess_failed(upload_dir, webserver_url) last_reprocessed = time.time() if studio_task_queue: args = studio_task_queue.popleft() From e2c83c142684ccf0f400ffd345ddcf530bf49ed7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:03:40 +0200 Subject: [PATCH 1002/1030] renamed webserver_cli.py into webserver.py --- openpype/hosts/webpublisher/webserver_service/__init__.py | 2 +- .../webserver_service/{webserver_cli.py => webserver.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename openpype/hosts/webpublisher/webserver_service/{webserver_cli.py => webserver.py} (100%) diff --git a/openpype/hosts/webpublisher/webserver_service/__init__.py b/openpype/hosts/webpublisher/webserver_service/__init__.py index e43f3f063a..73111d286e 100644 --- a/openpype/hosts/webpublisher/webserver_service/__init__.py +++ b/openpype/hosts/webpublisher/webserver_service/__init__.py @@ -1,4 +1,4 @@ -from .webserver_cli import run_webserver +from .webserver import run_webserver __all__ = ( diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver.py similarity index 100% rename from openpype/hosts/webpublisher/webserver_service/webserver_cli.py rename to openpype/hosts/webpublisher/webserver_service/webserver.py From 971ae6d1ed0ad07713524dd4e7066e100fd22b34 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:04:37 +0200 Subject: [PATCH 1003/1030] fix import in global commands --- openpype/pype_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index b6c1228ade..fe46a4bc54 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -61,8 +61,8 @@ class PypeCommands: @staticmethod def launch_webpublisher_webservercli(*args, **kwargs): - from openpype.hosts.webpublisher.webserver_service.webserver_cli \ - import (run_webserver) + from openpype.hosts.webpublisher.webserver_service import run_webserver + return run_webserver(*args, **kwargs) @staticmethod From ae11ae16d5fd0b9f1cdb86263a25ace48bbf9b04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:12:20 +0200 Subject: [PATCH 1004/1030] modify launch arguments --- .../webserver_service/webpublish_routes.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index e3de555ace..4039d2c8ec 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -14,6 +14,7 @@ from openpype.client import ( from openpype.lib import Logger from openpype.settings import get_project_settings from openpype_modules.webserver.base_routes import RestApiEndpoint +from openpype_modules.webpublisher import WebpublisherAddon from openpype_modules.webpublisher.lib import ( get_webpublish_conn, get_task_data, @@ -213,7 +214,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): # TVPaint filter { "extensions": [".tvpp"], - "command": "remotepublish", + "command": "publish", "arguments": { "targets": ["tvpaint_worker"] }, @@ -222,13 +223,13 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): # Photoshop filter { "extensions": [".psd", ".psb"], - "command": "remotepublishfromapp", + "command": "publishfromapp", "arguments": { - # Command 'remotepublishfromapp' requires --host argument + # Command 'publishfromapp' requires --host argument "host": "photoshop", # Make sure targets are set to None for cases that default # would change - # - targets argument is not used in 'remotepublishfromapp' + # - targets argument is not used in 'publishfromapp' "targets": ["remotepublish"] }, # does publish need to be handled by a queue, eg. only @@ -240,7 +241,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): batch_dir = os.path.join(self.resource.upload_dir, content["batch"]) # Default command and arguments - command = "remotepublish" + command = "publish" add_args = { # All commands need 'project' and 'user' "project": content["project_name"], @@ -271,6 +272,8 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): args = [ openpype_app, + "module", + WebpublisherAddon.name, command, batch_dir ] From 808d1a5dd121d2f771a75d8ea4d061522ca42306 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:36:11 +0200 Subject: [PATCH 1005/1030] abstrac provides has log attribute --- .../sync_server/providers/abstract_provider.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/providers/abstract_provider.py b/openpype/modules/sync_server/providers/abstract_provider.py index 9c808dc80e..e11a8ba71e 100644 --- a/openpype/modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/sync_server/providers/abstract_provider.py @@ -10,6 +10,8 @@ class AbstractProvider: CODE = '' LABEL = '' + _log = None + def __init__(self, project_name, site_name, tree=None, presets=None): self.presets = None self.active = False @@ -19,6 +21,12 @@ class AbstractProvider: super(AbstractProvider, self).__init__() + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + @abc.abstractmethod def is_active(self): """ @@ -199,11 +207,11 @@ class AbstractProvider: path = anatomy.fill_root(path) except KeyError: msg = "Error in resolving local root from anatomy" - log.error(msg) + self.log.error(msg) raise ValueError(msg) except IndexError: msg = "Path {} contains unfillable placeholder" - log.error(msg) + self.log.error(msg) raise ValueError(msg) return path From 5631fb66a79fe64c38073217aebe32b1a0fa5c60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:36:31 +0200 Subject: [PATCH 1006/1030] use log attribute in provides --- .../modules/sync_server/providers/dropbox.py | 17 +++--- .../modules/sync_server/providers/gdrive.py | 53 +++++++++++-------- .../modules/sync_server/providers/sftp.py | 15 +++--- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/openpype/modules/sync_server/providers/dropbox.py b/openpype/modules/sync_server/providers/dropbox.py index 89d6990841..e026ae7ef6 100644 --- a/openpype/modules/sync_server/providers/dropbox.py +++ b/openpype/modules/sync_server/providers/dropbox.py @@ -2,12 +2,9 @@ import os import dropbox -from openpype.api import Logger from .abstract_provider import AbstractProvider from ..utils import EditableScopes -log = Logger().get_logger("SyncServer") - class DropboxHandler(AbstractProvider): CODE = 'dropbox' @@ -20,26 +17,26 @@ class DropboxHandler(AbstractProvider): self.dbx = None if not self.presets: - log.info( + self.log.info( "Sync Server: There are no presets for {}.".format(site_name) ) return if not self.presets["enabled"]: - log.debug("Sync Server: Site {} not enabled for {}.". + self.log.debug("Sync Server: Site {} not enabled for {}.". format(site_name, project_name)) return token = self.presets.get("token", "") if not token: msg = "Sync Server: No access token for dropbox provider" - log.info(msg) + self.log.info(msg) return team_folder_name = self.presets.get("team_folder_name", "") if not team_folder_name: msg = "Sync Server: No team folder name for dropbox provider" - log.info(msg) + self.log.info(msg) return acting_as_member = self.presets.get("acting_as_member", "") @@ -47,7 +44,7 @@ class DropboxHandler(AbstractProvider): msg = ( "Sync Server: No acting member for dropbox provider" ) - log.info(msg) + self.log.info(msg) return try: @@ -55,7 +52,7 @@ class DropboxHandler(AbstractProvider): token, acting_as_member, team_folder_name ) except Exception as e: - log.info("Could not establish dropbox object: {}".format(e)) + self.log.info("Could not establish dropbox object: {}".format(e)) return super(AbstractProvider, self).__init__() @@ -448,7 +445,7 @@ class DropboxHandler(AbstractProvider): path = anatomy.fill_root(path) except KeyError: msg = "Error in resolving local root from anatomy" - log.error(msg) + self.log.error(msg) raise ValueError(msg) return path diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index bef707788b..9a3ce89cf5 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -5,12 +5,12 @@ import sys import six import platform -from openpype.api import Logger -from openpype.api import get_system_settings +from openpype.lib import Logger +from openpype.settings import get_system_settings from .abstract_provider import AbstractProvider from ..utils import time_function, ResumableError -log = Logger().get_logger("SyncServer") +log = Logger.get_logger("GDriveHandler") try: from googleapiclient.discovery import build @@ -69,13 +69,17 @@ class GDriveHandler(AbstractProvider): self.presets = presets if not self.presets: - log.info("Sync Server: There are no presets for {}.". - format(site_name)) + self.log.info( + "Sync Server: There are no presets for {}.".format(site_name) + ) return if not self.presets["enabled"]: - log.debug("Sync Server: Site {} not enabled for {}.". - format(site_name, project_name)) + self.log.debug( + "Sync Server: Site {} not enabled for {}.".format( + site_name, project_name + ) + ) return current_platform = platform.system().lower() @@ -85,20 +89,22 @@ class GDriveHandler(AbstractProvider): if not cred_path: msg = "Sync Server: Please, fill the credentials for gdrive "\ "provider for platform '{}' !".format(current_platform) - log.info(msg) + self.log.info(msg) return try: cred_path = cred_path.format(**os.environ) except KeyError as e: - log.info("Sync Server: The key(s) {} does not exist in the " - "environment variables".format(" ".join(e.args))) + self.log.info(( + "Sync Server: The key(s) {} does not exist in the " + "environment variables" + ).format(" ".join(e.args))) return if not os.path.exists(cred_path): msg = "Sync Server: No credentials for gdrive provider " + \ "for '{}' on path '{}'!".format(site_name, cred_path) - log.info(msg) + self.log.info(msg) return self.service = None @@ -318,7 +324,7 @@ class GDriveHandler(AbstractProvider): fields='id') media.stream() - log.debug("Start Upload! {}".format(source_path)) + self.log.debug("Start Upload! {}".format(source_path)) last_tick = status = response = None status_val = 0 while response is None: @@ -331,7 +337,7 @@ class GDriveHandler(AbstractProvider): if not last_tick or \ time.time() - last_tick >= server.LOG_PROGRESS_SEC: last_tick = time.time() - log.debug("Uploaded %d%%." % + self.log.debug("Uploaded %d%%." % int(status_val * 100)) server.update_db(project_name=project_name, new_file_id=None, @@ -350,8 +356,9 @@ class GDriveHandler(AbstractProvider): if 'has not granted' in ex._get_reason().strip(): raise PermissionError(ex._get_reason().strip()) - log.warning("Forbidden received, hit quota. " - "Injecting 60s delay.") + self.log.warning( + "Forbidden received, hit quota. Injecting 60s delay." + ) time.sleep(60) return False raise @@ -417,7 +424,7 @@ class GDriveHandler(AbstractProvider): if not last_tick or \ time.time() - last_tick >= server.LOG_PROGRESS_SEC: last_tick = time.time() - log.debug("Downloaded %d%%." % + self.log.debug("Downloaded %d%%." % int(status_val * 100)) server.update_db(project_name=project_name, new_file_id=None, @@ -629,9 +636,9 @@ class GDriveHandler(AbstractProvider): ["gdrive"] ) except KeyError: - log.info(("Sync Server: There are no presets for Gdrive " + - "provider."). - format(str(provider_presets))) + log.info(( + "Sync Server: There are no presets for Gdrive provider." + ).format(str(provider_presets))) return return provider_presets @@ -704,7 +711,7 @@ class GDriveHandler(AbstractProvider): roots[self.MY_DRIVE_STR] = self.service.files() \ .get(fileId='root').execute() except errors.HttpError: - log.warning("HttpError in sync loop, " + self.log.warning("HttpError in sync loop, " "trying next loop", exc_info=True) raise ResumableError @@ -727,7 +734,7 @@ class GDriveHandler(AbstractProvider): Returns: (dictionary) path as a key, folder id as a value """ - log.debug("build_tree len {}".format(len(folders))) + self.log.debug("build_tree len {}".format(len(folders))) if not self.root: # build only when necessary, could be expensive self.root = self._prepare_root_info() @@ -779,9 +786,9 @@ class GDriveHandler(AbstractProvider): loop_cnt += 1 if len(no_parents_yet) > 0: - log.debug("Some folders path are not resolved {}". + self.log.debug("Some folders path are not resolved {}". format(no_parents_yet)) - log.debug("Remove deleted folders from trash.") + self.log.debug("Remove deleted folders from trash.") return tree diff --git a/openpype/modules/sync_server/providers/sftp.py b/openpype/modules/sync_server/providers/sftp.py index 302ffae3e6..40f11cb9dd 100644 --- a/openpype/modules/sync_server/providers/sftp.py +++ b/openpype/modules/sync_server/providers/sftp.py @@ -4,10 +4,10 @@ import time import threading import platform -from openpype.api import Logger -from openpype.api import get_system_settings +from openpype.lib import Logger +from openpype.settings import get_system_settings from .abstract_provider import AbstractProvider -log = Logger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer-SFTPHandler") pysftp = None try: @@ -43,8 +43,9 @@ class SFTPHandler(AbstractProvider): self.presets = presets if not self.presets: - log.warning("Sync Server: There are no presets for {}.". - format(site_name)) + self.log.warning( + "Sync Server: There are no presets for {}.".format(site_name) + ) return # store to instance for reconnect @@ -423,7 +424,7 @@ class SFTPHandler(AbstractProvider): return pysftp.Connection(**conn_params) except (paramiko.ssh_exception.SSHException, pysftp.exceptions.ConnectionException): - log.warning("Couldn't connect", exc_info=True) + self.log.warning("Couldn't connect", exc_info=True) def _mark_progress(self, project_name, file, representation, server, site, source_path, target_path, direction): @@ -445,7 +446,7 @@ class SFTPHandler(AbstractProvider): time.time() - last_tick >= server.LOG_PROGRESS_SEC: status_val = target_file_size / source_file_size last_tick = time.time() - log.debug(direction + "ed %d%%." % int(status_val * 100)) + self.log.debug(direction + "ed %d%%." % int(status_val * 100)) server.update_db(project_name=project_name, new_file_id=None, file=file, From 54b8719b76c98b30d30e81b828e2dfb9ce13d0a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:49:04 +0200 Subject: [PATCH 1007/1030] fix attr initialization --- openpype/modules/timers_manager/rest_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 6686407350..4a2e9e6575 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -10,7 +10,7 @@ class TimersManagerModuleRestApi: happens in Workfile app. """ def __init__(self, user_module, server_manager): - self.log = None + self._log = None self.module = user_module self.server_manager = server_manager From 59f36cc7c8ef54e3ac54d547e5f772bc726f3f1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Aug 2022 18:49:20 +0200 Subject: [PATCH 1008/1030] log traceback when webserver connection is not possible --- openpype/modules/webserver/webserver_module.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py index 686bd27bfd..16861abd29 100644 --- a/openpype/modules/webserver/webserver_module.py +++ b/openpype/modules/webserver/webserver_module.py @@ -53,9 +53,12 @@ class WebServerModule(OpenPypeModule, ITrayService): try: module.webserver_initialization(self.server_manager) except Exception: - self.log.warning(( - "Failed to connect module \"{}\" to webserver." - ).format(module.name)) + self.log.warning( + ( + "Failed to connect module \"{}\" to webserver." + ).format(module.name), + exc_info=True + ) def tray_init(self): self.create_server_manager() From 3ad9533fa82955301383c53e096d8fde2067c778 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Thu, 25 Aug 2022 20:10:27 +0200 Subject: [PATCH 1009/1030] workfile template also matches against os.environ --- openpype/pipeline/workfile/path_resolving.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index ed1d1d793e..4cd225a515 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -408,6 +408,9 @@ def get_custom_workfile_template( # add root dict anatomy_context_data["root"] = anatomy.roots + # extend anatomy context with os.environ + anatomy_context_data.update(os.environ) + # get task type for the task in context current_task_type = anatomy_context_data["task"]["type"] From bad5b9b194f498903900ee283ed5a4b14e25a198 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 10:04:17 +0200 Subject: [PATCH 1010/1030] fix import --- openpype/hosts/photoshop/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/__init__.py b/openpype/hosts/photoshop/__init__.py index b3f66ea35c..773f73d624 100644 --- a/openpype/hosts/photoshop/__init__.py +++ b/openpype/hosts/photoshop/__init__.py @@ -1,4 +1,4 @@ -from .module import ( +from .addon import ( PhotoshopAddon, PHOTOSHOP_HOST_DIR, ) From 45c112eb84ae741d4b102ea89ac5c64c01f591f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 10:12:35 +0200 Subject: [PATCH 1011/1030] fixed arguments --- openpype/hosts/webpublisher/addon.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 85e16de4a6..7d26d5a7ff 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -57,7 +57,7 @@ def cli_main(): @click.option("-t", "--targets", help="Targets", default=None, multiple=True) def publish(project, path, user=None, targets=None): - """Start CLI publishing. + """Start publishing (Inner command). Publish collects json from paths provided as an argument. More than one path is allowed. @@ -70,13 +70,13 @@ def publish(project, path, user=None, targets=None): @cli_main.command() @click.argument("path") +@click.option("-p", "--project", help="Project") @click.option("-h", "--host", help="Host") @click.option("-u", "--user", help="User email address") -@click.option("-p", "--project", help="Project") @click.option("-t", "--targets", help="Targets", default=None, multiple=True) -def publishfromapp(project, path, user=None, targets=None): - """Start CLI publishing. +def publishfromapp(project, path, host, user=None, targets=None): + """Start publishing through application (Inner command). Publish collects json from paths provided as an argument. More than one path is allowed. @@ -84,16 +84,16 @@ def publishfromapp(project, path, user=None, targets=None): from .publish_functions import cli_publish_from_app - cli_publish_from_app(project, path, user, targets) + cli_publish_from_app(project, path, host, user, targets) @cli_main.command() -@click.option("-h", "--host", help="Host", default=None) -@click.option("-p", "--port", help="Port", default=None) @click.option("-e", "--executable", help="Executable") @click.option("-u", "--upload_dir", help="Upload dir") +@click.option("-h", "--host", help="Host", default=None) +@click.option("-p", "--port", help="Port", default=None) def webserver(executable, upload_dir, host=None, port=None): - """Starts webserver for communication with Webpublish FR via command line + """Start service for communication with Webpublish Front end. OP must be congigured on a machine, eg. OPENPYPE_MONGO filled AND FTRACK_BOT_API_KEY provided with api key from Ftrack. From 380965927ad4aa58672008588940c455f02d08cc Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 12:13:29 +0200 Subject: [PATCH 1012/1030] reversed dict merging, anatomy has precedence. --- openpype/pipeline/workfile/path_resolving.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 4cd225a515..97e00d807c 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -409,10 +409,11 @@ def get_custom_workfile_template( anatomy_context_data["root"] = anatomy.roots # extend anatomy context with os.environ - anatomy_context_data.update(os.environ) + full_context_data = os.environ + full_context_data.update(anatomy_context_data) # get task type for the task in context - current_task_type = anatomy_context_data["task"]["type"] + current_task_type = full_context_data["task"]["type"] # get path from matching profile matching_item = filter_profiles( @@ -424,7 +425,7 @@ def get_custom_workfile_template( if matching_item: template = matching_item["path"][platform.system().lower()] return StringTemplate.format_strict_template( - template, anatomy_context_data + template, full_context_data ).normalized() return None From 2d9f2a6e767f340589c0f1955904a2b6762e178a Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 14:46:03 +0200 Subject: [PATCH 1013/1030] os.environ is now a copy not an instance --- openpype/pipeline/workfile/path_resolving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 97e00d807c..4ab4a4936c 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -409,7 +409,7 @@ def get_custom_workfile_template( anatomy_context_data["root"] = anatomy.roots # extend anatomy context with os.environ - full_context_data = os.environ + full_context_data = os.environ.copy() full_context_data.update(anatomy_context_data) # get task type for the task in context From 2bfa9eea445a37e830db4dda036f5f8f18168573 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:06:50 +0200 Subject: [PATCH 1014/1030] renamed 'IHostModule' to 'IHostAddon' --- openpype/hosts/aftereffects/module.py | 4 ++-- openpype/hosts/blender/module.py | 4 ++-- openpype/hosts/harmony/addon.py | 4 ++-- openpype/hosts/hiero/module.py | 4 ++-- openpype/hosts/maya/module.py | 4 ++-- openpype/hosts/nuke/module.py | 4 ++-- openpype/hosts/photoshop/addon.py | 4 ++-- .../standalonepublisher/standalonepublish_module.py | 4 ++-- openpype/hosts/traypublisher/module.py | 4 ++-- openpype/hosts/tvpaint/tvpaint_module.py | 4 ++-- openpype/hosts/unreal/module.py | 4 ++-- openpype/hosts/webpublisher/addon.py | 4 ++-- openpype/modules/base.py | 10 +++++----- openpype/modules/interfaces.py | 4 ++-- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/aftereffects/module.py b/openpype/hosts/aftereffects/module.py index 93d575c186..dff9634ecf 100644 --- a/openpype/hosts/aftereffects/module.py +++ b/openpype/hosts/aftereffects/module.py @@ -1,8 +1,8 @@ from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon -class AfterEffectsModule(OpenPypeModule, IHostModule): +class AfterEffectsModule(OpenPypeModule, IHostAddon): name = "aftereffects" host_name = "aftereffects" diff --git a/openpype/hosts/blender/module.py b/openpype/hosts/blender/module.py index d6ff3b111c..3db7973c17 100644 --- a/openpype/hosts/blender/module.py +++ b/openpype/hosts/blender/module.py @@ -1,11 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class BlenderModule(OpenPypeModule, IHostModule): +class BlenderModule(OpenPypeModule, IHostAddon): name = "blender" host_name = "blender" diff --git a/openpype/hosts/harmony/addon.py b/openpype/hosts/harmony/addon.py index b051d68abb..872a7490b5 100644 --- a/openpype/hosts/harmony/addon.py +++ b/openpype/hosts/harmony/addon.py @@ -1,11 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) -class HarmonyAddon(OpenPypeModule, IHostModule): +class HarmonyAddon(OpenPypeModule, IHostAddon): name = "harmony" host_name = "harmony" diff --git a/openpype/hosts/hiero/module.py b/openpype/hosts/hiero/module.py index 375486e034..7883d2255f 100644 --- a/openpype/hosts/hiero/module.py +++ b/openpype/hosts/hiero/module.py @@ -1,12 +1,12 @@ import os import platform from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class HieroModule(OpenPypeModule, IHostModule): +class HieroModule(OpenPypeModule, IHostAddon): name = "hiero" host_name = "hiero" diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 5a215be8d2..674b36b250 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -1,11 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class OpenPypeMaya(OpenPypeModule, IHostModule): +class OpenPypeMaya(OpenPypeModule, IHostAddon): name = "openpype_maya" host_name = "maya" diff --git a/openpype/hosts/nuke/module.py b/openpype/hosts/nuke/module.py index e4706a36cb..444aa75ff2 100644 --- a/openpype/hosts/nuke/module.py +++ b/openpype/hosts/nuke/module.py @@ -1,12 +1,12 @@ import os import platform from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class NukeModule(OpenPypeModule, IHostModule): +class NukeModule(OpenPypeModule, IHostAddon): name = "nuke" host_name = "nuke" diff --git a/openpype/hosts/photoshop/addon.py b/openpype/hosts/photoshop/addon.py index 18899d4de8..a41d91554b 100644 --- a/openpype/hosts/photoshop/addon.py +++ b/openpype/hosts/photoshop/addon.py @@ -1,11 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) -class PhotoshopAddon(OpenPypeModule, IHostModule): +class PhotoshopAddon(OpenPypeModule, IHostAddon): name = "photoshop" host_name = "photoshop" diff --git a/openpype/hosts/standalonepublisher/standalonepublish_module.py b/openpype/hosts/standalonepublisher/standalonepublish_module.py index bf8e1d2c23..21b47beb54 100644 --- a/openpype/hosts/standalonepublisher/standalonepublish_module.py +++ b/openpype/hosts/standalonepublisher/standalonepublish_module.py @@ -5,12 +5,12 @@ import click from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import ITrayAction, IHostModule +from openpype.modules.interfaces import ITrayAction, IHostAddon STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostModule): +class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostAddon): label = "Publish" name = "standalonepublish_tool" host_name = "standalonepublisher" diff --git a/openpype/hosts/traypublisher/module.py b/openpype/hosts/traypublisher/module.py index 92a2312fec..c35ce2093a 100644 --- a/openpype/hosts/traypublisher/module.py +++ b/openpype/hosts/traypublisher/module.py @@ -5,12 +5,12 @@ import click from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import ITrayAction, IHostModule +from openpype.modules.interfaces import ITrayAction, IHostAddon TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class TrayPublishModule(OpenPypeModule, IHostModule, ITrayAction): +class TrayPublishModule(OpenPypeModule, IHostAddon, ITrayAction): label = "New Publish (beta)" name = "traypublish_tool" host_name = "traypublish" diff --git a/openpype/hosts/tvpaint/tvpaint_module.py b/openpype/hosts/tvpaint/tvpaint_module.py index a004359231..4b30ce667c 100644 --- a/openpype/hosts/tvpaint/tvpaint_module.py +++ b/openpype/hosts/tvpaint/tvpaint_module.py @@ -1,6 +1,6 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon TVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -13,7 +13,7 @@ def get_launch_script_path(): ) -class TVPaintModule(OpenPypeModule, IHostModule): +class TVPaintModule(OpenPypeModule, IHostAddon): name = "tvpaint" host_name = "tvpaint" diff --git a/openpype/hosts/unreal/module.py b/openpype/hosts/unreal/module.py index aa08c8c130..99c8851e8e 100644 --- a/openpype/hosts/unreal/module.py +++ b/openpype/hosts/unreal/module.py @@ -1,11 +1,11 @@ import os from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class UnrealModule(OpenPypeModule, IHostModule): +class UnrealModule(OpenPypeModule, IHostAddon): name = "unreal" host_name = "unreal" diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index 7d26d5a7ff..a64d74e62b 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -3,12 +3,12 @@ import os import click from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostModule +from openpype.modules.interfaces import IHostAddon WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class WebpublisherAddon(OpenPypeModule, IHostModule): +class WebpublisherAddon(OpenPypeModule, IHostAddon): name = "webpublisher" host_name = "webpublisher" diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 6db6ee9524..c96ca02ab7 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -35,7 +35,7 @@ from openpype.lib import ( from .interfaces import ( OpenPypeInterface, IPluginPaths, - IHostModule, + IHostAddon, ITrayModule, ITrayService ) @@ -811,13 +811,13 @@ class ModulesManager: Returns: OpenPypeModule: Found host module by name. - None: There was not found module inheriting IHostModule which has + None: There was not found module inheriting IHostAddon which has host name set to passed 'host_name'. """ for module in self.get_enabled_modules(): if ( - isinstance(module, IHostModule) + isinstance(module, IHostAddon) and module.host_name == host_name ): return module @@ -828,13 +828,13 @@ class ModulesManager: Returns: Iterable[str]: All available host names based on enabled modules - inheriting 'IHostModule'. + inheriting 'IHostAddon'. """ host_names = { module.host_name for module in self.get_enabled_modules() - if isinstance(module, IHostModule) + if isinstance(module, IHostAddon) } return host_names diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 13655773dd..f92ec6bf2d 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -385,8 +385,8 @@ class ISettingsChangeListener(OpenPypeInterface): pass -class IHostModule(OpenPypeInterface): - """Module which also contain a host implementation.""" +class IHostAddon(OpenPypeInterface): + """Addon which also contain a host implementation.""" @abstractproperty def host_name(self): From 2c81bb5788db784073eec6a61755c288f4dd41d6 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 26 Aug 2022 15:09:47 +0200 Subject: [PATCH 1015/1030] moved env logic inside matching check --- openpype/pipeline/workfile/path_resolving.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 4ab4a4936c..6d9e72dbd2 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -408,12 +408,8 @@ def get_custom_workfile_template( # add root dict anatomy_context_data["root"] = anatomy.roots - # extend anatomy context with os.environ - full_context_data = os.environ.copy() - full_context_data.update(anatomy_context_data) - # get task type for the task in context - current_task_type = full_context_data["task"]["type"] + current_task_type = anatomy_context_data["task"]["type"] # get path from matching profile matching_item = filter_profiles( @@ -423,6 +419,11 @@ def get_custom_workfile_template( # when path is available try to format it in case # there are some anatomy template strings if matching_item: + # extend anatomy context with os.environ to + # also allow formatting against env + full_context_data = os.environ.copy() + full_context_data.update(anatomy_context_data) + template = matching_item["path"][platform.system().lower()] return StringTemplate.format_strict_template( template, full_context_data From 0212f6fc06554945be11ecb02810f93c4103b620 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:11:14 +0200 Subject: [PATCH 1016/1030] renamed module.py to addon.py in unreal --- openpype/hosts/unreal/__init__.py | 4 ++-- openpype/hosts/unreal/{module.py => addon.py} | 4 ++-- openpype/hosts/unreal/lib.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename openpype/hosts/unreal/{module.py => addon.py} (92%) diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 41222f4f94..42dd8f0ac4 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -1,6 +1,6 @@ -from .module import UnrealModule +from .addon import UnrealAddon __all__ = ( - "UnrealModule", + "UnrealAddon", ) diff --git a/openpype/hosts/unreal/module.py b/openpype/hosts/unreal/addon.py similarity index 92% rename from openpype/hosts/unreal/module.py rename to openpype/hosts/unreal/addon.py index 99c8851e8e..16736214c5 100644 --- a/openpype/hosts/unreal/module.py +++ b/openpype/hosts/unreal/addon.py @@ -5,14 +5,14 @@ from openpype.modules.interfaces import IHostAddon UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class UnrealModule(OpenPypeModule, IHostAddon): +class UnrealAddon(OpenPypeModule, IHostAddon): name = "unreal" host_name = "unreal" def initialize(self, module_settings): self.enabled = True - def add_implementation_envs(self, env, app) -> None: + def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 8c453b38b9..d02c6de357 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Unreal launching and project tools.""" -import sys + import os import platform import json @@ -9,7 +9,7 @@ import subprocess import re from pathlib import Path from collections import OrderedDict -from openpype.api import get_project_settings +from openpype.settings import get_project_settings def get_engine_versions(env=None): From f0ddc5b746cfb9be546860cc99d1121bf9f21b61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:11:22 +0200 Subject: [PATCH 1017/1030] renamed 'tvpaint_module.py' to 'addon.py' --- openpype/hosts/tvpaint/__init__.py | 6 +++--- openpype/hosts/tvpaint/{tvpaint_module.py => addon.py} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename openpype/hosts/tvpaint/{tvpaint_module.py => addon.py} (95%) diff --git a/openpype/hosts/tvpaint/__init__.py b/openpype/hosts/tvpaint/__init__.py index 0a84b575dc..b98680f204 100644 --- a/openpype/hosts/tvpaint/__init__.py +++ b/openpype/hosts/tvpaint/__init__.py @@ -1,12 +1,12 @@ -from .tvpaint_module import ( +from .addon import ( get_launch_script_path, - TVPaintModule, + TVPaintAddon, TVPAINT_ROOT_DIR, ) __all__ = ( "get_launch_script_path", - "TVPaintModule", + "TVPaintAddon", "TVPAINT_ROOT_DIR", ) diff --git a/openpype/hosts/tvpaint/tvpaint_module.py b/openpype/hosts/tvpaint/addon.py similarity index 95% rename from openpype/hosts/tvpaint/tvpaint_module.py rename to openpype/hosts/tvpaint/addon.py index 4b30ce667c..d710e63f93 100644 --- a/openpype/hosts/tvpaint/tvpaint_module.py +++ b/openpype/hosts/tvpaint/addon.py @@ -13,7 +13,7 @@ def get_launch_script_path(): ) -class TVPaintModule(OpenPypeModule, IHostAddon): +class TVPaintAddon(OpenPypeModule, IHostAddon): name = "tvpaint" host_name = "tvpaint" From 3e912a88f6367f7488f90648b223abcd501e8d86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:12:19 +0200 Subject: [PATCH 1018/1030] renamed module.py to addon.py, changed addon name and host name --- openpype/hosts/traypublisher/__init__.py | 4 ++-- openpype/hosts/traypublisher/{module.py => addon.py} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename openpype/hosts/traypublisher/{module.py => addon.py} (86%) diff --git a/openpype/hosts/traypublisher/__init__.py b/openpype/hosts/traypublisher/__init__.py index 4eb7bf3eef..77ba908ddd 100644 --- a/openpype/hosts/traypublisher/__init__.py +++ b/openpype/hosts/traypublisher/__init__.py @@ -1,6 +1,6 @@ -from .module import TrayPublishModule +from .addon import TrayPublishAddon __all__ = ( - "TrayPublishModule", + "TrayPublishAddon", ) diff --git a/openpype/hosts/traypublisher/module.py b/openpype/hosts/traypublisher/addon.py similarity index 86% rename from openpype/hosts/traypublisher/module.py rename to openpype/hosts/traypublisher/addon.py index c35ce2093a..c86c835ed9 100644 --- a/openpype/hosts/traypublisher/module.py +++ b/openpype/hosts/traypublisher/addon.py @@ -10,10 +10,10 @@ from openpype.modules.interfaces import ITrayAction, IHostAddon TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class TrayPublishModule(OpenPypeModule, IHostAddon, ITrayAction): +class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction): label = "New Publish (beta)" - name = "traypublish_tool" - host_name = "traypublish" + name = "traypublisher" + host_name = "traypublisher" def initialize(self, modules_settings): self.enabled = True @@ -28,7 +28,7 @@ class TrayPublishModule(OpenPypeModule, IHostAddon, ITrayAction): self._experimental_tools = ExperimentalTools() def tray_menu(self, *args, **kwargs): - super(TrayPublishModule, self).tray_menu(*args, **kwargs) + super(TrayPublishAddon, self).tray_menu(*args, **kwargs) traypublisher = self._experimental_tools.get("traypublisher") visible = False if traypublisher and traypublisher.enabled: @@ -53,7 +53,7 @@ class TrayPublishModule(OpenPypeModule, IHostAddon, ITrayAction): click_group.add_command(cli_main) -@click.group(TrayPublishModule.name, help="TrayPublisher related commands.") +@click.group(TrayPublishAddon.name, help="TrayPublisher related commands.") def cli_main(): pass From 4a35b4bea7610bedfd780882bde2a0216a3bde76 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:13:16 +0200 Subject: [PATCH 1019/1030] renamed standalonepublisher to addon --- openpype/hosts/standalonepublisher/__init__.py | 4 ++-- .../{standalonepublish_module.py => addon.py} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename openpype/hosts/standalonepublisher/{standalonepublish_module.py => addon.py} (90%) diff --git a/openpype/hosts/standalonepublisher/__init__.py b/openpype/hosts/standalonepublisher/__init__.py index 394d5be397..f47fa6b573 100644 --- a/openpype/hosts/standalonepublisher/__init__.py +++ b/openpype/hosts/standalonepublisher/__init__.py @@ -1,6 +1,6 @@ -from .standalonepublish_module import StandAlonePublishModule +from .addon import StandAlonePublishAddon __all__ = ( - "StandAlonePublishModule", + "StandAlonePublishAddon", ) diff --git a/openpype/hosts/standalonepublisher/standalonepublish_module.py b/openpype/hosts/standalonepublisher/addon.py similarity index 90% rename from openpype/hosts/standalonepublisher/standalonepublish_module.py rename to openpype/hosts/standalonepublisher/addon.py index 21b47beb54..40a156ee70 100644 --- a/openpype/hosts/standalonepublisher/standalonepublish_module.py +++ b/openpype/hosts/standalonepublisher/addon.py @@ -10,9 +10,9 @@ from openpype.modules.interfaces import ITrayAction, IHostAddon STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostAddon): +class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon): label = "Publish" - name = "standalonepublish_tool" + name = "standalonepublisher" host_name = "standalonepublisher" def initialize(self, modules_settings): @@ -42,7 +42,7 @@ class StandAlonePublishModule(OpenPypeModule, ITrayAction, IHostAddon): @click.group( - StandAlonePublishModule.name, + StandAlonePublishAddon.name, help="StandalonePublisher related commands.") def cli_main(): pass From 7af8e8998465b33841440677a16bfe9ef870e9dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:14:05 +0200 Subject: [PATCH 1020/1030] renamed maya to addon --- openpype/hosts/maya/__init__.py | 4 ++-- openpype/hosts/maya/{module.py => addon.py} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename openpype/hosts/maya/{module.py => addon.py} (94%) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 72b4d5853c..860db766f3 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,6 +1,6 @@ -from .module import OpenPypeMaya +from .addon import MayaAddon __all__ = ( - "OpenPypeMaya", + "MayaAddon", ) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/addon.py similarity index 94% rename from openpype/hosts/maya/module.py rename to openpype/hosts/maya/addon.py index 674b36b250..7b1f7bf754 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/addon.py @@ -5,8 +5,8 @@ from openpype.modules.interfaces import IHostAddon MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class OpenPypeMaya(OpenPypeModule, IHostAddon): - name = "openpype_maya" +class MayaAddon(OpenPypeModule, IHostAddon): + name = "maya" host_name = "maya" def initialize(self, module_settings): From 621b2dbe882ea97c55d1910735a27e508fab303a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:14:33 +0200 Subject: [PATCH 1021/1030] renamed hiero to addon --- openpype/hosts/hiero/__init__.py | 6 +++--- openpype/hosts/hiero/{module.py => addon.py} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename openpype/hosts/hiero/{module.py => addon.py} (97%) diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index a307e265d5..e6744d5aec 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -1,10 +1,10 @@ -from .module import ( +from .addon import ( HIERO_ROOT_DIR, - HieroModule, + HieroAddon, ) __all__ = ( "HIERO_ROOT_DIR", - "HieroModule", + "HieroAddon", ) diff --git a/openpype/hosts/hiero/module.py b/openpype/hosts/hiero/addon.py similarity index 97% rename from openpype/hosts/hiero/module.py rename to openpype/hosts/hiero/addon.py index 7883d2255f..3523e9aed7 100644 --- a/openpype/hosts/hiero/module.py +++ b/openpype/hosts/hiero/addon.py @@ -6,7 +6,7 @@ from openpype.modules.interfaces import IHostAddon HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class HieroModule(OpenPypeModule, IHostAddon): +class HieroAddon(OpenPypeModule, IHostAddon): name = "hiero" host_name = "hiero" From a991d4b6fe008db77ff4b83c18a14a7f9f95a07f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:15:14 +0200 Subject: [PATCH 1022/1030] renamed blender to addon --- openpype/hosts/blender/__init__.py | 4 ++-- openpype/hosts/blender/{module.py => addon.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename openpype/hosts/blender/{module.py => addon.py} (98%) diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 58d7ac656f..2a6603606a 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -1,6 +1,6 @@ -from .module import BlenderModule +from .addon import BlenderAddon __all__ = ( - "BlenderModule", + "BlenderAddon", ) diff --git a/openpype/hosts/blender/module.py b/openpype/hosts/blender/addon.py similarity index 98% rename from openpype/hosts/blender/module.py rename to openpype/hosts/blender/addon.py index 3db7973c17..3ee638a5bb 100644 --- a/openpype/hosts/blender/module.py +++ b/openpype/hosts/blender/addon.py @@ -5,7 +5,7 @@ from openpype.modules.interfaces import IHostAddon BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class BlenderModule(OpenPypeModule, IHostAddon): +class BlenderAddon(OpenPypeModule, IHostAddon): name = "blender" host_name = "blender" From 3d7b2179c855d466bebb580cb8082084bb2ab44b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:15:42 +0200 Subject: [PATCH 1023/1030] renamed aftereffects to addon --- openpype/hosts/aftereffects/__init__.py | 4 ++-- openpype/hosts/aftereffects/{module.py => addon.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename openpype/hosts/aftereffects/{module.py => addon.py} (92%) diff --git a/openpype/hosts/aftereffects/__init__.py b/openpype/hosts/aftereffects/__init__.py index c9ad6aaeeb..ae750d05b6 100644 --- a/openpype/hosts/aftereffects/__init__.py +++ b/openpype/hosts/aftereffects/__init__.py @@ -1,6 +1,6 @@ -from .module import AfterEffectsModule +from .addon import AfterEffectsAddon __all__ = ( - "AfterEffectsModule", + "AfterEffectsAddon", ) diff --git a/openpype/hosts/aftereffects/module.py b/openpype/hosts/aftereffects/addon.py similarity index 92% rename from openpype/hosts/aftereffects/module.py rename to openpype/hosts/aftereffects/addon.py index dff9634ecf..94843e7dc5 100644 --- a/openpype/hosts/aftereffects/module.py +++ b/openpype/hosts/aftereffects/addon.py @@ -2,7 +2,7 @@ from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostAddon -class AfterEffectsModule(OpenPypeModule, IHostAddon): +class AfterEffectsAddon(OpenPypeModule, IHostAddon): name = "aftereffects" host_name = "aftereffects" From 511bf71f61426a4794ca9bc9890b49d82ce7917e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:17:28 +0200 Subject: [PATCH 1024/1030] fix standalone publisher settings --- openpype/hosts/standalonepublisher/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py index 40a156ee70..98ec44d4e2 100644 --- a/openpype/hosts/standalonepublisher/addon.py +++ b/openpype/hosts/standalonepublisher/addon.py @@ -16,7 +16,7 @@ class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon): host_name = "standalonepublisher" def initialize(self, modules_settings): - self.enabled = modules_settings[self.name]["enabled"] + self.enabled = modules_settings["standalonepublish_tool"]["enabled"] self.publish_paths = [ os.path.join(STANDALONEPUBLISH_ROOT_DIR, "plugins", "publish") ] From 310bc6b70523e1e24b9060d5b3163678dbca3417 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 15:17:51 +0200 Subject: [PATCH 1025/1030] renamed nuke to addon --- openpype/hosts/nuke/__init__.py | 6 +++--- openpype/hosts/nuke/{module.py => addon.py} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename openpype/hosts/nuke/{module.py => addon.py} (97%) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index 718307583e..8ab565939b 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -1,10 +1,10 @@ -from .module import ( +from .addon import ( NUKE_ROOT_DIR, - NukeModule, + NukeAddon, ) __all__ = ( "NUKE_ROOT_DIR", - "NukeModule", + "NukeAddon", ) diff --git a/openpype/hosts/nuke/module.py b/openpype/hosts/nuke/addon.py similarity index 97% rename from openpype/hosts/nuke/module.py rename to openpype/hosts/nuke/addon.py index 444aa75ff2..54e4da5195 100644 --- a/openpype/hosts/nuke/module.py +++ b/openpype/hosts/nuke/addon.py @@ -6,7 +6,7 @@ from openpype.modules.interfaces import IHostAddon NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -class NukeModule(OpenPypeModule, IHostAddon): +class NukeAddon(OpenPypeModule, IHostAddon): name = "nuke" host_name = "nuke" From 6801a719d33378e1729cc9022d6e924e7778b924 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 16:06:59 +0200 Subject: [PATCH 1026/1030] added method to get last available variant in application manager --- openpype/lib/applications.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index eaa4c1a0a8..e249ae4f1c 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -469,6 +469,19 @@ class ApplicationManager: for tool in group: self.tools[tool.full_name] = tool + def find_latest_available_variant_for_group(self, group_name): + group = self.app_groups.get(group_name) + if group is None or not group.enabled: + return None + + output = None + for _, variant in reversed(sorted(group.variants.items())): + executable = variant.find_executable() + if executable: + output = variant + break + return output + def launch(self, app_name, **data): """Launch procedure. From 5736fa2382e7d2cb63e3a007c3b04bc2805243bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 16:07:16 +0200 Subject: [PATCH 1027/1030] fix testing classes --- tests/lib/testing_classes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 2b4d7deb48..64676f62f4 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -12,8 +12,6 @@ import platform from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler -from openpype.lib.remote_publish import find_variant_key - class BaseTest: """Empty base test class""" @@ -210,7 +208,10 @@ class PublishTest(ModuleUnitTest): application_manager = ApplicationManager() if not app_variant: - app_variant = find_variant_key(application_manager, self.APP) + variant = ( + application_manager.find_latest_available_variant_for_group( + self.APP)) + app_variant = variant.name yield "{}/{}".format(self.APP, app_variant) @@ -342,4 +343,4 @@ class HostFixtures(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """"Adds init scripts (like userSetup) to expected location""" - raise NotImplementedError \ No newline at end of file + raise NotImplementedError From a0c3eb6f809e6aa6c7ee0c43f330a67e2466594b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 16:32:34 +0200 Subject: [PATCH 1028/1030] removed unnecessary lines --- openpype/modules/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c96ca02ab7..09aea50424 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -397,9 +397,6 @@ def _load_modules(): log.error(msg, exc_info=True) - - - @six.add_metaclass(ABCMeta) class OpenPypeModule: """Base class of pype module. From 4fda8d6ff2602421bcc2eacd74d4ff8b50525d4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Aug 2022 16:34:36 +0200 Subject: [PATCH 1029/1030] fix addon name --- openpype/tools/standalonepublish/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 3ceeb3ad48..081235c91c 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -236,7 +236,7 @@ def main(): signal.signal(signal.SIGTERM, signal_handler) modules_manager = ModulesManager() - module = modules_manager.modules_by_name["standalonepublish_tool"] + module = modules_manager.modules_by_name["standalonepublisher"] window = Window(module.publish_paths) window.show() From 55849039b554bfab52eefaa81906dba9a6d02d06 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 27 Aug 2022 04:12:30 +0000 Subject: [PATCH 1030/1030] [Automated] Bump version --- CHANGELOG.md | 55 ++++++++++++++++++++++++--------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a45f65b6f7..2a8e962085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.14.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.0...HEAD) @@ -9,22 +9,49 @@ - Documentation: Few updates [\#3698](https://github.com/pypeclub/OpenPype/pull/3698) - Documentation: Settings development [\#3660](https://github.com/pypeclub/OpenPype/pull/3660) +**🆕 New features** + +- Webpublisher:change create flatten image into tri state [\#3678](https://github.com/pypeclub/OpenPype/pull/3678) + **🚀 Enhancements** +- Settings: Remove settings lock on tray exit [\#3720](https://github.com/pypeclub/OpenPype/pull/3720) +- General: Added helper getters to modules manager [\#3712](https://github.com/pypeclub/OpenPype/pull/3712) - Unreal: Define unreal as module and use host class [\#3701](https://github.com/pypeclub/OpenPype/pull/3701) - Settings: Lock settings UI session [\#3700](https://github.com/pypeclub/OpenPype/pull/3700) +- General: Benevolent context label collector [\#3686](https://github.com/pypeclub/OpenPype/pull/3686) +- Ftrack: Store ftrack entities on hierarchy integration to instances [\#3677](https://github.com/pypeclub/OpenPype/pull/3677) - Ftrack: More logs related to auto sync value change [\#3671](https://github.com/pypeclub/OpenPype/pull/3671) +- Blender: ops refresh manager after process events [\#3663](https://github.com/pypeclub/OpenPype/pull/3663) **🐛 Bug fixes** +- General: Logger tweaks [\#3741](https://github.com/pypeclub/OpenPype/pull/3741) +- Nuke: color-space settings from anatomy is working [\#3721](https://github.com/pypeclub/OpenPype/pull/3721) +- Settings: Fix studio default anatomy save [\#3716](https://github.com/pypeclub/OpenPype/pull/3716) +- Maya: Use project name instead of project code [\#3709](https://github.com/pypeclub/OpenPype/pull/3709) - Settings: Fix project overrides save [\#3708](https://github.com/pypeclub/OpenPype/pull/3708) - Workfiles tool: Fix published workfile filtering [\#3704](https://github.com/pypeclub/OpenPype/pull/3704) - PS, AE: Provide default variant value for workfile subset [\#3703](https://github.com/pypeclub/OpenPype/pull/3703) - RoyalRender: handle host name that is not set [\#3695](https://github.com/pypeclub/OpenPype/pull/3695) - Flame: retime is working on clip publishing [\#3684](https://github.com/pypeclub/OpenPype/pull/3684) +- Webpublisher: added check for empty context [\#3682](https://github.com/pypeclub/OpenPype/pull/3682) **🔀 Refactored code** +- General: Host addons cleanup [\#3744](https://github.com/pypeclub/OpenPype/pull/3744) +- Webpublisher: Webpublisher is used as addon [\#3740](https://github.com/pypeclub/OpenPype/pull/3740) +- Photoshop: Defined photoshop as addon [\#3736](https://github.com/pypeclub/OpenPype/pull/3736) +- Harmony: Defined harmony as addon [\#3734](https://github.com/pypeclub/OpenPype/pull/3734) +- General: Module interfaces cleanup [\#3731](https://github.com/pypeclub/OpenPype/pull/3731) +- AfterEffects: Move AE functions from general lib [\#3730](https://github.com/pypeclub/OpenPype/pull/3730) +- Blender: Define blender as module [\#3729](https://github.com/pypeclub/OpenPype/pull/3729) +- AfterEffects: Define AfterEffects as module [\#3728](https://github.com/pypeclub/OpenPype/pull/3728) +- General: Replace PypeLogger with Logger [\#3725](https://github.com/pypeclub/OpenPype/pull/3725) +- Nuke: Define nuke as module [\#3724](https://github.com/pypeclub/OpenPype/pull/3724) +- General: Move subset name functionality [\#3723](https://github.com/pypeclub/OpenPype/pull/3723) +- General: Move creators plugin getter [\#3714](https://github.com/pypeclub/OpenPype/pull/3714) +- General: Move constants from lib to client [\#3713](https://github.com/pypeclub/OpenPype/pull/3713) - Loader: Subset groups using client operations [\#3710](https://github.com/pypeclub/OpenPype/pull/3710) - TVPaint: Defined as module [\#3707](https://github.com/pypeclub/OpenPype/pull/3707) - StandalonePublisher: Define StandalonePublisher as module [\#3706](https://github.com/pypeclub/OpenPype/pull/3706) @@ -33,6 +60,7 @@ **Merged pull requests:** +- Hiero: Define hiero as module [\#3717](https://github.com/pypeclub/OpenPype/pull/3717) - Deadline: better logging for DL webservice failures [\#3694](https://github.com/pypeclub/OpenPype/pull/3694) - Photoshop: resize saved images in ExtractReview for ffmpeg [\#3676](https://github.com/pypeclub/OpenPype/pull/3676) @@ -40,10 +68,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.0-nightly.1...3.14.0) -**🆕 New features** - -- Maya: Build workfile by template [\#3578](https://github.com/pypeclub/OpenPype/pull/3578) - **🚀 Enhancements** - Ftrack: Addiotional component metadata [\#3685](https://github.com/pypeclub/OpenPype/pull/3685) @@ -69,7 +93,6 @@ - Maya: Hosts as modules [\#3647](https://github.com/pypeclub/OpenPype/pull/3647) - 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) -- General: Workfiles builder using query functions [\#3598](https://github.com/pypeclub/OpenPype/pull/3598) **Merged pull requests:** @@ -91,12 +114,6 @@ - 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** @@ -109,35 +126,21 @@ - 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) -- 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) -- Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) **🔀 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:** - 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) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.2-nightly.4...3.12.2) -**🐛 Bug fixes** - -- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - ## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.1-nightly.6...3.12.1) diff --git a/openpype/version.py b/openpype/version.py index e738689c20..7894bb8bf4 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.1-nightly.2" +__version__ = "3.14.1-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index bfc570f597..75e4721d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.14.1-nightly.2" # OpenPype +version = "3.14.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 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 literal 80331 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 z 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 CA{hw) diff --git a/website/static/img/methodmadness.png b/website/static/img/methodmadness.png new file mode 100644 index 0000000000000000000000000000000000000000..9dd0681d4a029b11c8db74154934333db42e1851 GIT binary patch literal 8650 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 literal 0 HcmV?d00001 diff --git a/website/static/img/noghost.png b/website/static/img/noghost.png new file mode 100644 index 0000000000000000000000000000000000000000..febaedcae8212926ae8d7eb3705c8304531f8375 GIT binary patch literal 22435 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