From 12766377b43e9391509e52af68a95d1121411a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 14 Feb 2022 18:34:36 +0100 Subject: [PATCH 001/337] fix case with single mesh and prefixes --- .../create/create_unreal_staticmesh.py | 1 + .../hosts/maya/plugins/load/load_reference.py | 3 ++- .../publish/collect_unreal_staticmesh.py | 17 ++++++++++-- .../publish/extract_unreal_staticmesh.py | 27 ++++++++++++++----- .../validate_unreal_staticmesh_naming.py | 9 ++++++- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 9ad560ab7c..1fe7e57abc 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -33,6 +33,7 @@ class CreateUnrealStaticMesh(plugin.Creator): def process(self): with lib.undo_chunk(): + self.name = "{}_{}".format(self.family, self.name) instance = super(CreateUnrealStaticMesh, self).process() content = cmds.sets(instance, query=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0565b0b95c..7cdd91a7ea 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -20,7 +20,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "camera", "rig", "camerarig", - "xgen"] + "xgen", + "unrealStaticMesh"] representations = ["ma", "abc", "fbx", "mb"] label = "Reference" diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index b1fb0542f2..8d9b88ed32 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from maya import cmds import pyblish.api +from avalon.api import Session +from openpype.api import get_project_settings class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): @@ -16,10 +18,21 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["unrealStaticMesh"] def process(self, instance): + project_settings = get_project_settings(Session["AVALON_PROJECT"]) + sm_prefix = ( + project_settings + ["maya"] + ["create"] + ["CreateUnrealStaticMesh"] + ["static_mesh_prefix"] + ) # add fbx family to trigger fbx extractor instance.data["families"].append("fbx") - # take the name from instance (without the `S_` prefix) - instance.data["staticMeshCombinedName"] = instance.name[2:] + # take the name from instance (without the `unrealStaticMesh_` prefix) + instance.data["staticMeshCombinedName"] = "{}_{}".format( + sm_prefix, + instance.name[len(instance.data.get("family"))+3:] + ) geometry_set = [i for i in instance if i == "geometry_SET"] instance.data["membersToCombine"] = cmds.sets( diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 32dc9d1d1c..f46360e34a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -15,13 +15,24 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): def process(self, instance): to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) + duplicates = [] + + # if we have more objects, combine them into one + # or just duplicate the single one + if len(to_combine) > 1: + self.log.info( + "merging {} into {}".format( + " + ".join(to_combine), static_mesh_name)) + duplicates = cmds.duplicate(to_combine, ic=True) + cmds.polyUnite( + *duplicates, + n=static_mesh_name, ch=False) + else: + self.log.info( + "duplicating {} to {} for export".format( + to_combine[0], static_mesh_name) + ) + cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) if not instance.data.get("cleanNodes"): instance.data["cleanNodes"] = [] @@ -31,3 +42,5 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): instance.data["setMembers"] = [static_mesh_name] instance.data["setMembers"] += instance.data["collisionMembers"] + + self.log.debug(instance.data["setMembers"]) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 901a2ec75e..b886e7da75 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -71,6 +71,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["collision_prefixes"] ) + static_mesh_prefix = ( + project_settings + ["maya"] + ["create"] + ["CreateUnrealStaticMesh"] + ["static_mesh_prefix"] + ) combined_geometry_name = instance.data.get( "staticMeshCombinedName", None) @@ -107,7 +114,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name + combined_geometry_name[len(static_mesh_prefix)+1:] ) if not obj.startswith(expected_collision): From 1c0601518a3488b450b9c97d30e7c9d3011b1fb4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Feb 2022 15:58:47 +0100 Subject: [PATCH 002/337] remove debug print --- .../hosts/maya/plugins/publish/extract_unreal_staticmesh.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index f46360e34a..0799d574a2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -42,5 +42,3 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): instance.data["setMembers"] = [static_mesh_name] instance.data["setMembers"] += instance.data["collisionMembers"] - - self.log.debug(instance.data["setMembers"]) From 3f7602ce8c7a463ff0472c33ac0e7a3df177cf80 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Feb 2022 16:01:18 +0100 Subject: [PATCH 003/337] fix prefix --- 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 24e8e4a29b..4aaf6c705a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -52,7 +52,7 @@ "", "_Main" ], - "static_mesh_prefix": "S_", + "static_mesh_prefix": "S", "collision_prefixes": [ "UBX", "UCP", From 3cf48636b1a885704e8e30ddf7104595d1d23df9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 17 Feb 2022 13:18:44 +0100 Subject: [PATCH 004/337] rename family to staticMesh --- .../maya/plugins/create/create_unreal_staticmesh.py | 5 ++--- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- .../maya/plugins/publish/collect_unreal_staticmesh.py | 4 ++-- .../maya/plugins/publish/extract_unreal_staticmesh.py | 2 +- .../publish/validate_unreal_mesh_triangulated.py | 2 +- .../publish/validate_unreal_staticmesh_naming.py | 2 +- .../maya/plugins/publish/validate_unreal_up_axis.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 ++- .../settings/defaults/project_anatomy/templates.json | 2 +- .../settings/defaults/project_settings/global.json | 11 +++++++++++ 10 files changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 1fe7e57abc..f62d15fe62 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -10,7 +10,7 @@ class CreateUnrealStaticMesh(plugin.Creator): """Unreal Static Meshes with collisions.""" name = "staticMeshMain" label = "Unreal - Static Mesh" - family = "unrealStaticMesh" + family = "staticMesh" icon = "cube" dynamic_subset_keys = ["asset"] @@ -28,12 +28,11 @@ class CreateUnrealStaticMesh(plugin.Creator): variant, task_name, asset_id, project_name, host_name ) dynamic_data["asset"] = Session.get("AVALON_ASSET") - return dynamic_data def process(self): + self.name = "{}_{}".format(self.family, self.name) with lib.undo_chunk(): - self.name = "{}_{}".format(self.family, self.name) instance = super(CreateUnrealStaticMesh, self).process() content = cmds.sets(instance, query=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7cdd91a7ea..8713182d3f 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -21,7 +21,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "rig", "camerarig", "xgen", - "unrealStaticMesh"] + "staticMesh"] representations = ["ma", "abc", "fbx", "mb"] label = "Reference" diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 8d9b88ed32..604aa58b50 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -15,7 +15,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Static Meshes" - families = ["unrealStaticMesh"] + families = ["staticMesh"] def process(self, instance): project_settings = get_project_settings(Session["AVALON_PROJECT"]) @@ -28,7 +28,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): ) # add fbx family to trigger fbx extractor instance.data["families"].append("fbx") - # take the name from instance (without the `unrealStaticMesh_` prefix) + # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, instance.name[len(instance.data.get("family"))+3:] diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 0799d574a2..6153417de4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -10,7 +10,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Static Mesh" - families = ["unrealStaticMesh"] + families = ["staticMesh"] def process(self, instance): to_combine = instance.data.get("membersToCombine") diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index b2ef174374..737664ffd3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -10,7 +10,7 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): order = openpype.api.ValidateMeshOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] category = "geometry" label = "Mesh is Triangulated" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index b886e7da75..89769a3421 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -52,7 +52,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): optional = True order = openpype.api.ValidateContentsOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] label = "Unreal StaticMesh Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex_mesh = r"(?P.*))" diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py index 5a8c29c22d..b3af643048 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -11,7 +11,7 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): optional = True order = openpype.api.ValidateContentsOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] label = "Unreal Up-Axis check" actions = [openpype.api.RepairAction] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf214d9139..9ced6a1d7d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -100,7 +100,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "redshiftproxy", "effect", "xgen", - "hda" + "hda", + "staticMesh" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index d46d449c77..2ab3ff5c54 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -28,7 +28,7 @@ }, "delivery": {}, "unreal": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", "path": "{@folder}/{@file}" }, diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index f08bee8b2d..93aba808db 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -192,6 +192,17 @@ "task_types": [], "tasks": [], "template_name": "render" + }, + { + "families": [ + "staticMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template_name": "unreal" } ], "subset_grouping_profiles": [ From 487b273a09e3cc67bc0386d047bfb4309db66439 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:05:07 +0100 Subject: [PATCH 005/337] refactor fbx extractor --- openpype/hosts/maya/api/fbx.py | 208 ++++++++++++++++++ .../hosts/maya/plugins/publish/clean_nodes.py | 31 --- .../publish/collect_unreal_staticmesh.py | 2 - .../hosts/maya/plugins/publish/extract_fbx.py | 196 ++--------------- .../publish/extract_unreal_staticmesh.py | 89 ++++++-- 5 files changed, 287 insertions(+), 239 deletions(-) create mode 100644 openpype/hosts/maya/api/fbx.py delete mode 100644 openpype/hosts/maya/plugins/publish/clean_nodes.py diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py new file mode 100644 index 0000000000..3a8ae19ff7 --- /dev/null +++ b/openpype/hosts/maya/api/fbx.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +"""Tools to work with FBX.""" +import os +import logging + +from pyblish.api import Instance + +from maya import cmds # noqa +import maya.mel as mel # noqa + + +class FBXExtractor: + """Extract FBX from Maya. + + This extracts reproducible FBX exports ignoring any of the settings set + on the local machine in the FBX export options window. + + All export settings are applied with the `FBXExport*` commands prior + to the `FBXExport` call itself. The options can be overridden with + their + nice names as seen in the "options" property on this class. + + For more information on FBX exports see: + - https://knowledge.autodesk.com/support/maya/learn-explore/caas + /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4 + -9CB19C28F4E0-htm.html + - http://forums.cgsociety.org/archive/index.php?t-1032853.html + - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE + /LKs9hakE28kJ + + """ + @property + def options(self): + """Overridable options for FBX Export + + Given in the following format + - {NAME: EXPECTED TYPE} + + If the overridden option's type does not match, + the option is not included and a warning is logged. + + """ + + return { + "cameras": bool, + "smoothingGroups": bool, + "hardEdges": bool, + "tangents": bool, + "smoothMesh": bool, + "instances": bool, + # "referencedContainersContent": bool, # deprecated in Maya 2016+ + "bakeComplexAnimation": int, + "bakeComplexStart": int, + "bakeComplexEnd": int, + "bakeComplexStep": int, + "bakeResampleAnimation": bool, + "animationOnly": bool, + "useSceneName": bool, + "quaternion": str, # "euler" + "shapes": bool, + "skins": bool, + "constraints": bool, + "lights": bool, + "embeddedTextures": bool, + "inputConnections": bool, + "upAxis": str, # x, y or z, + "triangulate": bool + } + + @property + def default_options(self): + """The default options for FBX extraction. + + This includes shapes, skins, constraints, lights and incoming + connections and exports with the Y-axis as up-axis. + + By default this uses the time sliders start and end time. + + """ + + start_frame = int(cmds.playbackOptions(query=True, + animationStartTime=True)) + end_frame = int(cmds.playbackOptions(query=True, + animationEndTime=True)) + + return { + "cameras": False, + "smoothingGroups": False, + "hardEdges": False, + "tangents": False, + "smoothMesh": False, + "instances": False, + "bakeComplexAnimation": True, + "bakeComplexStart": start_frame, + "bakeComplexEnd": end_frame, + "bakeComplexStep": 1, + "bakeResampleAnimation": True, + "animationOnly": False, + "useSceneName": False, + "quaternion": "euler", + "shapes": True, + "skins": True, + "constraints": False, + "lights": True, + "embeddedTextures": True, + "inputConnections": True, + "upAxis": "y", + "triangulate": False + } + + def __init__(self, log=None): + # Ensure FBX plug-in is loaded + self.log = log or logging.getLogger(__class__.__name__) + cmds.loadPlugin("fbxmaya", quiet=True) + + def parse_overrides(self, instance, options): + """Inspect data of instance to determine overridden options + + An instance may supply any of the overridable options + as data, the option is then added to the extraction. + + """ + + for key in instance.data: + if key not in self.options: + continue + + # Ensure the data is of correct type + value = instance.data[key] + if not isinstance(value, self.options[key]): + self.log.warning( + "Overridden attribute {key} was of " + "the wrong type: {invalid_type} " + "- should have been {valid_type}".format( + key=key, + invalid_type=type(value).__name__, + valid_type=self.options[key].__name__)) + continue + + options[key] = value + + return options + + def set_options_from_instance(self, instance): + # type: (Instance) -> None + """Sets FBX export options from data in the instance. + + Args: + instance (Instance): Instance data. + + """ + # Parse export options + options = self.default_options + options = self.parse_overrides(instance, options) + self.log.info("Export options: {0}".format(options)) + + # Collect the start and end including handles + # TODO: Move this to library function (pypeclub/OpenPype#2648) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + handle_start = instance.data.get("handleStart", 0) + handle_end = instance.data.get("handleEnd", 0) + if handle_start: + start -= handle_start + if handle_end: + end += handle_end + + options['bakeComplexStart'] = start + options['bakeComplexEnd'] = end + + # First apply the default export settings to be fully consistent + # each time for successive publishes + mel.eval("FBXResetExport") + + # Apply the FBX overrides through MEL since the commands + # only work correctly in MEL according to online + # available discussions on the topic + _iteritems = getattr(options, "iteritems", options.items) + for option, value in _iteritems(): + key = option[0].upper() + option[1:] # uppercase first letter + + # Boolean must be passed as lower-case strings + # as to MEL standards + if isinstance(value, bool): + value = str(value).lower() + + template = "FBXExport{0} {1}" if key == "UpAxis" else \ + "FBXExport{0} -v {1}" # noqa + cmd = template.format(key, value) + self.log.info(cmd) + mel.eval(cmd) + + # Never show the UI or generate a log + mel.eval("FBXExportShowUI -v false") + mel.eval("FBXExportGenerateLog -v false") + + @staticmethod + def export(members, path): + # type: (list, str) -> None + """Export members as FBX with given path. + + Args: + members (list): List of members to export. + path (str): Path to use for export. + + """ + cmds.select(members, r=1, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/plugins/publish/clean_nodes.py b/openpype/hosts/maya/plugins/publish/clean_nodes.py deleted file mode 100644 index 03995cdabe..0000000000 --- a/openpype/hosts/maya/plugins/publish/clean_nodes.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -"""Cleanup leftover nodes.""" -from maya import cmds # noqa -import pyblish.api - - -class CleanNodesUp(pyblish.api.InstancePlugin): - """Cleans up the staging directory after a successful publish. - - This will also clean published renders and delete their parent directories. - - """ - - order = pyblish.api.IntegratorOrder + 10 - label = "Clean Nodes" - optional = True - active = True - - def process(self, instance): - if not instance.data.get("cleanNodes"): - self.log.info("Nothing to clean.") - return - - nodes_to_clean = instance.data.pop("cleanNodes", []) - self.log.info("Removing {} nodes".format(len(nodes_to_clean))) - for node in nodes_to_clean: - try: - cmds.delete(node) - except ValueError: - # object might be already deleted, don't complain about it - pass diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 604aa58b50..1a0a561efd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -26,8 +26,6 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["static_mesh_prefix"] ) - # add fbx family to trigger fbx extractor - instance.data["families"].append("fbx") # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py index 844084b9ab..fbbe8e06b0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py @@ -5,152 +5,29 @@ from maya import cmds # noqa import maya.mel as mel # noqa import pyblish.api import openpype.api -from openpype.hosts.maya.api.lib import ( - root_parent, - maintained_selection -) +from openpype.hosts.maya.api.lib import maintained_selection + +from openpype.hosts.maya.api import fbx class ExtractFBX(openpype.api.Extractor): """Extract FBX from Maya. - This extracts reproducible FBX exports ignoring any of the settings set - on the local machine in the FBX export options window. - - All export settings are applied with the `FBXExport*` commands prior - to the `FBXExport` call itself. The options can be overridden with their - nice names as seen in the "options" property on this class. - - For more information on FBX exports see: - - https://knowledge.autodesk.com/support/maya/learn-explore/caas - /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4 - -9CB19C28F4E0-htm.html - - http://forums.cgsociety.org/archive/index.php?t-1032853.html - - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE - /LKs9hakE28kJ + This extracts reproducible FBX exports ignoring any of the + settings set on the local machine in the FBX export options window. """ - order = pyblish.api.ExtractorOrder label = "Extract FBX" families = ["fbx"] - @property - def options(self): - """Overridable options for FBX Export - - Given in the following format - - {NAME: EXPECTED TYPE} - - If the overridden option's type does not match, - the option is not included and a warning is logged. - - """ - - return { - "cameras": bool, - "smoothingGroups": bool, - "hardEdges": bool, - "tangents": bool, - "smoothMesh": bool, - "instances": bool, - # "referencedContainersContent": bool, # deprecated in Maya 2016+ - "bakeComplexAnimation": int, - "bakeComplexStart": int, - "bakeComplexEnd": int, - "bakeComplexStep": int, - "bakeResampleAnimation": bool, - "animationOnly": bool, - "useSceneName": bool, - "quaternion": str, # "euler" - "shapes": bool, - "skins": bool, - "constraints": bool, - "lights": bool, - "embeddedTextures": bool, - "inputConnections": bool, - "upAxis": str, # x, y or z, - "triangulate": bool - } - - @property - def default_options(self): - """The default options for FBX extraction. - - This includes shapes, skins, constraints, lights and incoming - connections and exports with the Y-axis as up-axis. - - By default this uses the time sliders start and end time. - - """ - - start_frame = int(cmds.playbackOptions(query=True, - animationStartTime=True)) - end_frame = int(cmds.playbackOptions(query=True, - animationEndTime=True)) - - return { - "cameras": False, - "smoothingGroups": False, - "hardEdges": False, - "tangents": False, - "smoothMesh": False, - "instances": False, - "bakeComplexAnimation": True, - "bakeComplexStart": start_frame, - "bakeComplexEnd": end_frame, - "bakeComplexStep": 1, - "bakeResampleAnimation": True, - "animationOnly": False, - "useSceneName": False, - "quaternion": "euler", - "shapes": True, - "skins": True, - "constraints": False, - "lights": True, - "embeddedTextures": True, - "inputConnections": True, - "upAxis": "y", - "triangulate": False - } - - def parse_overrides(self, instance, options): - """Inspect data of instance to determine overridden options - - An instance may supply any of the overridable options - as data, the option is then added to the extraction. - - """ - - for key in instance.data: - if key not in self.options: - continue - - # Ensure the data is of correct type - value = instance.data[key] - if not isinstance(value, self.options[key]): - self.log.warning( - "Overridden attribute {key} was of " - "the wrong type: {invalid_type} " - "- should have been {valid_type}".format( - key=key, - invalid_type=type(value).__name__, - valid_type=self.options[key].__name__)) - continue - - options[key] = value - - return options - def process(self, instance): - - # Ensure FBX plug-in is loaded - cmds.loadPlugin("fbxmaya", quiet=True) + fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path - stagingDir = self.staging_dir(instance) + staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) - path = os.path.join(stagingDir, filename) + path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need # to format it into a string in a mel expression @@ -162,58 +39,13 @@ class ExtractFBX(openpype.api.Extractor): self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) - # Parse export options - options = self.default_options - options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) - - # Collect the start and end including handles - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles - - options['bakeComplexStart'] = start - options['bakeComplexEnd'] = end - - # First apply the default export settings to be fully consistent - # each time for successive publishes - mel.eval("FBXResetExport") - - # Apply the FBX overrides through MEL since the commands - # only work correctly in MEL according to online - # available discussions on the topic - _iteritems = getattr(options, "iteritems", options.items) - for option, value in _iteritems(): - key = option[0].upper() + option[1:] # uppercase first letter - - # Boolean must be passed as lower-case strings - # as to MEL standards - if isinstance(value, bool): - value = str(value).lower() - - template = "FBXExport{0} {1}" if key == "UpAxis" else "FBXExport{0} -v {1}" # noqa - cmd = template.format(key, value) - self.log.info(cmd) - mel.eval(cmd) - - # Never show the UI or generate a log - mel.eval("FBXExportShowUI -v false") - mel.eval("FBXExportGenerateLog -v false") + fbx_exporter.set_options_from_instance(instance) # Export - if "unrealStaticMesh" in instance.data["families"]: - with maintained_selection(): - with root_parent(members): - self.log.info("Un-parenting: {}".format(members)) - cmds.select(members, r=1, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) - else: - with maintained_selection(): - cmds.select(members, r=1, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) + with maintained_selection(): + fbx_exporter.export(members, path) + cmds.select(members, r=1, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) if "representations" not in instance.data: instance.data["representations"] = [] @@ -222,7 +54,7 @@ class ExtractFBX(openpype.api.Extractor): 'name': 'fbx', 'ext': 'fbx', 'files': filename, - "stagingDir": stagingDir, + "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 6153417de4..d3d491594a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -1,9 +1,18 @@ # -*- coding: utf-8 -*- """Create Unreal Static Mesh data to be extracted as FBX.""" -import openpype.api -import pyblish.api +import os + from maya import cmds # noqa +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import ( + root_parent, + maintained_selection, + delete_after +) +from openpype.hosts.maya.api import fbx + class ExtractUnrealStaticMesh(openpype.api.Extractor): """Extract FBX from Maya. """ @@ -13,32 +22,64 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): + fbx_exporter = fbx.FBXExtractor(log=self.log) to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") duplicates = [] - # if we have more objects, combine them into one - # or just duplicate the single one - if len(to_combine) > 1: - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) - else: - self.log.info( - "duplicating {} to {} for export".format( - to_combine[0], static_mesh_name) - ) - cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) + # delete created temporary nodes after extraction + with delete_after() as delete_bin: + # if we have more objects, combine them into one + # or just duplicate the single one + if len(to_combine) > 1: + self.log.info( + "merging {} into {}".format( + " + ".join(to_combine), static_mesh_name)) + duplicates = cmds.duplicate(to_combine, ic=True) + cmds.polyUnite( + *duplicates, + n=static_mesh_name, ch=False) + else: + self.log.info( + "duplicating {} to {} for export".format( + to_combine[0], static_mesh_name) + ) + cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) - if not instance.data.get("cleanNodes"): - instance.data["cleanNodes"] = [] + delete_bin.extend(static_mesh_name) + delete_bin.extend(duplicates) - instance.data["cleanNodes"].append(static_mesh_name) - instance.data["cleanNodes"] += duplicates + members = [static_mesh_name] + members += instance.data["collisionMembers"] - instance.data["setMembers"] = [static_mesh_name] - instance.data["setMembers"] += instance.data["collisionMembers"] + fbx_exporter = fbx.FBXExtractor() + + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) + + fbx_exporter.set_options_from_instance(instance) + + with maintained_selection(): + with root_parent(members): + self.log.info("Un-parenting: {}".format(members)) + fbx_exporter.export(members, path) + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract FBX successful to: {0}".format(path)) \ No newline at end of file From 9604dca2593745380fc24741b7b7f5cf45f76ba8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:40:21 +0100 Subject: [PATCH 006/337] fix logging --- openpype/hosts/maya/api/fbx.py | 2 +- .../maya/plugins/publish/extract_unreal_staticmesh.py | 8 +++++--- .../plugins/publish/validate_unreal_staticmesh_naming.py | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 3a8ae19ff7..659f456e1a 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -110,7 +110,7 @@ class FBXExtractor: def __init__(self, log=None): # Ensure FBX plug-in is loaded - self.log = log or logging.getLogger(__class__.__name__) + self.log = log or logging.getLogger(self.__class__.__name__) cmds.loadPlugin("fbxmaya", quiet=True) def parse_overrides(self, instance, options): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index d3d491594a..c5d2710dc2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,7 +22,6 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - fbx_exporter = fbx.FBXExtractor(log=self.log) to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") duplicates = [] @@ -46,13 +45,13 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): ) cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) - delete_bin.extend(static_mesh_name) + delete_bin.extend([static_mesh_name]) delete_bin.extend(duplicates) members = [static_mesh_name] members += instance.data["collisionMembers"] - fbx_exporter = fbx.FBXExtractor() + fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) @@ -74,6 +73,9 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): self.log.info("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) + if "representations" not in instance.data: + instance.data["representations"] = [] + representation = { 'name': 'fbx', 'ext': 'fbx', diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 89769a3421..e233fd190c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -79,6 +79,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["static_mesh_prefix"] ) + to_combine = instance.data.get("membersToCombine") + if not to_combine: + raise ValueError("Missing geometry to export.") combined_geometry_name = instance.data.get( "staticMeshCombinedName", None) if cls.validate_mesh: From 3a42aa5c943e3a1a9a03a9f5c34cde0dbe8263d7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:42:19 +0100 Subject: [PATCH 007/337] fix family name in defaults --- openpype/settings/defaults/project_settings/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 93aba808db..efed25287a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -298,7 +298,7 @@ }, { "families": [ - "unrealStaticMesh" + "staticMesh" ], "hosts": [ "maya" From f5087f4e47588d31bd335e51c648c8eb6bb33425 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 19:04:24 +0100 Subject: [PATCH 008/337] =?UTF-8?q?fix=20hound=20=F0=9F=90=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/fbx.py | 1 - .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 2 +- .../hosts/maya/plugins/publish/extract_unreal_staticmesh.py | 2 +- .../maya/plugins/publish/validate_unreal_staticmesh_naming.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 659f456e1a..00c58153af 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Tools to work with FBX.""" -import os import logging from pyblish.api import Instance diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 1a0a561efd..2c0bec2c1a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -29,7 +29,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family"))+3:] + instance.name[len(instance.data.get("family")) + 3:] ) geometry_set = [i for i in instance if i == "geometry_SET"] diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index c5d2710dc2..0c7d61f8f5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -84,4 +84,4 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) \ No newline at end of file + self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index e233fd190c..fd19e3d2af 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -117,7 +117,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name[len(static_mesh_prefix)+1:] + combined_geometry_name[len(static_mesh_prefix) + 1:] ) if not obj.startswith(expected_collision): From 8dcc2eabff6a39c47f4fae519cc9de658039312d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Feb 2022 22:24:37 +0100 Subject: [PATCH 009/337] flame: adding batch utils for creating batch in desktop --- openpype/hosts/flame/api/batch_utils.py | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openpype/hosts/flame/api/batch_utils.py diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py new file mode 100644 index 0000000000..3a155c4b8d --- /dev/null +++ b/openpype/hosts/flame/api/batch_utils.py @@ -0,0 +1,37 @@ +import flame + + +def create_batch(name, frame_start, frame_end, **kwargs): + schematicReels = ['LoadedReel1'] + shelfReels = ['ShelfReel1'] + + handle_start = kwargs.get("handleStart") + handle_end = kwargs.get("handleEnd") + + if handle_start: + frame_start -= handle_start + if handle_end: + frame_end += handle_end + + # Create batch group with name, start_frame value, duration value, + # set of schematic reel names, set of shelf reel names + flame.batch.create_batch_group( + name, + start_frame=frame_start, + duration=frame_end, + reels=schematicReels, + shelf_reels=shelfReels + ) + + if kwargs.get("switch_batch_tab"): + # use this command to switch to the batch tab + flame.batch.go_to() + + comp = flame.batch.create_node("Comp") + writeFile = flame.batch.create_node("Write File") + + # connect nodes + flame.batch.connect_nodes(comp, "Result", writeFile, "Front") + + # sort batch nodes + flame.batch.organize() From 5391f1fff3abe36dde6289778ab2823e79616ab7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Feb 2022 22:46:27 +0100 Subject: [PATCH 010/337] flame: adding write node to batch utils --- openpype/hosts/flame/api/batch_utils.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 3a155c4b8d..2c80834928 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -5,6 +5,7 @@ def create_batch(name, frame_start, frame_end, **kwargs): schematicReels = ['LoadedReel1'] shelfReels = ['ShelfReel1'] + write_pref = kwargs["write_pref"] handle_start = kwargs.get("handleStart") handle_end = kwargs.get("handleEnd") @@ -27,11 +28,23 @@ def create_batch(name, frame_start, frame_end, **kwargs): # use this command to switch to the batch tab flame.batch.go_to() - comp = flame.batch.create_node("Comp") - writeFile = flame.batch.create_node("Write File") + comp_node = flame.batch.create_node("Comp") + + # create write node + write_node = flame.batch.create_node('Write File') + write_node.media_path = write_pref["media_path"] + write_node.media_path_pattern = write_pref["media_path_pattern"] + write_node.create_clip = write_pref["create_clip"] + write_node.include_setup = write_pref["include_setup"] + write_node.create_clip_path = write_pref["create_clip_path"] + write_node.include_setup_path = write_pref["include_setup_path"] + write_node.file_type = write_pref["file_type"] + write_node.bit_depth = write_pref["bit_depth"] + write_node.frame_index_mode = write_pref["frame_index_mode"] + write_node.frame_padding = int(write_pref["frame_padding"]) # connect nodes - flame.batch.connect_nodes(comp, "Result", writeFile, "Front") + flame.batch.connect_nodes(comp_node, "Result", write_node, "Front") # sort batch nodes flame.batch.organize() From cdc3d0be792e718b17fb1290f3f05a5ea8c4380f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Feb 2022 12:49:11 +0100 Subject: [PATCH 011/337] flame: batch utils to api --- openpype/hosts/flame/api/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 56bbadd2fc..98a1a23e89 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -70,6 +70,9 @@ from .render_utils import ( export_clip, get_preset_path_by_xml_name ) +from .batch_utils import ( + create_batch +) __all__ = [ # constants @@ -140,5 +143,8 @@ __all__ = [ # render utils "export_clip", - "get_preset_path_by_xml_name" + "get_preset_path_by_xml_name", + + # batch utils + "create_batch" ] From 5d8e3e293f46860f9de52f93736f9646023de501 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Feb 2022 12:49:33 +0100 Subject: [PATCH 012/337] flame: adding docstrigs to create_batch --- openpype/hosts/flame/api/batch_utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 2c80834928..a1fe7961c4 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -2,8 +2,15 @@ import flame def create_batch(name, frame_start, frame_end, **kwargs): - schematicReels = ['LoadedReel1'] - shelfReels = ['ShelfReel1'] + """Create Batch Group in active project's Desktop + + Args: + name (str): name of batch group to be created + frame_start (int): start frame of batch + frame_end (int): end frame of batch + """ + schematic_reels = kwargs.get("shematic_reels") or ['LoadedReel1'] + shelf_reels = kwargs.get("shelf_reels") or ['ShelfReel1'] write_pref = kwargs["write_pref"] handle_start = kwargs.get("handleStart") @@ -20,8 +27,8 @@ def create_batch(name, frame_start, frame_end, **kwargs): name, start_frame=frame_start, duration=frame_end, - reels=schematicReels, - shelf_reels=shelfReels + reels=schematic_reels, + shelf_reels=shelf_reels ) if kwargs.get("switch_batch_tab"): From 162df8c0aca975da19778cd05e22771f651458a9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Feb 2022 13:00:14 +0100 Subject: [PATCH 013/337] flame: itegrator wip --- .../flame/plugins/publish/integrate_batch_group.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 openpype/hosts/flame/plugins/publish/integrate_batch_group.py diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py new file mode 100644 index 0000000000..fd88ed318e --- /dev/null +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -0,0 +1,14 @@ +import pyblish +import openpype.hosts.flame.api as opfapi + +@pyblish.api.log +class IntegrateBatchGroup(pyblish.api.InstancePlugin): + """Integrate published shot to batch group""" + + order = pyblish.api.IntegratorOrder + 0.45 + label = "Integrate Batch Groups" + hosts = ["flame"] + families = ["clip"] + + def process(self, instance): + opfapi.create_batch \ No newline at end of file From 1121bc8eaa42a0f8fcdda1001500908e42c7308e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:36:43 +0100 Subject: [PATCH 014/337] disable unnecessary plugins --- .../maya/plugins/publish/validate_unreal_mesh_triangulated.py | 1 + .../maya/plugins/publish/validate_unreal_staticmesh_naming.py | 4 ++-- .../hosts/maya/plugins/publish/validate_unreal_up_axis.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index 737664ffd3..c05121a1b0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -14,6 +14,7 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): category = "geometry" label = "Mesh is Triangulated" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + active = False @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index fd19e3d2af..d15d52f3bd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -53,7 +53,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): order = openpype.api.ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] - label = "Unreal StaticMesh Name" + label = "Unreal Static Mesh Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex_mesh = r"(?P.*))" regex_collision = r"(?P.*)" @@ -101,7 +101,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.warning("No collision objects to validate.") return False - regex_collision = "{}{}".format( + regex_collision = "{}{}_(\\d+)".format( "(?P({}))_".format( "|".join("{0}".format(p) for p in collision_prefixes) ) or "", cls.regex_collision diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py index b3af643048..5e1b04889f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -9,6 +9,7 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): """Validate if Z is set as up axis in Maya""" optional = True + active = False order = openpype.api.ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] From 691f23d72eab7ab0114efc6a9890ddcdce89f0da Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:38:48 +0100 Subject: [PATCH 015/337] unify handles --- openpype/hosts/maya/api/fbx.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 00c58153af..7980cd029c 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -154,15 +154,8 @@ class FBXExtractor: self.log.info("Export options: {0}".format(options)) # Collect the start and end including handles - # TODO: Move this to library function (pypeclub/OpenPype#2648) - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - handle_start = instance.data.get("handleStart", 0) - handle_end = instance.data.get("handleEnd", 0) - if handle_start: - start -= handle_start - if handle_end: - end += handle_end + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] options['bakeComplexStart'] = start options['bakeComplexEnd'] = end From 901df528d4107e9423e7bc81aa80108024af143f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:42:36 +0100 Subject: [PATCH 016/337] remove ftrack submodules from old location --- .gitmodules | 8 +------- .../modules/default_modules/ftrack/python2_vendor/arrow | 1 - .../ftrack/python2_vendor/ftrack-python-api | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/arrow delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api diff --git a/.gitmodules b/.gitmodules index e1b0917e9d..67b820a247 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,10 +3,4 @@ url = https://github.com/pypeclub/avalon-core.git [submodule "repos/avalon-unreal-integration"] path = repos/avalon-unreal-integration - url = https://github.com/pypeclub/avalon-unreal-integration.git -[submodule "openpype/modules/default_modules/ftrack/python2_vendor/arrow"] - path = openpype/modules/default_modules/ftrack/python2_vendor/arrow - url = https://github.com/arrow-py/arrow.git -[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"] - path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api - url = https://bitbucket.org/ftrack/ftrack-python-api.git \ No newline at end of file + url = https://github.com/pypeclub/avalon-unreal-integration.git \ No newline at end of file diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/arrow b/openpype/modules/default_modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e From 28e11a5b2d847d0db971fe8a7d5c707064ac17fd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 15:18:38 +0100 Subject: [PATCH 017/337] fix frame handling and collision name determination --- openpype/hosts/maya/api/fbx.py | 6 ++++-- .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 7980cd029c..92683da51b 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -154,8 +154,10 @@ class FBXExtractor: self.log.info("Export options: {0}".format(options)) # Collect the start and end including handles - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] + start = instance.data.get("frameStartHandle") or \ + instance.context.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") or \ + instance.context.data.get("frameEndHandle") options['bakeComplexStart'] = start options['bakeComplexEnd'] = end diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 2c0bec2c1a..ddcc3f691f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -29,7 +29,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family")) + 3:] + instance.name[len(instance.data.get("family")) + 1:] ) geometry_set = [i for i in instance if i == "geometry_SET"] From bd5478731cc6f6839d7dcbbeb1fd6d9d37e7cff0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 22:39:53 +0100 Subject: [PATCH 018/337] change default templates --- .../defaults/project_anatomy/templates.json | 15 ++++++++++++--- .../defaults/project_settings/global.json | 9 ++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 2ab3ff5c54..7d01248653 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -28,9 +28,18 @@ }, "delivery": {}, "unreal": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", - "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "folder": "{root[work]}/{project[name]}/unreal/{task[name]}", + "file": "{project[code]}_{asset}", "path": "{@folder}/{@file}" }, - "others": {} + "others": { + "maya2unreal": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", + "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "path": "{@folder}/{@file}" + }, + "__dynamic_keys_labels__": { + "maya2unreal": "Maya to Unreal" + } + } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index efed25287a..86786cc9ed 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -202,7 +202,7 @@ ], "task_types": [], "tasks": [], - "template_name": "unreal" + "template_name": "maya2unreal" } ], "subset_grouping_profiles": [ @@ -315,6 +315,13 @@ "task_types": [], "hosts": [], "workfile_template": "work" + }, + { + "task_types": [], + "hosts": [ + "unreal" + ], + "workfile_template": "unreal" } ], "last_workfile_on_startup": [ From 8c0d7ed1ee09c84b233cb7ec4068312afa27507a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 4 Mar 2022 18:38:26 +0100 Subject: [PATCH 019/337] initial work on skeletal mesh support --- .../create/create_unreal_skeletalmesh.py | 50 ++++++++++++++++ .../publish/collect_unreal_skeletalmesh.py | 23 ++++++++ .../publish/collect_unreal_staticmesh.py | 7 +-- .../publish/extract_unreal_skeletalmesh.py | 57 +++++++++++++++++++ .../publish/extract_unreal_staticmesh.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 +- .../defaults/project_settings/global.json | 14 ++++- .../defaults/project_settings/maya.json | 5 ++ .../schemas/schema_maya_create.json | 26 +++++++++ 9 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py create mode 100644 openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py new file mode 100644 index 0000000000..a6deeeee2e --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""Creator for Unreal Skeletal Meshes.""" +from openpype.hosts.maya.api import plugin, lib +from avalon.api import Session +from maya import cmds # noqa + + +class CreateUnrealSkeletalMesh(plugin.Creator): + """Unreal Static Meshes with collisions.""" + name = "staticMeshMain" + label = "Unreal - Skeletal Mesh" + family = "skeletalMesh" + icon = "thumbs-up" + dynamic_subset_keys = ["asset"] + + joint_hints = [] + + def __init__(self, *args, **kwargs): + """Constructor.""" + super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs) + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + dynamic_data["asset"] = Session.get("AVALON_ASSET") + return dynamic_data + + def process(self): + self.name = "{}_{}".format(self.family, self.name) + with lib.undo_chunk(): + instance = super(CreateUnrealSkeletalMesh, self).process() + content = cmds.sets(instance, query=True) + + # empty set and process its former content + cmds.sets(content, rm=instance) + geometry_set = cmds.sets(name="geometry_SET", empty=True) + joints_set = cmds.sets(name="joints_SET", empty=True) + + cmds.sets([geometry_set, joints_set], forceElement=instance) + members = cmds.ls(content) or [] + + for node in members: + if node in self.joint_hints: + cmds.sets(node, forceElement=joints_set) + else: + cmds.sets(node, forceElement=geometry_set) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py new file mode 100644 index 0000000000..4b1de865c5 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api +from avalon.api import Session +from openpype.api import get_project_settings + + +class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): + """Collect Unreal Skeletal Mesh.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Unreal Skeletal Meshes" + families = ["skeletalMesh"] + + def process(self, instance): + # set fbx overrides on instance + instance.data["smoothingGroups"] = True + instance.data["smoothMesh"] = True + instance.data["triangulate"] = True + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index ddcc3f691f..59f8df1ef1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -6,12 +6,7 @@ from openpype.api import get_project_settings class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): - """Collect Unreal Static Mesh - - Ensures always only a single frame is extracted (current frame). This - also sets correct FBX options for later extraction. - - """ + """Collect Unreal Static Mesh.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Static Meshes" diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py new file mode 100644 index 0000000000..0ad1a92292 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Create Unreal Skeletal Mesh data to be extracted as FBX.""" +import os + +from maya import cmds # noqa + +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import ( + root_parent, + maintained_selection, + delete_after +) +from openpype.hosts.maya.api import fbx + + +class ExtractUnrealSkeletalMesh(openpype.api.Extractor): + """Extract Unreal Skeletal Mesh as FBX from Maya. """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Unreal Skeletal Mesh" + families = ["skeletalMesh"] + + def process(self, instance): + fbx_exporter = fbx.FBXExtractor(log=self.log) + + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(instance)) + self.log.info("Instance: {0}".format(instance[:])) + + fbx_exporter.set_options_from_instance(instance) + with maintained_selection(): + with root_parent(instance): + self.log.info("Un-parenting: {}".format(instance)) + fbx_exporter.export(instance, path) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 0c7d61f8f5..22a3af3059 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -15,7 +15,7 @@ from openpype.hosts.maya.api import fbx class ExtractUnrealStaticMesh(openpype.api.Extractor): - """Extract FBX from Maya. """ + """Extract Unreal Static Mesh as FBX from Maya. """ order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Static Mesh" diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 48b87c697b..fce8c73d1d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -102,7 +102,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "staticMesh" + "staticMesh", + "skeletalMesh" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 86786cc9ed..b7f6414cfb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -195,7 +195,8 @@ }, { "families": [ - "staticMesh" + "staticMesh", + "skeletalMesh" ], "hosts": [ "maya" @@ -306,6 +307,17 @@ "task_types": [], "tasks": [], "template": "S_{asset}{variant}" + }, + { + "families": [ + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template": "SK_{asset}{variant}" } ] }, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 362835e558..6317d30b3f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -60,6 +60,11 @@ "UCX" ] }, + "CreateUnrealSkeletalMesh": { + "enabled": true, + "defaults": [], + "joint_hints": "jnt_org" + }, "CreateAnimation": { "enabled": true, "defaults": [ 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 0544b4bab7..6dc10ed2a5 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 @@ -97,6 +97,32 @@ } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateUnrealSkeletalMesh", + "label": "Create Unreal - Skeletal Mesh", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + }, + { + "type": "text", + "key": "joint_hints", + "label": "Joint root hint" + } + ] + }, { "type": "schema_template", From 5e5f6e0879a470688072c3125145bdc544f10cb6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 8 Mar 2022 17:19:57 +0100 Subject: [PATCH 020/337] fix un-parenting --- .../publish/collect_unreal_skeletalmesh.py | 23 +++++++++++++++++++ .../publish/extract_unreal_skeletalmesh.py | 14 +++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 4b1de865c5..7d479b706c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -21,3 +21,26 @@ class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame + + geo_sets = [ + i for i in instance[:] + if i.lower().startswith("geometry_set") + ] + + joint_sets = [ + i for i in instance[:] + if i.lower().startswith("joints_set") + ] + + instance.data["geometry"] = [] + instance.data["joints"] = [] + + for geo_set in geo_sets: + geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True) + if geo_content: + instance.data["geometry"] += geo_content + + for join_set in joint_sets: + join_content = cmds.ls(cmds.sets(join_set, query=True), long=True) + if join_content: + instance.data["joints"] += join_content diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 0ad1a92292..c0b408c3f0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -29,19 +29,25 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) + geo = instance.data.get("geometry") + joints = instance.data.get("joints") + + to_extract = geo + joints + # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(instance)) + self.log.info("Members: {0}".format(to_extract)) self.log.info("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) with maintained_selection(): - with root_parent(instance): - self.log.info("Un-parenting: {}".format(instance)) - fbx_exporter.export(instance, path) + with root_parent(to_extract): + rooted = [i.split("|")[-1] for i in to_extract] + self.log.info("Un-parenting: {}".format(to_extract)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From 592c95b1a66e0067b797b7a6d7458fa8127ac03a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Mar 2022 01:14:21 +0100 Subject: [PATCH 021/337] fixes static mesh side of things --- .../plugins/publish/collect_unreal_staticmesh.py | 15 ++++++++++++--- .../plugins/publish/extract_unreal_staticmesh.py | 2 +- .../publish/validate_unreal_staticmesh_naming.py | 7 +++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 59f8df1ef1..02f21187b3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -from maya import cmds +from maya import cmds # noqa import pyblish.api from avalon.api import Session from openpype.api import get_project_settings +from pprint import pformat class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): @@ -24,17 +25,25 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family")) + 1:] - ) + instance.data.get("subset")[len(sm_prefix) + 1:]) + + self.log.info("joined mesh name: {}".format( + instance.data.get("staticMeshCombinedName"))) geometry_set = [i for i in instance if i == "geometry_SET"] instance.data["membersToCombine"] = cmds.sets( geometry_set, query=True) + self.log.info("joining meshes: {}".format( + pformat(instance.data.get("membersToCombine")))) + collision_set = [i for i in instance if i == "collisions_SET"] instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) + self.log.info("collisions: {}".format( + pformat(instance.data.get("collisionMembers")))) + # set fbx overrides on instance instance.data["smoothingGroups"] = True instance.data["smoothMesh"] = True diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 22a3af3059..987370d395 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -46,7 +46,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) delete_bin.extend([static_mesh_name]) - delete_bin.extend(duplicates) + # delete_bin.extend(duplicates) members = [static_mesh_name] members += instance.data["collisionMembers"] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index d15d52f3bd..1ecf436582 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -115,9 +115,12 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.error("{} is invalid".format(obj)) invalid.append(obj) else: + un_prefixed = combined_geometry_name[ + len(static_mesh_prefix) + 1: + ] expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name[len(static_mesh_prefix) + 1:] + un_prefixed ) if not obj.startswith(expected_collision): @@ -130,7 +133,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - combined_geometry_name, + un_prefixed, )) invalid.append(obj) From bc596899f5b96e27c9b1e3937384d18c4462ff3e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Mar 2022 23:45:40 +0100 Subject: [PATCH 022/337] fixed fbx settings --- openpype/hosts/maya/api/fbx.py | 6 +++--- .../maya/plugins/publish/collect_unreal_skeletalmesh.py | 5 ----- .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 5 ----- openpype/plugins/publish/collect_resources_path.py | 5 ++++- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 92683da51b..f8fe189589 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -84,10 +84,10 @@ class FBXExtractor: return { "cameras": False, - "smoothingGroups": False, + "smoothingGroups": True, "hardEdges": False, "tangents": False, - "smoothMesh": False, + "smoothMesh": True, "instances": False, "bakeComplexAnimation": True, "bakeComplexStart": start_frame, @@ -101,7 +101,7 @@ class FBXExtractor: "skins": True, "constraints": False, "lights": True, - "embeddedTextures": True, + "embeddedTextures": False, "inputConnections": True, "upAxis": "y", "triangulate": False diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 7d479b706c..2b176e3a6d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -13,11 +13,6 @@ class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): families = ["skeletalMesh"] def process(self, instance): - # set fbx overrides on instance - instance.data["smoothingGroups"] = True - instance.data["smoothMesh"] = True - instance.data["triangulate"] = True - frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 02f21187b3..faa5880e43 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -44,11 +44,6 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): self.log.info("collisions: {}".format( pformat(instance.data.get("collisionMembers")))) - # set fbx overrides on instance - instance.data["smoothingGroups"] = True - instance.data["smoothMesh"] = True - instance.data["triangulate"] = True - frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index fa181301ee..1f509365c7 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -53,7 +53,10 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "textures", "action", "background", - "effect" + "effect", + "staticMesh", + "skeletalMesh" + ] def process(self, instance): From 52dd76158fab048bd5ca8b9b1435b483aaa3c205 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Mar 2022 19:08:52 +0100 Subject: [PATCH 023/337] simplify whole process --- .../publish/collect_unreal_staticmesh.py | 24 +------ .../publish/extract_unreal_staticmesh.py | 62 ++++++------------- .../validate_unreal_staticmesh_naming.py | 14 +---- 3 files changed, 24 insertions(+), 76 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index faa5880e43..728a26931b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api -from avalon.api import Session -from openpype.api import get_project_settings from pprint import pformat @@ -14,28 +12,12 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["staticMesh"] def process(self, instance): - project_settings = get_project_settings(Session["AVALON_PROJECT"]) - sm_prefix = ( - project_settings - ["maya"] - ["create"] - ["CreateUnrealStaticMesh"] - ["static_mesh_prefix"] - ) - # take the name from instance (without the `staticMesh_` prefix) - instance.data["staticMeshCombinedName"] = "{}_{}".format( - sm_prefix, - instance.data.get("subset")[len(sm_prefix) + 1:]) - - self.log.info("joined mesh name: {}".format( - instance.data.get("staticMeshCombinedName"))) - geometry_set = [i for i in instance if i == "geometry_SET"] - instance.data["membersToCombine"] = cmds.sets( + instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) - self.log.info("joining meshes: {}".format( - pformat(instance.data.get("membersToCombine")))) + self.log.info("geometry: {}".format( + pformat(instance.data.get("geometryMembers")))) collision_set = [i for i in instance if i == "collisions_SET"] instance.data["collisionMembers"] = cmds.sets( diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 987370d395..02dd5dc572 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,56 +22,30 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - to_combine = instance.data.get("membersToCombine") - static_mesh_name = instance.data.get("staticMeshCombinedName") - duplicates = [] + geo = instance.data.get("geometryMembers", []) + members = geo + instance.data.get("collisionMembers", []) - # delete created temporary nodes after extraction - with delete_after() as delete_bin: - # if we have more objects, combine them into one - # or just duplicate the single one - if len(to_combine) > 1: - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) - else: - self.log.info( - "duplicating {} to {} for export".format( - to_combine[0], static_mesh_name) - ) - cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) + fbx_exporter = fbx.FBXExtractor(log=self.log) - delete_bin.extend([static_mesh_name]) - # delete_bin.extend(duplicates) + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) - members = [static_mesh_name] - members += instance.data["collisionMembers"] + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') - fbx_exporter = fbx.FBXExtractor(log=self.log) + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) - # Define output path - staging_dir = self.staging_dir(instance) - filename = "{0}.fbx".format(instance.name) - path = os.path.join(staging_dir, filename) + fbx_exporter.set_options_from_instance(instance) - # The export requires forward slashes because we need - # to format it into a string in a mel expression - path = path.replace('\\', '/') - - self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(members)) - self.log.info("Instance: {0}".format(instance[:])) - - fbx_exporter.set_options_from_instance(instance) - - with maintained_selection(): - with root_parent(members): - self.log.info("Un-parenting: {}".format(members)) - fbx_exporter.export(members, path) + with maintained_selection(): + with root_parent(members): + self.log.info("Un-parenting: {}".format(members)) + fbx_exporter.export(members, path) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 1ecf436582..920e0982dc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -79,18 +79,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["static_mesh_prefix"] ) - to_combine = instance.data.get("membersToCombine") - if not to_combine: - raise ValueError("Missing geometry to export.") - combined_geometry_name = instance.data.get( - "staticMeshCombinedName", None) if cls.validate_mesh: # compile regex for testing names regex_mesh = "{}{}".format( ("_" + cls.static_mesh_prefix) or "", cls.regex_mesh ) sm_r = re.compile(regex_mesh) - if not sm_r.match(combined_geometry_name): + if not sm_r.match(instance.data.get("subset")): cls.log.error("Mesh doesn't comply with name validation.") return True @@ -115,12 +110,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.error("{} is invalid".format(obj)) invalid.append(obj) else: - un_prefixed = combined_geometry_name[ - len(static_mesh_prefix) + 1: - ] expected_collision = "{}_{}".format( cl_m.group("prefix"), - un_prefixed + instance.data.get("subset") ) if not obj.startswith(expected_collision): @@ -133,7 +125,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - un_prefixed, + instance.data.get("subset"), )) invalid.append(obj) From 87b256387d453ce760a13fd6151a67436e7b6ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 14 Mar 2022 22:30:26 +0100 Subject: [PATCH 024/337] fix for multiple subsets --- .../maya/plugins/publish/collect_unreal_staticmesh.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 728a26931b..79d0856fa0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -12,14 +12,20 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["staticMesh"] def process(self, instance): - geometry_set = [i for i in instance if i == "geometry_SET"] + geometry_set = [ + i for i in instance + if i.startswith("geometry_SET") + ] instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) self.log.info("geometry: {}".format( pformat(instance.data.get("geometryMembers")))) - collision_set = [i for i in instance if i == "collisions_SET"] + collision_set = [ + i for i in instance + if i.startswith("collisions_SET") + ] instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) From d31bee0c3e1a1c368dc099cc420a199c67919f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Mar 2022 00:41:46 +0100 Subject: [PATCH 025/337] fix parenting for skeletal meshes --- openpype/hosts/maya/api/fbx.py | 2 +- openpype/hosts/maya/api/lib.py | 20 ++++++++++++++++--- .../publish/extract_unreal_skeletalmesh.py | 19 ++++++++++++------ .../publish/extract_unreal_staticmesh.py | 4 ++-- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index f8fe189589..260241f5fc 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -198,5 +198,5 @@ class FBXExtractor: path (str): Path to use for export. """ - cmds.select(members, r=1, noExpand=True) + cmds.select(members, r=True, noExpand=True) mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 41c67a6209..a5199d8443 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3085,11 +3085,20 @@ def set_colorspace(): @contextlib.contextmanager -def root_parent(nodes): - # type: (list) -> list +def parent_nodes(nodes, parent=None): + # type: (list, str) -> list """Context manager to un-parent provided nodes and return them back.""" import pymel.core as pm # noqa + parent_node = None + delete_parent = False + + if parent: + if not cmds.objExists(parent): + parent_node = pm.createNode("transform", n=parent, ss=False) + delete_parent = True + else: + parent_node = pm.PyNode(parent) node_parents = [] for node in nodes: n = pm.PyNode(node) @@ -3100,9 +3109,14 @@ def root_parent(nodes): node_parents.append((n, root)) try: for node in node_parents: - node[0].setParent(world=True) + if not parent: + node[0].setParent(world=True) + else: + node[0].setParent(parent_node) yield finally: for node in node_parents: if node[1]: node[0].setParent(node[1]) + if delete_parent: + pm.delete(parent_node) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index c0b408c3f0..6f4c70fc07 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -7,9 +7,8 @@ from maya import cmds # noqa import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( - root_parent, - maintained_selection, - delete_after + parent_nodes, + maintained_selection ) from openpype.hosts.maya.api import fbx @@ -43,10 +42,18 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): self.log.info("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) + + parent = "{}{}".format( + instance.data["asset"], + instance.data.get("variant", "") + ) with maintained_selection(): - with root_parent(to_extract): - rooted = [i.split("|")[-1] for i in to_extract] - self.log.info("Un-parenting: {}".format(to_extract)) + with parent_nodes(to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) fbx_exporter.export(rooted, path) if "representations" not in instance.data: diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 02dd5dc572..c3cc322a29 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -7,7 +7,7 @@ from maya import cmds # noqa import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( - root_parent, + parent_nodes, maintained_selection, delete_after ) @@ -43,7 +43,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): fbx_exporter.set_options_from_instance(instance) with maintained_selection(): - with root_parent(members): + with parent_nodes(members): self.log.info("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) From 08370f53e564174c87f1dd9316ecb2dc53898f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Mar 2022 01:01:14 +0100 Subject: [PATCH 026/337] fix getting correct name with prefix --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 920e0982dc..c0eeb82688 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -104,6 +104,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_r = re.compile(regex_collision) + mesh_name = "{}{}".format(instance.data["asset"], + instance.data.get("variant", [])) + for obj in collision_set: cl_m = cl_r.match(obj) if not cl_m: @@ -112,7 +115,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - instance.data.get("subset") + mesh_name ) if not obj.startswith(expected_collision): @@ -121,11 +124,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): "Collision object name doesn't match " "static mesh name" ) - cls.log.error("{}_{} != {}_{}".format( + cls.log.error("{}_{} != {}_{}*".format( cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - instance.data.get("subset"), + mesh_name, )) invalid.append(obj) From c120135f1fa467587a1b41f0e3c6282a2285b200 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Mar 2022 21:08:35 +0100 Subject: [PATCH 027/337] Removed submodule openpype/modules/default_modules/ftrack/python2_vendor/arrow --- openpype/modules/default_modules/ftrack/python2_vendor/arrow | 1 - 1 file changed, 1 deletion(-) delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/arrow diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/arrow b/openpype/modules/default_modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 From d1a733cf885e8955746ef6e71db01b76fe7c96be Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Mar 2022 21:08:43 +0100 Subject: [PATCH 028/337] Removed submodule openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api --- .../default_modules/ftrack/python2_vendor/ftrack-python-api | 1 - 1 file changed, 1 deletion(-) delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e From fdb9f0da77f1a996be4160f52acdc1036238bc39 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Mar 2022 21:08:50 +0100 Subject: [PATCH 029/337] Removed submodule repos/avalon-unreal-integration --- repos/avalon-unreal-integration | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/avalon-unreal-integration diff --git a/repos/avalon-unreal-integration b/repos/avalon-unreal-integration deleted file mode 160000 index 43f6ea9439..0000000000 --- a/repos/avalon-unreal-integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 43f6ea943980b29c02a170942b566ae11f2b7080 From e7c5aa16df8e46090140572cc77c607e6da4e707 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 18 Mar 2022 17:23:26 +0300 Subject: [PATCH 030/337] Fixes comparing against render filename Fixes comparison against AOV pattern match to produce correct "review" flags of the "beauty" pass, for proper generation of review burnin and thumbnail. --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 19d504b6c9..2c8bcdf4fc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -448,7 +448,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = False if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: - if re.match(aov_pattern, aov): + # Matching against the AOV pattern in the render files + # In order to match the AOV name, we must compare against the render filename string + # We are grabbing the render filename string from the collection that we have grabbed from expected files (exp_files) + render_file_name = os.path.basename(col[0]) + if re.match(aov_pattern, render_file_name): preview = True break From 509f3289418a297ac17e3a0de93a987390cd8370 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 18 Mar 2022 17:25:34 +0300 Subject: [PATCH 031/337] Fix regex in global OpenPype Deadline settings Fixes the "beauty" regex for the "reviewable subsets filter" in the Publish Deadline settings. --- .../defaults/project_settings/deadline.json | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 5bb0a4022e..1859b480a1 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -15,33 +15,6 @@ "deadline" ] }, - "ProcessSubmittedJobOnFarm": { - "enabled": true, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50, - "publishing_script": "", - "skip_integration_repre_list": [], - "aov_filter": { - "maya": [ - ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" - ], - "nuke": [ - ".*" - ], - "aftereffects": [ - ".*" - ], - "celaction": [ - ".*" - ], - "harmony": [ - ".*" - ] - } - }, "MayaSubmitDeadline": { "enabled": true, "optional": false, @@ -95,6 +68,33 @@ "group": "", "department": "", "multiprocess": true + }, + "ProcessSubmittedJobOnFarm": { + "enabled": true, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 50, + "publishing_script": "", + "skip_integration_repre_list": [], + "aov_filter": { + "maya": [ + ".*(?:[\\._-])*([Bb]eauty)(?:[\\.|_])*.*" + ], + "nuke": [ + ".*" + ], + "aftereffects": [ + ".*" + ], + "celaction": [ + ".*" + ], + "harmony": [ + ".*" + ] + } } } } \ No newline at end of file From b59100025e22cdc211615e2a868e902dbf8ad832 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 21 Mar 2022 14:31:58 +0300 Subject: [PATCH 032/337] Fix hound comment warning line length for comments adjusted --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 2c8bcdf4fc..2ad1dcd691 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -449,8 +449,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: # Matching against the AOV pattern in the render files - # In order to match the AOV name, we must compare against the render filename string - # We are grabbing the render filename string from the collection that we have grabbed from expected files (exp_files) + # In order to match the AOV name + # we must compare against the render filename string + # We are grabbing the render filename string + # from the collection that we have grabbed from expected files (exp_files) render_file_name = os.path.basename(col[0]) if re.match(aov_pattern, render_file_name): preview = True From 15b1c86e88bd055b815d663047856f019c54f779 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 21 Mar 2022 14:31:58 +0300 Subject: [PATCH 033/337] Fix hound comment warning line length for comments adjusted --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 2c8bcdf4fc..2ad1dcd691 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -449,8 +449,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: # Matching against the AOV pattern in the render files - # In order to match the AOV name, we must compare against the render filename string - # We are grabbing the render filename string from the collection that we have grabbed from expected files (exp_files) + # In order to match the AOV name + # we must compare against the render filename string + # We are grabbing the render filename string + # from the collection that we have grabbed from expected files (exp_files) render_file_name = os.path.basename(col[0]) if re.match(aov_pattern, render_file_name): preview = True From b517fa0ce068ac9ec9f5d52f1430e31e2a69ea63 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 21 Mar 2022 15:13:40 +0300 Subject: [PATCH 034/337] Fix hound warning, again --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 2ad1dcd691..5dd1be1f54 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -452,7 +452,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # In order to match the AOV name # we must compare against the render filename string # We are grabbing the render filename string - # from the collection that we have grabbed from expected files (exp_files) + # from the collection that we have grabbed from exp_files render_file_name = os.path.basename(col[0]) if re.match(aov_pattern, render_file_name): preview = True From 8efa09c6d2d4fc2558ed6e814a00d76981f78a42 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 21 Mar 2022 15:18:21 +0300 Subject: [PATCH 035/337] Simplify regex for "beauty" pass Simplifies the "regex" used to sift for the reviewable pass for thumbnail and burnin --- openpype/settings/defaults/project_settings/deadline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1859b480a1..b2104a04eb 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -80,7 +80,7 @@ "skip_integration_repre_list": [], "aov_filter": { "maya": [ - ".*(?:[\\._-])*([Bb]eauty)(?:[\\.|_])*.*" + ".*([Bb]eauty).*" ], "nuke": [ ".*" From 80e08d43ce3617d57280a04ae426c0d42a595709 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 21 Mar 2022 15:53:39 +0300 Subject: [PATCH 036/337] Test if col[0] is remainder or list for file_name --- .../modules/deadline/plugins/publish/submit_publish_job.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 5dd1be1f54..b1f6f9a485 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -453,7 +453,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # we must compare against the render filename string # We are grabbing the render filename string # from the collection that we have grabbed from exp_files - render_file_name = os.path.basename(col[0]) + if isinstance(col, list): + render_file_name = os.path.basename(col[0]) + else: + render_file_name = os.path.basename(col) if re.match(aov_pattern, render_file_name): preview = True break From cc7a5e0a6fddfb7d0d55dae0f3445251f4d9c5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 22 Mar 2022 15:03:25 +0100 Subject: [PATCH 037/337] node renaming wip --- .../publish/extract_unreal_skeletalmesh.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 6f4c70fc07..58154638e6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Create Unreal Skeletal Mesh data to be extracted as FBX.""" import os +from contextlib import contextmanager from maya import cmds # noqa @@ -12,6 +13,16 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api import fbx +@contextmanager +def renamed(original_name, renamed_name): + # type: (str, str) -> None + try: + cmds.rename(original_name, renamed_name) + yield + finally: + cmds.rename(renamed_name, original_name) + yield + class ExtractUnrealSkeletalMesh(openpype.api.Extractor): """Extract Unreal Skeletal Mesh as FBX from Maya. """ @@ -48,13 +59,14 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): instance.data.get("variant", "") ) with maintained_selection(): - with parent_nodes(to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + with renamed() + with parent_nodes(to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From db03b47b8ee5615bb19ee90f1dc52f8f05cf379b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Mar 2022 15:14:38 +0100 Subject: [PATCH 038/337] hound fix --- openpype/hosts/flame/api/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 7c03186ff0..97f83ccf07 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -146,7 +146,6 @@ __all__ = [ "export_clip", "get_preset_path_by_xml_name", "modify_preset_file", - # batch utils "create_batch" From aab2ed17f8d582056f5835613845d13ea7205b24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Mar 2022 15:15:04 +0100 Subject: [PATCH 039/337] flame: ingegrate batch wip --- .../plugins/publish/integrate_batch_group.py | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index fd88ed318e..aaa405343c 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -1,6 +1,7 @@ import pyblish import openpype.hosts.flame.api as opfapi + @pyblish.api.log class IntegrateBatchGroup(pyblish.api.InstancePlugin): """Integrate published shot to batch group""" @@ -11,4 +12,67 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): families = ["clip"] def process(self, instance): - opfapi.create_batch \ No newline at end of file + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + + asset_name = instance.data["asset"] + write_pref_data = self._get_write_prefs(instance) + + batch_data = { + "shematic_reels": [ + "OP_LoadedReel" + ], + "write_pref": write_pref_data, + "handleStart": handle_start, + "handleEnd": handle_end + } + + opfapi.create_batch(asset_name, frame_start, frame_end, batch_data) + + def _get_write_prefs(self, instance): + # The path attribute where the rendered clip is exported + # /path/to/file.[0001-0010].exr + media_path = "{render_path}".format() + # name of file represented by tokens + media_path_pattern = "_v." + # The Create Open Clip attribute of the Write File node. \ + # Determines if an Open Clip is created by the Write File node. + create_clip = True + # The Include Setup attribute of the Write File node. + # Determines if a Batch Setup file is created by the Write File node. + include_setup = True + # The path attribute where the Open Clip file is exported by + # the Write File node. + create_clip_path = "" + include_setup_path = None + # The file type for the files written by the Write File node. + # Setting this attribute also overwrites format_extension, + # bit_depth and compress_mode to match the defaults for + # this file type. + file_type = "OpenEXR" + # The bit depth for the files written by the Write File node. + # This attribute resets to match file_type whenever file_type is set. + bit_depth = "16" + frame_index_mode = None + frame_padding = 0 + # The versioning mode of the Open Clip exported by the Write File node. + # Only available if create_clip = True. + version_mode = "Follow Iteration" + version_name = "v" + + return { + "media_path": media_path, + "media_path_pattern": media_path_pattern, + "create_clip": create_clip, + "include_setup": include_setup, + "create_clip_path": create_clip_path, + "include_setup_path": include_setup_path, + "file_type": file_type, + "bit_depth": bit_depth, + "frame_index_mode": frame_index_mode, + "frame_padding": frame_padding, + "version_mode": version_mode, + "version_name": version_name + } From 8f77e92d6f456e06c98326c4d0c55b6cb4f70208 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Mar 2022 16:48:16 +0100 Subject: [PATCH 040/337] rename top node for variants --- .../publish/extract_unreal_skeletalmesh.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 58154638e6..5b0eb5a3bc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -13,6 +13,7 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api import fbx + @contextmanager def renamed(original_name, renamed_name): # type: (str, str) -> None @@ -21,7 +22,6 @@ def renamed(original_name, renamed_name): yield finally: cmds.rename(renamed_name, original_name) - yield class ExtractUnrealSkeletalMesh(openpype.api.Extractor): @@ -42,6 +42,8 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): geo = instance.data.get("geometry") joints = instance.data.get("joints") + joints_parent = cmds.listRelatives(joints, p=True) + to_extract = geo + joints # The export requires forward slashes because we need @@ -54,19 +56,30 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): fbx_exporter.set_options_from_instance(instance) + # This magic is done for variants. To let Unreal merge correctly + # existing data, top node must have the same name. So for every + # variant we extract we need to rename top node of the rig correctly. + # It is finally done in context manager so it won't affect current + # scene. parent = "{}{}".format( instance.data["asset"], instance.data.get("variant", "") ) - with maintained_selection(): - with renamed() - with parent_nodes(to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + + renamed_to_extract = [] + for node in to_extract: + node_path = node.split("|") + node_path[1] = parent + renamed_to_extract.append("|".join(node_path)) + + with renamed(joints_parent, parent): + with parent_nodes(renamed_to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in renamed_to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From 12aeb88e0a2cd7ccfa6dae9dd7f20d83bf4577b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Mar 2022 17:13:11 +0100 Subject: [PATCH 041/337] flame: integrate batch [wip] --- .../plugins/publish/integrate_batch_group.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index aaa405343c..780531287b 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -1,3 +1,4 @@ +import os import pyblish import openpype.hosts.flame.api as opfapi @@ -32,9 +33,12 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): opfapi.create_batch(asset_name, frame_start, frame_end, batch_data) def _get_write_prefs(self, instance): + shot_path = instance.data[""] + render_dir_path = os.path.join( + shot_path, "work", task, "render", "flame") # The path attribute where the rendered clip is exported # /path/to/file.[0001-0010].exr - media_path = "{render_path}".format() + media_path = render_dir_path # name of file represented by tokens media_path_pattern = "_v." # The Create Open Clip attribute of the Write File node. \ @@ -46,17 +50,33 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # The path attribute where the Open Clip file is exported by # the Write File node. create_clip_path = "" - include_setup_path = None + # The path attribute where the Batch setup file + # is exported by the Write File node. + include_setup_path = "./_v" # The file type for the files written by the Write File node. # Setting this attribute also overwrites format_extension, # bit_depth and compress_mode to match the defaults for # this file type. file_type = "OpenEXR" + # The file extension for the files written by the Write File node. + # This attribute resets to match file_type whenever file_type + # is set. If you require a specific extension, you must + # set format_extension after setting file_type. + format_extension = "exr" # The bit depth for the files written by the Write File node. # This attribute resets to match file_type whenever file_type is set. bit_depth = "16" - frame_index_mode = None - frame_padding = 0 + # The compressing attribute for the files exported by the Write + # File node. Only relevant when file_type in 'OpenEXR', 'Sgi', 'Tiff' + compress = True + # The compression format attribute for the specific File Types + # export by the Write File node. You must set compress_mode + # after setting file_type. + compress_mode = "DWAB" + # The frame index mode attribute of the Write File node. + # Value range: `Use Timecode` or `Use Start Frame` + frame_index_mode = "Use Start Frame" + frame_padding = 6 # The versioning mode of the Open Clip exported by the Write File node. # Only available if create_clip = True. version_mode = "Follow Iteration" @@ -70,7 +90,10 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): "create_clip_path": create_clip_path, "include_setup_path": include_setup_path, "file_type": file_type, + "format_extension": format_extension, "bit_depth": bit_depth, + "compress": compress, + "compress_mode": compress_mode, "frame_index_mode": frame_index_mode, "frame_padding": frame_padding, "version_mode": version_mode, From 7dd0c86a17f0a95ac6e16416473978169cf793a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Mar 2022 17:11:48 +0100 Subject: [PATCH 042/337] flame: collect timeline instances settings --- .../defaults/project_settings/flame.json | 31 +++++++ .../projects_schema/schema_project_flame.json | 81 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index c7188b10b5..939752c778 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -20,6 +20,37 @@ } }, "publish": { + "CollectTimelineInstances": { + "xml_preset_attrs_from_comments": [ + { + "name": "width", + "type": "number" + }, + { + "name": "height", + "type": "number" + }, + { + "name": "pixelRatio", + "type": "float" + }, + { + "name": "resizeType", + "type": "string" + }, + { + "name": "resizeFilter", + "type": "string" + } + ], + "add_tasks": [ + { + "name": "compositing", + "type": "Compositing", + "create_batch_group": true + } + ] + }, "ExtractSubsetResources": { "keep_original_representation": false, "export_presets_mapping": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index e352f8b132..8057b07d9c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -136,6 +136,87 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectTimelineInstances", + "label": "Collect Timeline Instances", + "is_group": true, + "children": [ + { + "type": "collapsible-wrap", + "label": "XML presets attributes parsable from segment comments", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "list", + "key": "xml_preset_attrs_from_comments", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "name", + "label": "Attribute name" + }, + { + "key": "type", + "label": "Attribute type", + "type": "enum", + "default": "number", + "enum_items": [ + { + "number": "number" + }, + { + "float": "float" + }, + { + "string": "string" + } + ] + } + ] + } + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Add tasks", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "list", + "key": "add_tasks", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "name", + "label": "Task name" + }, + { + "key": "type", + "label": "Task type", + "multiselection": false, + "type": "task-types-enum" + }, + { + "type": "boolean", + "key": "create_batch_group", + "label": "Create batch group" + } + ] + } + } + ] + } + ] + }, { "type": "dict", "collapsible": true, From 074703f8cff5b4432a98d1fb28f9a6f42943c694 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Mar 2022 17:13:09 +0100 Subject: [PATCH 043/337] flame: use settings in collect timeline instances --- .../publish/collect_timeline_instances.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 70340ad7a2..94348601b2 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -21,15 +21,9 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): audio_track_items = [] - # TODO: add to settings # settings - xml_preset_attrs_from_comments = { - "width": "number", - "height": "number", - "pixelRatio": "float", - "resizeType": "string", - "resizeFilter": "string" - } + xml_preset_attrs_from_comments = [] + add_tasks = [] def process(self, context): project = context.data["flameProject"] @@ -106,7 +100,11 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): "fps": self.fps, "flameSourceClip": source_clip, "sourceFirstFrame": int(first_frame), - "path": file_path + "path": file_path, + "flameAddTasks": self.add_tasks, + "tasks": { + task["name"]: {"type": task["type"]} + for task in self.add_tasks} }) # get otio clip data @@ -181,14 +179,17 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # split to key and value key, value = split.split(":") - for a_name, a_type in self.xml_preset_attrs_from_comments.items(): + for attr_data in self.xml_preset_attrs_from_comments: + a_name = attr_data["name"] + a_type = attr_data["type"] + # exclude all not related attributes if a_name.lower() not in key.lower(): continue # get pattern defined by type pattern = TXT_PATERN - if a_type in ("number" , "float"): + if a_type in ("number", "float"): pattern = NUM_PATERN res_goup = pattern.findall(value) From 029135acf5513756ffc3e01fafa66e8ddd565d32 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Mar 2022 20:38:46 +0100 Subject: [PATCH 044/337] flame: integrator bath group [finishing] --- openpype/hosts/flame/api/batch_utils.py | 12 +++- .../plugins/publish/integrate_batch_group.py | 65 +++++++++++++++---- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index a1fe7961c4..d309c5985d 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -37,8 +37,12 @@ def create_batch(name, frame_start, frame_end, **kwargs): comp_node = flame.batch.create_node("Comp") + # TODO: convert this to iterational processing, + # so it could be driven from `imageio` settigns # create write node write_node = flame.batch.create_node('Write File') + # assign attrs + write_node.name = write_pref["name"] write_node.media_path = write_pref["media_path"] write_node.media_path_pattern = write_pref["media_path_pattern"] write_node.create_clip = write_pref["create_clip"] @@ -46,11 +50,15 @@ def create_batch(name, frame_start, frame_end, **kwargs): write_node.create_clip_path = write_pref["create_clip_path"] write_node.include_setup_path = write_pref["include_setup_path"] write_node.file_type = write_pref["file_type"] + write_node.format_extension = write_pref["format_extension"] write_node.bit_depth = write_pref["bit_depth"] + write_node.compress = write_pref["compress"] + write_node.compress_mode = write_pref["compress_mode"] write_node.frame_index_mode = write_pref["frame_index_mode"] - write_node.frame_padding = int(write_pref["frame_padding"]) + write_node.frame_padding = write_pref["frame_padding"] + write_node.version_mode = write_pref["version_mode"] + write_node.version_name = write_pref["version_name"] - # connect nodes flame.batch.connect_nodes(comp_node, "Result", write_node, "Front") # sort batch nodes diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 780531287b..808c059816 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -1,5 +1,7 @@ import os +from pprint import pformat import pyblish +from openpype.lib import get_workdir import openpype.hosts.flame.api as opfapi @@ -17,25 +19,52 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): frame_end = instance.data["frameEnd"] handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - asset_name = instance.data["asset"] - write_pref_data = self._get_write_prefs(instance) + add_tasks = instance.data["flameAddTasks"] - batch_data = { - "shematic_reels": [ - "OP_LoadedReel" - ], - "write_pref": write_pref_data, - "handleStart": handle_start, - "handleEnd": handle_end - } + # iterate all tasks from settings + for task_data in add_tasks: + # exclude batch group + if not task_data["create_batch_group"]: + continue + task_name = task_data["name"] + batchgroup_name = "{}_{}".format(asset_name, task_name) + write_pref_data = self._get_write_prefs(instance, task_data) - opfapi.create_batch(asset_name, frame_start, frame_end, batch_data) + batch_data = { + "shematic_reels": [ + "OP_LoadedReel" + ], + "write_pref": write_pref_data, + "handleStart": handle_start, + "handleEnd": handle_end + } + self.log.debug( + "__ batch_data: {}".format(pformat(batch_data))) - def _get_write_prefs(self, instance): - shot_path = instance.data[""] + # create batch with utils + opfapi.create_batch( + batchgroup_name, + frame_start, + frame_end, + batch_data + ) + + def _get_write_prefs(self, instance, task_data): + anatomy_data = instance.data["anatomyData"] + + task_workfile_path = self._get_shot_task_dir_path(instance, task_data) + self.log.debug("__ task_workfile_path: {}".format(task_workfile_path)) + + # TODO: this might be done with template in settings render_dir_path = os.path.join( - shot_path, "work", task, "render", "flame") + task_workfile_path, "render", "flame") + + # TODO: add most of these to `imageio/flame/batch/write_node` + name = "{project[code]}_{asset}_{task[name]}".format( + **anatomy_data + ) + # The path attribute where the rendered clip is exported # /path/to/file.[0001-0010].exr media_path = render_dir_path @@ -83,6 +112,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): version_name = "v" return { + "name": name, "media_path": media_path, "media_path_pattern": media_path_pattern, "create_clip": create_clip, @@ -99,3 +129,10 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): "version_mode": version_mode, "version_name": version_name } + + def _get_shot_task_dir_path(self, instance, task_data): + project_doc = instance.data["projectEntity"] + asset_entity = instance.data["assetEntity"] + + return get_workdir( + project_doc, asset_entity, task_data["name"], "flame") From 9fb6d7a7230149b9d1c7b273eb8fcc532a84a3fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 12:04:29 +0100 Subject: [PATCH 045/337] flame: batchgroup uses duration --- openpype/hosts/flame/api/batch_utils.py | 14 ++++++-------- .../flame/plugins/publish/integrate_batch_group.py | 5 +++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index d309c5985d..a47d62a10e 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -1,7 +1,7 @@ import flame -def create_batch(name, frame_start, frame_end, **kwargs): +def create_batch(name, frame_start, frame_duration, **kwargs): """Create Batch Group in active project's Desktop Args: @@ -13,20 +13,18 @@ def create_batch(name, frame_start, frame_end, **kwargs): shelf_reels = kwargs.get("shelf_reels") or ['ShelfReel1'] write_pref = kwargs["write_pref"] - handle_start = kwargs.get("handleStart") - handle_end = kwargs.get("handleEnd") + handle_start = kwargs.get("handleStart") or 0 + handle_end = kwargs.get("handleEnd") or 0 - if handle_start: - frame_start -= handle_start - if handle_end: - frame_end += handle_end + frame_start -= handle_start + frame_duration += handle_start + handle_end # Create batch group with name, start_frame value, duration value, # set of schematic reel names, set of shelf reel names flame.batch.create_batch_group( name, start_frame=frame_start, - duration=frame_end, + duration=frame_duration, reels=schematic_reels, shelf_reels=shelf_reels ) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 808c059816..0a21d6ca2d 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -19,6 +19,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): frame_end = instance.data["frameEnd"] handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] + frame_duration = (frame_end - frame_start) + 1 asset_name = instance.data["asset"] add_tasks = instance.data["flameAddTasks"] @@ -46,8 +47,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): opfapi.create_batch( batchgroup_name, frame_start, - frame_end, - batch_data + frame_duration, + **batch_data ) def _get_write_prefs(self, instance, task_data): From a87f778f1e95d16d09097d923ca3f1d519e86126 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 12:56:18 +0100 Subject: [PATCH 046/337] flame: reuse batch groups --- openpype/hosts/flame/api/__init__.py | 4 ++- openpype/hosts/flame/api/lib.py | 9 +++++++ .../plugins/publish/integrate_batch_group.py | 26 ++++++++++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 97f83ccf07..561aaab3de 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -29,7 +29,8 @@ from .lib import ( get_frame_from_filename, get_padding_from_filename, maintained_object_duplication, - get_clip_segment + get_clip_segment, + get_batch_group_from_desktop ) from .utils import ( setup, @@ -105,6 +106,7 @@ __all__ = [ "get_padding_from_filename", "maintained_object_duplication", "get_clip_segment", + "get_batch_group_from_desktop", # pipeline "install", diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 74d9e7607a..9a6b86209d 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -708,3 +708,12 @@ def get_clip_segment(flame_clip): raise ValueError("Clip `{}` has too many segments!".format(name)) return segments[0] + + +def get_batch_group_from_desktop(name): + project = get_current_project() + project_desktop = project.current_workspace.desktop + + for bgroup in project_desktop.batch_groups: + if bgroup.name.get_value() == name: + return bgroup diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 0a21d6ca2d..3a8173791a 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -43,13 +43,25 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): self.log.debug( "__ batch_data: {}".format(pformat(batch_data))) - # create batch with utils - opfapi.create_batch( - batchgroup_name, - frame_start, - frame_duration, - **batch_data - ) + # check if the batch group already exists + bgroup = opfapi.get_batch_group_from_desktop(batchgroup_name) + + if not bgroup: + self.log.info( + "Creating new batch group: {}".format(batchgroup_name)) + # create batch with utils + opfapi.create_batch( + batchgroup_name, + frame_start, + frame_duration, + **batch_data + ) + else: + self.log.info( + "Updating batch group: {}".format(batchgroup_name)) + # update already created batch group + bgroup.start_frame = frame_start + bgroup.duration = frame_duration def _get_write_prefs(self, instance, task_data): anatomy_data = instance.data["anatomyData"] From c26ff2ab544fadc7121d54ec49a2b35433e6122a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 12:56:36 +0100 Subject: [PATCH 047/337] flame: fix task name on write file node --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 3a8173791a..af2b0fad65 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -65,6 +65,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): def _get_write_prefs(self, instance, task_data): anatomy_data = instance.data["anatomyData"] + # update task data in anatomy data + anatomy_data.update(task_data) task_workfile_path = self._get_shot_task_dir_path(instance, task_data) self.log.debug("__ task_workfile_path: {}".format(task_workfile_path)) From 590e966a7d18a4c1f7dea0e08a3056a202607670 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 14:29:49 +0100 Subject: [PATCH 048/337] flame: updating anatomy data with correct task data --- .../plugins/publish/integrate_batch_group.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index af2b0fad65..c54eeec05c 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -1,4 +1,5 @@ import os +import copy from pprint import pformat import pyblish from openpype.lib import get_workdir @@ -63,10 +64,28 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): bgroup.start_frame = frame_start bgroup.duration = frame_duration - def _get_write_prefs(self, instance, task_data): - anatomy_data = instance.data["anatomyData"] + def _get_anamoty_data_with_current_task(self, instance, task_data): + anatomy_data = copy.deepcopy(instance.data["anatomyData"]) + task_name = task_data["name"] + task_type = task_data["type"] + anatomy_obj = instance.context.data["anatomy"] + # update task data in anatomy data - anatomy_data.update(task_data) + project_task_types = anatomy_obj["tasks"] + task_code = project_task_types.get(task_type, {}).get("short_name") + anatomy_data.update({ + "task": { + "name": task_name, + "type": task_type, + "short": task_code + } + }) + return anatomy_data + + def _get_write_prefs(self, instance, task_data): + # update task in anatomy data + anatomy_data = self._get_anamoty_data_with_current_task( + instance, task_data) task_workfile_path = self._get_shot_task_dir_path(instance, task_data) self.log.debug("__ task_workfile_path: {}".format(task_workfile_path)) From c6cfdfbd3aac60dd8469048354b423f39edd4e9a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 24 Mar 2022 16:41:39 +0300 Subject: [PATCH 049/337] Added new file `flagging.py` to new farm directory --- openpype/pipeline/farm/flagging.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openpype/pipeline/farm/flagging.py diff --git a/openpype/pipeline/farm/flagging.py b/openpype/pipeline/farm/flagging.py new file mode 100644 index 0000000000..e69de29bb2 From d5521ae8407a9f9bb4d1f05e8a1ef048700acf45 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 15:37:05 +0100 Subject: [PATCH 050/337] flame: adding loading of plate into integrator --- openpype/hosts/flame/api/batch_utils.py | 4 +- .../plugins/publish/integrate_batch_group.py | 87 +++++++++++-------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index a47d62a10e..99e053faf1 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -21,7 +21,7 @@ def create_batch(name, frame_start, frame_duration, **kwargs): # Create batch group with name, start_frame value, duration value, # set of schematic reel names, set of shelf reel names - flame.batch.create_batch_group( + bgroup = flame.batch.create_batch_group( name, start_frame=frame_start, duration=frame_duration, @@ -61,3 +61,5 @@ def create_batch(name, frame_start, frame_duration, **kwargs): # sort batch nodes flame.batch.organize() + + return bgroup diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index c54eeec05c..97b456c18c 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -16,12 +16,6 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): families = ["clip"] def process(self, instance): - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - frame_duration = (frame_end - frame_start) + 1 - asset_name = instance.data["asset"] add_tasks = instance.data["flameAddTasks"] # iterate all tasks from settings @@ -29,40 +23,59 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # exclude batch group if not task_data["create_batch_group"]: continue - task_name = task_data["name"] - batchgroup_name = "{}_{}".format(asset_name, task_name) - write_pref_data = self._get_write_prefs(instance, task_data) - batch_data = { - "shematic_reels": [ - "OP_LoadedReel" - ], - "write_pref": write_pref_data, - "handleStart": handle_start, - "handleEnd": handle_end - } - self.log.debug( - "__ batch_data: {}".format(pformat(batch_data))) + # create or get already created batch group + bgroup = self._get_batch_group(instance, task_data) - # check if the batch group already exists - bgroup = opfapi.get_batch_group_from_desktop(batchgroup_name) + # load plate to batch group + self.log.info("Loading subset `{}` into batch `{}`".format( + instance.data["subset"], bgroup.name.get_value() + )) - if not bgroup: - self.log.info( - "Creating new batch group: {}".format(batchgroup_name)) - # create batch with utils - opfapi.create_batch( - batchgroup_name, - frame_start, - frame_duration, - **batch_data - ) - else: - self.log.info( - "Updating batch group: {}".format(batchgroup_name)) - # update already created batch group - bgroup.start_frame = frame_start - bgroup.duration = frame_duration + def _get_batch_group(self, instance, task_data): + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + frame_duration = (frame_end - frame_start) + 1 + asset_name = instance.data["asset"] + + task_name = task_data["name"] + batchgroup_name = "{}_{}".format(asset_name, task_name) + write_pref_data = self._get_write_prefs(instance, task_data) + + batch_data = { + "shematic_reels": [ + "OP_LoadedReel" + ], + "write_pref": write_pref_data, + "handleStart": handle_start, + "handleEnd": handle_end + } + self.log.debug( + "__ batch_data: {}".format(pformat(batch_data))) + + # check if the batch group already exists + bgroup = opfapi.get_batch_group_from_desktop(batchgroup_name) + + if not bgroup: + self.log.info( + "Creating new batch group: {}".format(batchgroup_name)) + # create batch with utils + bgroup = opfapi.create_batch( + batchgroup_name, + frame_start, + frame_duration, + **batch_data + ) + else: + self.log.info( + "Updating batch group: {}".format(batchgroup_name)) + # update already created batch group + bgroup.start_frame = frame_start + bgroup.duration = frame_duration + + return bgroup def _get_anamoty_data_with_current_task(self, instance, task_data): anatomy_data = copy.deepcopy(instance.data["anatomyData"]) From 38268bc83102964c22db15129516a32ba5d5f455 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 16:28:46 +0100 Subject: [PATCH 051/337] flame: let extractor drive loading to batch group --- .../settings/defaults/project_settings/flame.json | 4 +++- .../projects_schema/schema_project_flame.json | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 939752c778..a2b9bef103 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -62,7 +62,9 @@ "ignore_comment_attrs": false, "colorspace_out": "ACES - ACEScg", "representation_add_range": true, - "representation_tags": [] + "representation_tags": [], + "load_to_batch_group": true, + "batch_group_loader_name": "LoadClip" } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 8057b07d9c..c991577799 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -302,6 +302,20 @@ "type": "text", "multiline": false } + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "load_to_batch_group", + "label": "Load to batch group reel", + "default": false + }, + { + "type": "text", + "key": "batch_group_loader_name", + "label": "Use loader name" } ] } From 0407465ee1a2438d8a84d9d0704bb38dd56c1a2c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 17:24:21 +0100 Subject: [PATCH 052/337] flame: add loadable arguments to extracted repres --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 32f6b9508f..7c29bcf944 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -211,7 +211,11 @@ class ExtractSubsetResources(openpype.api.Extractor): "tags": repre_tags, "data": { "colorspace": color_out - } + }, + "load_to_batch_group": preset_config.get( + "load_to_batch_group"), + "batch_group_loader_name": preset_config.get( + "batch_group_loader_name") } # collect all available content of export dir From 638864150493e0f46b2fd41a6fbe0609434dc536 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 17:24:43 +0100 Subject: [PATCH 053/337] flame: finalize loading procedure in batch integrator --- .../plugins/publish/integrate_batch_group.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 97b456c18c..62211d7ace 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -4,6 +4,7 @@ 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 @pyblish.api.log @@ -15,6 +16,9 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): hosts = ["flame"] families = ["clip"] + # settings + default_loader = "LoadClip" + def process(self, instance): add_tasks = instance.data["flameAddTasks"] @@ -31,6 +35,77 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): self.log.info("Loading subset `{}` into batch `{}`".format( instance.data["subset"], bgroup.name.get_value() )) + self._load_clip_to_context(instance, bgroup) + + def _load_clip_to_context(self, instance, bgroup): + # get all loaders for host + loaders = op_pipeline.discover_loader_plugins() + + # get all published representations + published_representations = instance.data["published_representations"] + + # get all loadable representations + representations = instance.data["representations"] + + # get repre_id for the loadable representations + loadable_representations = [ + { + "name": _repr["name"], + "loader": _repr.get("batch_group_loader_name"), + # match loader to the loadable representation + "_id": next( + ( + id + for id, repr in published_representations.items() + if repr["representation"]["name"] == _repr["name"] + ), + None + ) + } + for _repr in representations + if _repr.get("load_to_batch_group") is not None + ] + + # get representation context from the repre_id + representation_ids = [ + repre["_id"] + for repre in loadable_representations + if repre["_id"] is not None + ] + repre_contexts = op_pipeline.load.get_repres_contexts( + representation_ids) + + # loop all returned repres from repre_context dict + for repre_id, repre_context in repre_contexts.items(): + # get loader name by representation id + loader_name = next( + ( + repr["loader"] + for repr in loadable_representations + if repr["_id"] == repre_id + ), + self.default_loader + ) + # get loader plugin + Loader = next( + ( + loader_plugin + for loader_plugin in loaders + if loader_plugin.__name__ == loader_name + ), + None + ) + if Loader: + # load to flame by representation context + op_pipeline.load.load_with_repre_context(Loader, repre_context) + else: + self.log.warning( + "Something got wrong and there is not Loader found for " + "following data: {}".format( + pformat(loadable_representations)) + ) + + def _get_batch_group(self, instance, task_data): frame_start = instance.data["frameStart"] From cde1caaa9180fcc7e4165995e9760429f2a55e07 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 17:40:27 +0100 Subject: [PATCH 054/337] flame: clean args types --- .../publish/extract_subset_resources.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 7c29bcf944..00b87c05a0 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -108,6 +108,18 @@ class ExtractSubsetResources(openpype.api.Extractor): ignore_comment_attrs = preset_config["ignore_comment_attrs"] color_out = preset_config["colorspace_out"] + # get attribures related loading in integrate_batch_group + load_to_batch_group = preset_config.get( + "load_to_batch_group") + batch_group_loader_name = preset_config.get( + "batch_group_loader_name") + + # convert to None if empty string + if batch_group_loader_name: + batch_group_loader_name = str(batch_group_loader_name) + if batch_group_loader_name == "": + batch_group_loader_name = None + # get frame range with handles for representation range frame_start_handle = frame_start - handle_start source_duration_handles = ( @@ -212,10 +224,8 @@ class ExtractSubsetResources(openpype.api.Extractor): "data": { "colorspace": color_out }, - "load_to_batch_group": preset_config.get( - "load_to_batch_group"), - "batch_group_loader_name": preset_config.get( - "batch_group_loader_name") + "load_to_batch_group": load_to_batch_group, + "batch_group_loader_name": batch_group_loader_name } # collect all available content of export dir From 4cfd22b6393b3a4d7e5c18046f7d0340ce124e27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 17:47:06 +0100 Subject: [PATCH 055/337] flame: improving loading with exception --- .../hosts/flame/plugins/publish/integrate_batch_group.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 62211d7ace..08632c3018 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -97,7 +97,13 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): ) if Loader: # load to flame by representation context - op_pipeline.load.load_with_repre_context(Loader, repre_context) + try: + op_pipeline.load.load_with_repre_context( + Loader, repre_context) + except op_pipeline.load.IncompatibleLoaderError as msg: + self.log.error( + "Check allowed representations for Loader `{}` " + "in settings > error: {}".format(Loader.__name__, msg)) else: self.log.warning( "Something got wrong and there is not Loader found for " From a4f8cdb76962f6f4c9e4efef49079c3926e486e6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Mar 2022 17:51:22 +0100 Subject: [PATCH 056/337] flame: better logging for loading fail --- .../hosts/flame/plugins/publish/integrate_batch_group.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 08632c3018..f1049e4697 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -103,7 +103,14 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): except op_pipeline.load.IncompatibleLoaderError as msg: self.log.error( "Check allowed representations for Loader `{}` " - "in settings > error: {}".format(Loader.__name__, msg)) + "in settings > error: {}".format( + Loader.__name__, msg)) + self.log.error( + "Representaton context >>{}<< is not compatible " + "with loader `{}`".format( + pformat(repre_context), Loader.__name__ + ) + ) else: self.log.warning( "Something got wrong and there is not Loader found for " From 76bc7799f1dbcc993b5dbd2868f9309bd3e7e234 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Mar 2022 10:52:54 +0100 Subject: [PATCH 057/337] add top node validator --- .../publish/extract_unreal_staticmesh.py | 5 +-- .../help/validate_skeletalmesh_hierarchy.xml | 14 ++++++++ .../validate_skeletalmesh_hierarchy.py | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index c3cc322a29..92fa1b5933 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,8 +22,9 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - geo = instance.data.get("geometryMembers", []) - members = geo + instance.data.get("collisionMembers", []) + members = instance.data.get("geometryMembers", []) + if instance.data.get("collisionMembers"): + members = members + instance.data.get("collisionMembers") fbx_exporter = fbx.FBXExtractor(log=self.log) diff --git a/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml new file mode 100644 index 0000000000..d30c4cb69d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml @@ -0,0 +1,14 @@ + + + +Skeletal Mesh Top Node +## Skeletal meshes needs common root + +Skeletal meshes and their joints must be under one common root. + +### How to repair? + +Make sure all geometry and joints resides under same root. + + + diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py new file mode 100644 index 0000000000..dda7e063f6 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import pyblish.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError + +from maya import cmds + + +class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): + """Adheres to the content of 'model' family + + - Must have one top group. (configurable) + - Must only contain: transforms, meshes and groups + + """ + + order = openpype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["skeletalMesh"] + label = "Skeletal Mesh Top Node" + + def process(self, instance): + geo = instance.data.get("geometry") + joints = instance.data.get("joints") + joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] + geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] + + self.log.info(joints_parents) + self.log.info(geo_parents) + self.log.info(set(joints_parents + geo_parents)) + + if len(set(joints_parents + geo_parents)) != 1: + raise PublishXmlValidationError( + self, + "Multiple roots on geometry or joints." + ) From c210d3914efd9cd9d3657b054c01ec3dd28ce4d1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 14:51:59 +0300 Subject: [PATCH 058/337] Refactor function for matching AOV into new file --- openpype/pipeline/farm/patterning.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 openpype/pipeline/farm/patterning.py diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py new file mode 100644 index 0000000000..3138dd6873 --- /dev/null +++ b/openpype/pipeline/farm/patterning.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import os +import re + +def match_aov_pattern(self, app, render_file_name): + """Matching against a AOV pattern in the render files + In order to match the AOV name + we must compare against the render filename string + that we are grabbing the render filename string + from the collection that we have grabbed from exp_files. + """ + + if app in self.aov_filter.keys(): + for aov_pattern in self.aov_filter[app]: + if re.match(aov_pattern, render_file_name): + preview = True + return preview \ No newline at end of file From b9f5bb3a7be9c0f167694115c51ad8fdc84bd357 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 14:54:32 +0300 Subject: [PATCH 059/337] Cleanup placement --- .../plugins/publish/submit_publish_job.py | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index b1f6f9a485..7f65011864 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -8,6 +8,7 @@ from copy import copy, deepcopy import requests import clique import openpype.api +from openpype.pipeline.farm.patterning import match_aov_pattern from avalon import api, io @@ -446,21 +447,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): app = os.environ.get("AVALON_APP", "") preview = False - if app in self.aov_filter.keys(): - for aov_pattern in self.aov_filter[app]: - # Matching against the AOV pattern in the render files - # In order to match the AOV name - # we must compare against the render filename string - # We are grabbing the render filename string - # from the collection that we have grabbed from exp_files - if isinstance(col, list): - render_file_name = os.path.basename(col[0]) - else: - render_file_name = os.path.basename(col) - if re.match(aov_pattern, render_file_name): - preview = True - break + if isinstance(col, list): + render_file_name = os.path.basename(col[0]) + else: + render_file_name = os.path.basename(col) + + preview = match_aov_pattern(self, app, render_file_name) + + + if instance_data.get("multipartExr"): preview = True @@ -532,18 +528,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): for collection in collections: ext = collection.tail.lstrip(".") preview = False + render_file_name = list(collection[0]) + app = os.environ.get("AVALON_APP", "") # if filtered aov name is found in filename, toggle it for # preview video rendering - for app in self.aov_filter.keys(): - if os.environ.get("AVALON_APP", "") == app: - for aov in self.aov_filter[app]: - if re.match( - aov, - list(collection)[0] - ): - preview = True - break - + preview = match_aov_pattern(self, app, render_file_name) # toggle preview on if multipart is on if instance.get("multipartExr", False): preview = True From f175d77d006d145b1d2a88cdb71e78b26882c9af Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 14:57:50 +0300 Subject: [PATCH 060/337] remove unused import --- openpype/pipeline/farm/patterning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 3138dd6873..e62362b0ba 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import os import re def match_aov_pattern(self, app, render_file_name): @@ -9,7 +8,8 @@ def match_aov_pattern(self, app, render_file_name): that we are grabbing the render filename string from the collection that we have grabbed from exp_files. """ - + + if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: if re.match(aov_pattern, render_file_name): From d40429d5e7d6a70a6402bc89e2d8616d1abaf2d2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 14:58:30 +0300 Subject: [PATCH 061/337] remove empty lines --- openpype/pipeline/farm/patterning.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index e62362b0ba..7e717a9fff 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -8,8 +8,6 @@ def match_aov_pattern(self, app, render_file_name): that we are grabbing the render filename string from the collection that we have grabbed from exp_files. """ - - if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: if re.match(aov_pattern, render_file_name): From 911cfb2a94f1b330531b2905782500c5e8ecc5e2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 15:04:03 +0300 Subject: [PATCH 062/337] adds empty line --- openpype/pipeline/farm/patterning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 7e717a9fff..0ee8499e73 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- import re + def match_aov_pattern(self, app, render_file_name): """Matching against a AOV pattern in the render files In order to match the AOV name we must compare against the render filename string that we are grabbing the render filename string - from the collection that we have grabbed from exp_files. + from the collection that we have grabbed from exp_files. """ if app in self.aov_filter.keys(): for aov_pattern in self.aov_filter[app]: From 2429e5c07f792d998fc4def930e6b8288e3ed340 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 15:24:03 +0300 Subject: [PATCH 063/337] style fixes --- openpype/pipeline/farm/patterning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 0ee8499e73..308546a1c9 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -5,7 +5,7 @@ import re def match_aov_pattern(self, app, render_file_name): """Matching against a AOV pattern in the render files In order to match the AOV name - we must compare against the render filename string + we must compare against the render filename string that we are grabbing the render filename string from the collection that we have grabbed from exp_files. """ From 6e70c412e9866da08b859751088f6265371cd3ef Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 25 Mar 2022 16:12:16 +0300 Subject: [PATCH 064/337] Remove unused file --- openpype/pipeline/farm/flagging.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/pipeline/farm/flagging.py diff --git a/openpype/pipeline/farm/flagging.py b/openpype/pipeline/farm/flagging.py deleted file mode 100644 index e69de29bb2..0000000000 From 585d53deee223f359e6732620ea0188f8d00ec5c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 25 Mar 2022 15:21:22 +0100 Subject: [PATCH 065/337] flame: improving loading in integrate batch plugin --- .../plugins/publish/extract_subset_resources.py | 6 ++---- .../flame/plugins/publish/integrate_batch_group.py | 14 +++++++++----- 2 files changed, 11 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 00b87c05a0..31f7b6d574 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -115,10 +115,8 @@ class ExtractSubsetResources(openpype.api.Extractor): "batch_group_loader_name") # convert to None if empty string - if batch_group_loader_name: - batch_group_loader_name = str(batch_group_loader_name) - if batch_group_loader_name == "": - batch_group_loader_name = None + if batch_group_loader_name == "": + batch_group_loader_name = None # get frame range with handles for representation range frame_start_handle = frame_start - handle_start diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index f1049e4697..81b304ff0b 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -66,6 +66,9 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): if _repr.get("load_to_batch_group") is not None ] + self.log.debug("__ loadable_representations: {}".format(pformat( + loadable_representations))) + # get representation context from the repre_id representation_ids = [ repre["_id"] @@ -75,17 +78,20 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): repre_contexts = op_pipeline.load.get_repres_contexts( representation_ids) + self.log.debug("__ repre_contexts: {}".format(pformat( + repre_contexts))) + # loop all returned repres from repre_context dict for repre_id, repre_context in repre_contexts.items(): + self.log.debug("__ repre_id: {}".format(repre_id)) # get loader name by representation id loader_name = next( ( repr["loader"] for repr in loadable_representations if repr["_id"] == repre_id - ), - self.default_loader - ) + )) or self.default_loader + # get loader plugin Loader = next( ( @@ -118,8 +124,6 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): pformat(loadable_representations)) ) - - def _get_batch_group(self, instance, task_data): frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] From ed4388184ad768dbf00ce050efff8eaf11d3cf7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 25 Mar 2022 15:56:12 +0100 Subject: [PATCH 066/337] flame: adding clip loader to current batch --- .../flame/plugins/load/load_clip_batch.py | 135 ++++++++++++++++++ .../defaults/project_settings/flame.json | 22 +++ .../projects_schema/schema_project_flame.json | 42 ++++++ 3 files changed, 199 insertions(+) create mode 100644 openpype/hosts/flame/plugins/load/load_clip_batch.py diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py new file mode 100644 index 0000000000..81af34744e --- /dev/null +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -0,0 +1,135 @@ +import os +import flame +from pprint import pformat +import openpype.hosts.flame.api as opfapi + + +class LoadClipBatch(opfapi.ClipLoader): + """Load a subset to timeline as clip + + Place clip to timeline on its asset origin timings collected + during conforming to project + """ + + families = ["render2d", "source", "plate", "render", "review"] + representations = ["exr", "dpx", "jpg", "jpeg", "png", "h264"] + + label = "Load as clip to current batch" + order = -10 + icon = "code-fork" + color = "orange" + + # settings + reel_name = "OP_LoadedReel" + clip_name_template = "{asset}_{subset}_{representation}" + + def load(self, context, name, namespace, options): + + # get flame objects + self.batch = flame.batch + + # load clip to timeline and get main variables + namespace = namespace + version = context['version'] + version_data = version.get("data", {}) + version_name = version.get("name", None) + colorspace = version_data.get("colorspace", None) + clip_name = self.clip_name_template.format( + **context["representation"]["context"]) + + # todo: settings in imageio + # convert colorspace with ocio to flame mapping + # in imageio flame section + colorspace = colorspace + + # create workfile path + workfile_dir = os.environ["AVALON_WORKDIR"] + openclip_dir = os.path.join( + workfile_dir, clip_name + ) + openclip_path = os.path.join( + openclip_dir, clip_name + ".clip" + ) + if not os.path.exists(openclip_dir): + os.makedirs(openclip_dir) + + # prepare clip data from context ad send it to openClipLoader + loading_context = { + "path": self.fname.replace("\\", "/"), + "colorspace": colorspace, + "version": "v{:0>3}".format(version_name), + "logger": self.log + + } + self.log.debug(pformat( + loading_context + )) + self.log.debug(openclip_path) + + # make openpype clip file + opfapi.OpenClipSolver(openclip_path, loading_context).make() + + # prepare Reel group in actual desktop + opc = self._get_clip( + clip_name, + openclip_path + ) + + # add additional metadata from the version to imprint Avalon knob + add_keys = [ + "frameStart", "frameEnd", "source", "author", + "fps", "handleStart", "handleEnd" + ] + + # move all version data keys to tag data + data_imprint = { + key: version_data.get(key, str(None)) + for key in add_keys + } + # add variables related to version context + data_imprint.update({ + "version": version_name, + "colorspace": colorspace, + "objectName": clip_name + }) + + # TODO: finish the containerisation + # opc_segment = opfapi.get_clip_segment(opc) + + # return opfapi.containerise( + # opc_segment, + # name, namespace, context, + # self.__class__.__name__, + # data_imprint) + + return opc + + def _get_clip(self, name, clip_path): + reel = self._get_reel() + + # with maintained openclip as opc + matching_clip = next( + ( + cl for cl in reel.clips + if cl.name.get_value() == name + ) + ) + + if not matching_clip: + created_clips = flame.import_clips(str(clip_path), reel) + return created_clips.pop() + + return matching_clip + + def _get_reel(self): + + matching_reel = [ + rg for rg in self.batch.reels + if rg.name.get_value() == self.reel_name + ] + + return ( + matching_reel.pop() + if matching_reel + else self.batch.create_reel(str(self.reel_name)) + ) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index a2b9bef103..afd0834c9d 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -92,6 +92,28 @@ "reel_group_name": "OpenPype_Reels", "reel_name": "Loaded", "clip_name_template": "{asset}_{subset}_{representation}" + }, + "LoadClipBatch": { + "enabled": true, + "families": [ + "render2d", + "source", + "plate", + "render", + "review" + ], + "representations": [ + "exr", + "dpx", + "jpg", + "jpeg", + "png", + "h264", + "mov", + "mp4" + ], + "reel_name": "OP_LoadedReel", + "clip_name_template": "{asset}_{subset}_{representation}" } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index c991577799..fe11d63ac2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -376,6 +376,48 @@ "label": "Clip name template" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "LoadClipBatch", + "label": "Load as clip to current batch", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + }, + { + "type": "list", + "key": "representations", + "label": "Representations", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "reel_name", + "label": "Reel name" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "clip_name_template", + "label": "Clip name template" + } + ] } ] } From 267a3e04ed4fda8c3837af28379e3a8812312fb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 25 Mar 2022 16:52:03 +0100 Subject: [PATCH 067/337] flame: improving batch attributes --- openpype/hosts/flame/api/batch_utils.py | 1 + openpype/hosts/flame/plugins/load/load_clip_batch.py | 11 +++++------ .../flame/plugins/publish/integrate_batch_group.py | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 99e053faf1..43742c6e4f 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -56,6 +56,7 @@ def create_batch(name, frame_start, frame_duration, **kwargs): write_node.frame_padding = write_pref["frame_padding"] write_node.version_mode = write_pref["version_mode"] write_node.version_name = write_pref["version_name"] + write_node.version_padding = write_pref["version_padding"] flame.batch.connect_nodes(comp_node, "Result", write_node, "Front") diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 81af34744e..bf0bbb5168 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -108,12 +108,11 @@ class LoadClipBatch(opfapi.ClipLoader): reel = self._get_reel() # with maintained openclip as opc - matching_clip = next( - ( - cl for cl in reel.clips - if cl.name.get_value() == name - ) - ) + matching_clip = None + for cl in reel.clips: + if cl.name.get_value() != name: + continue + matching_clip = cl if not matching_clip: created_clips = flame.import_clips(str(clip_path), reel) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 81b304ff0b..536bf0d807 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -249,6 +249,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # Only available if create_clip = True. version_mode = "Follow Iteration" version_name = "v" + version_padding = 3 return { "name": name, @@ -266,7 +267,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): "frame_index_mode": frame_index_mode, "frame_padding": frame_padding, "version_mode": version_mode, - "version_name": version_name + "version_name": version_name, + "version_padding": version_padding } def _get_shot_task_dir_path(self, instance, task_data): From f8e99f38c97cf37b8001e4c5848d93e00a7f9107 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 25 Mar 2022 16:53:47 +0100 Subject: [PATCH 068/337] flame: make dirs for batch renders add one more directory layer for renders --- .../hosts/flame/plugins/publish/integrate_batch_group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 536bf0d807..eaab429111 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -199,6 +199,9 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): render_dir_path = os.path.join( task_workfile_path, "render", "flame") + if not os.path.exists(render_dir_path): + os.makedirs(render_dir_path, mode=0o777) + # TODO: add most of these to `imageio/flame/batch/write_node` name = "{project[code]}_{asset}_{task[name]}".format( **anatomy_data @@ -208,7 +211,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # /path/to/file.[0001-0010].exr media_path = render_dir_path # name of file represented by tokens - media_path_pattern = "_v." + media_path_pattern = "_v/_v." # The Create Open Clip attribute of the Write File node. \ # Determines if an Open Clip is created by the Write File node. create_clip = True From e631218ee44c36f9f7fabdcf666d77daccd771be Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:29:26 +0300 Subject: [PATCH 069/337] refactor function, fix comments --- .../plugins/publish/submit_publish_job.py | 9 +++--- openpype/pipeline/farm/patterning.py | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7f65011864..0a374a75b6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -452,11 +452,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_file_name = os.path.basename(col[0]) else: render_file_name = os.path.basename(col) - - preview = match_aov_pattern(self, app, render_file_name) + aov_patterns = self.aov_filter.keys() + preview = match_aov_pattern(app, aov_patterns, render_file_name) - + # toggle preview on if multipart is on if instance_data.get("multipartExr"): preview = True @@ -530,9 +530,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = False render_file_name = list(collection[0]) app = os.environ.get("AVALON_APP", "") + aov_patterns = self.aov_filter.keys() # if filtered aov name is found in filename, toggle it for # preview video rendering - preview = match_aov_pattern(self, app, render_file_name) + preview = match_aov_pattern(app, aov_patterns, render_file_name) # toggle preview on if multipart is on if instance.get("multipartExr", False): preview = True diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 308546a1c9..0ad7e682fa 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -1,16 +1,22 @@ # -*- coding: utf-8 -*- import re - -def match_aov_pattern(self, app, render_file_name): - """Matching against a AOV pattern in the render files - In order to match the AOV name - we must compare against the render filename string - that we are grabbing the render filename string - from the collection that we have grabbed from exp_files. +def match_aov_pattern(app, aov_patterns, render_file_name): + """Matching against a `AOV` pattern in the render files. + + In order to match the AOV name we must compare + against the render filename string that we are + grabbing the render filename string from the collection + that we have grabbed from `exp_files`. + + Args: + app (str): Host name. + aov_patterns (list): List of AOV patterns from AOV filters. + render_file_name (str): Incoming file name to match against. + + Returns: + bool: Review state for rendered file (render_file_name). """ - if app in self.aov_filter.keys(): - for aov_pattern in self.aov_filter[app]: - if re.match(aov_pattern, render_file_name): - preview = True - return preview \ No newline at end of file + aov_pattern = aov_patterns.get(app, []) + if aov_pattern: + return any(re.match(aov_pattern, render_file_name) for aov_pattern in aov_patterns) From cc86482f028b8fa34ab248ead2075db9af9983a4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:30:50 +0300 Subject: [PATCH 070/337] style fixes --- openpype/pipeline/farm/patterning.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 0ad7e682fa..60467d47fa 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- import re + def match_aov_pattern(app, aov_patterns, render_file_name): """Matching against a `AOV` pattern in the render files. - In order to match the AOV name we must compare - against the render filename string that we are - grabbing the render filename string from the collection + In order to match the AOV name we must compare + against the render filename string that we are + grabbing the render filename string from the collection that we have grabbed from `exp_files`. Args: From 6cd0423e5f8f0d3cacd9f24d86508c2280d7e90e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:31:58 +0300 Subject: [PATCH 071/337] remove whitespace --- openpype/pipeline/farm/patterning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 60467d47fa..d0a25f8e77 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -4,17 +4,17 @@ import re def match_aov_pattern(app, aov_patterns, render_file_name): """Matching against a `AOV` pattern in the render files. - + In order to match the AOV name we must compare against the render filename string that we are grabbing the render filename string from the collection that we have grabbed from `exp_files`. - + Args: app (str): Host name. aov_patterns (list): List of AOV patterns from AOV filters. render_file_name (str): Incoming file name to match against. - + Returns: bool: Review state for rendered file (render_file_name). """ From 72b6ae620a5a69a312976f6821c3b443d9f27478 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:33:37 +0300 Subject: [PATCH 072/337] remove extra line and whitespace --- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 0a374a75b6..a4e07a0684 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -454,8 +454,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_file_name = os.path.basename(col) aov_patterns = self.aov_filter.keys() preview = match_aov_pattern(app, aov_patterns, render_file_name) - - + # toggle preview on if multipart is on if instance_data.get("multipartExr"): preview = True From d45a7fdb3d849b11ff526d94c00fc9d6a4c60f01 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:36:22 +0300 Subject: [PATCH 073/337] Fix line length --- openpype/pipeline/farm/patterning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index d0a25f8e77..ad59ecb509 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -20,4 +20,5 @@ def match_aov_pattern(app, aov_patterns, render_file_name): """ aov_pattern = aov_patterns.get(app, []) if aov_pattern: - return any(re.match(aov_pattern, render_file_name) for aov_pattern in aov_patterns) + return any(re.match(aov_pattern, render_file_name) + for aov_pattern in aov_patterns) From 071ae5876571252e1c025fdd9dcc01cf24fcbd00 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:37:49 +0300 Subject: [PATCH 074/337] fix over indentation --- openpype/pipeline/farm/patterning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index ad59ecb509..e92078b27c 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -20,5 +20,5 @@ def match_aov_pattern(app, aov_patterns, render_file_name): """ aov_pattern = aov_patterns.get(app, []) if aov_pattern: - return any(re.match(aov_pattern, render_file_name) - for aov_pattern in aov_patterns) + return any(re.match(aov_pattern, render_file_name) + for aov_pattern in aov_patterns) From b54bba9b0f492908d33f3f6e77f63adadf224663 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:39:15 +0300 Subject: [PATCH 075/337] fix under indentaiton --- openpype/pipeline/farm/patterning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index e92078b27c..f853b77601 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -21,4 +21,4 @@ def match_aov_pattern(app, aov_patterns, render_file_name): aov_pattern = aov_patterns.get(app, []) if aov_pattern: return any(re.match(aov_pattern, render_file_name) - for aov_pattern in aov_patterns) + for aov_pattern in aov_patterns) From b11d73671f7adc94d9a439f76b2f00d2172ed422 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:48:19 +0300 Subject: [PATCH 076/337] Fix function error, assuming one aov_pattern --- openpype/pipeline/farm/patterning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index f853b77601..e534ed7506 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -20,5 +20,4 @@ def match_aov_pattern(app, aov_patterns, render_file_name): """ aov_pattern = aov_patterns.get(app, []) if aov_pattern: - return any(re.match(aov_pattern, render_file_name) - for aov_pattern in aov_patterns) + return any(re.match(aov_pattern, render_file_name)) From 340afab7d468cae8d4b30d7b90315b8ef3a9883a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 21:55:13 +0300 Subject: [PATCH 077/337] remove unneeded any() --- openpype/pipeline/farm/patterning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index e534ed7506..4703f4999d 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -20,4 +20,6 @@ def match_aov_pattern(app, aov_patterns, render_file_name): """ aov_pattern = aov_patterns.get(app, []) if aov_pattern: - return any(re.match(aov_pattern, render_file_name)) + if re.match(aov_pattern, render_file_name): + preview = True + return preview From 087f939aa6e4ea19a3addce828cb86753fb27cdb Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 22:46:57 +0300 Subject: [PATCH 078/337] add missing else statement --- openpype/pipeline/farm/patterning.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 4703f4999d..e1c05df77f 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -23,3 +23,5 @@ def match_aov_pattern(app, aov_patterns, render_file_name): if re.match(aov_pattern, render_file_name): preview = True return preview + else: + return False From 67f5f69f00b7ad14e5d7d37c515eb80f43520bbb Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 26 Mar 2022 22:57:51 +0300 Subject: [PATCH 079/337] fix passing keys only to matching function --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index a4e07a0684..16078fc236 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -452,7 +452,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_file_name = os.path.basename(col[0]) else: render_file_name = os.path.basename(col) - aov_patterns = self.aov_filter.keys() + aov_patterns = self.aov_filter preview = match_aov_pattern(app, aov_patterns, render_file_name) # toggle preview on if multipart is on @@ -529,7 +529,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = False render_file_name = list(collection[0]) app = os.environ.get("AVALON_APP", "") - aov_patterns = self.aov_filter.keys() + aov_patterns = self.aov_filter # if filtered aov name is found in filename, toggle it for # preview video rendering preview = match_aov_pattern(app, aov_patterns, render_file_name) From b711ba51745768809cdbc0c13c8fad0d67da71b9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sun, 27 Mar 2022 23:48:24 +0200 Subject: [PATCH 080/337] fix validator --- .../publish/validate_skeletalmesh_hierarchy.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index dda7e063f6..cffbd13834 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -22,14 +22,17 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): def process(self, instance): geo = instance.data.get("geometry") joints = instance.data.get("joints") - joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] - geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] + # joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] + # geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] - self.log.info(joints_parents) - self.log.info(geo_parents) - self.log.info(set(joints_parents + geo_parents)) + joints_parents = cmds.ls(joints, long=True) + geo_parents = cmds.ls(geo, long=True) - if len(set(joints_parents + geo_parents)) != 1: + parents_set = { + parent.split("|")[1] for parent in (joints_parents + geo_parents) + } + + if len(set(parents_set)) != 1: raise PublishXmlValidationError( self, "Multiple roots on geometry or joints." From 3a55b806345ec823177f30796ba018d860aea33d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 28 Mar 2022 00:07:15 +0200 Subject: [PATCH 081/337] fix docstring and remove unused code --- .../plugins/publish/validate_skeletalmesh_hierarchy.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index cffbd13834..54a86d27cf 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -7,12 +7,7 @@ from maya import cmds class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): - """Adheres to the content of 'model' family - - - Must have one top group. (configurable) - - Must only contain: transforms, meshes and groups - - """ + """Validates that nodes has common root.""" order = openpype.api.ValidateContentsOrder hosts = ["maya"] @@ -22,8 +17,6 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): def process(self, instance): geo = instance.data.get("geometry") joints = instance.data.get("joints") - # joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] - # geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] joints_parents = cmds.ls(joints, long=True) geo_parents = cmds.ls(geo, long=True) From 228d3cfc004b21398ee0fb381ed8ea5fa28e6826 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 28 Mar 2022 01:54:40 +0200 Subject: [PATCH 082/337] fix hierarchy --- .../publish/extract_unreal_skeletalmesh.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 5b0eb5a3bc..98dbf117dc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -66,20 +66,22 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): instance.data.get("variant", "") ) + joints_parents = cmds.ls(joints, long=True) + geo_parents = cmds.ls(geo, long=True) + + parent_node = { + parent.split("|")[1] for parent in (joints_parents + geo_parents) + }.pop() + renamed_to_extract = [] for node in to_extract: node_path = node.split("|") node_path[1] = parent renamed_to_extract.append("|".join(node_path)) - with renamed(joints_parent, parent): - with parent_nodes(renamed_to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in renamed_to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + with renamed(parent_node, parent): + self.log.info("Extracting: {}".format(renamed_to_extract, path)) + fbx_exporter.export(renamed_to_extract, path) if "representations" not in instance.data: instance.data["representations"] = [] From 08f80ecf15911f1e96808fd8c6032b55d4f596e7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 13:29:16 +0200 Subject: [PATCH 083/337] flame: make sure only one clip in xml --- openpype/hosts/flame/api/plugin.py | 59 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 4c9d3c5383..3a322e5208 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,24 +1,22 @@ +import itertools import os import re import shutil import sys -from xml.etree import ElementTree as ET -import six -import qargparse -from Qt import QtWidgets, QtCore -import openpype.api as openpype -from openpype.pipeline import ( - LegacyCreator, - LoaderPlugin, -) -from openpype import style -from . import ( - lib as flib, - pipeline as fpipeline, - constants -) - +import xml.etree.cElementTree as cET from copy import deepcopy +from xml.etree import ElementTree as ET + +import openpype.api as openpype +import qargparse +import six +from openpype import style +from openpype.pipeline import LegacyCreator, LoaderPlugin +from Qt import QtCore, QtWidgets + +from . import constants +from . import lib as flib +from . import pipeline as fpipeline log = openpype.Logger.get_logger(__name__) @@ -749,10 +747,39 @@ class OpenClipSolver: # execute creation of clip xml template data try: openpype.run_subprocess(cmd_args) + self._make_single_clip_media_info() except TypeError: self.log.error("Error creating self.tmp_file") six.reraise(*sys.exc_info()) + def _make_single_clip_media_info(self): + with open(self.tmp_file) as f: + lines = f.readlines() + _added_root = itertools.chain( + "", deepcopy(lines)[1:], "") + new_root = ET.fromstringlist(_added_root) + + # find the clip which is matching to my input name + xml_clips = new_root.findall("clip") + matching_clip = None + for xml_clip in xml_clips: + if xml_clip.find("name").text == self.feed_basename: + matching_clip = xml_clip + + if not matching_clip: + # return warning there is missing clip + raise ET.ParseError( + "Missing clip in `{}`. Available clips {}".format( + self.feed_basename, [ + xml_clip.find("name").text + for xml_clip in xml_clips + ] + )) + # save it as new file + tree = cET.ElementTree(matching_clip) + tree.write(self.tmp_file, xml_declaration=True, + method='xml', encoding='UTF-8') + def _clear_tmp_file(self): if os.path.isfile(self.tmp_file): os.remove(self.tmp_file) From 34a65cb646e5d267899f3f3df5eed4de72ac2074 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 13:29:40 +0200 Subject: [PATCH 084/337] flame: ignore clip file with zero lines --- openpype/hosts/flame/api/plugin.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 3a322e5208..949e8ad406 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -708,19 +708,32 @@ class OpenClipSolver: self.feed_dir = os.path.dirname(feed_path) self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - if not os.path.isfile(openclip_file_path): + if not self._is_valid_tmp_file(openclip_file_path): # openclip does not exist yet and will be created self.tmp_file = self.out_file = openclip_file_path self.create_new_clip = True else: + # update already created clip # output a temp file self.out_file = openclip_file_path self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) + + # remove previously generated temp files + # it will be regenerated self._clear_tmp_file() self.log.info("Temp File: {}".format(self.tmp_file)) + def _is_valid_tmp_file(self, file): + # check if file exists + if os.path.isfile(file): + with open(self.tmp_file) as f: + lines = f.readlines() + if len(lines) < 1: + self._clear_tmp_file() + return False + def make(self): self._generate_media_info_file() From 1c6ab37f351e87a0a4a01a93eb14de47668333f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 20:02:58 +0200 Subject: [PATCH 085/337] flame: improving tmp file validation --- openpype/hosts/flame/api/plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 949e8ad406..ab60bbad11 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -730,9 +730,12 @@ class OpenClipSolver: if os.path.isfile(file): with open(self.tmp_file) as f: lines = f.readlines() - if len(lines) < 1: - self._clear_tmp_file() - return False + if len(lines) > 2: + return True + + # file is probably corrupted + self._clear_tmp_file() + return False def make(self): self._generate_media_info_file() From 0abc8ae61367a3ee03a896704557b324ffc1e1bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 20:18:59 +0200 Subject: [PATCH 086/337] flame: rework xml write file --- openpype/hosts/flame/api/plugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ab60bbad11..0eba06a86d 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -791,10 +791,8 @@ class OpenClipSolver: for xml_clip in xml_clips ] )) - # save it as new file - tree = cET.ElementTree(matching_clip) - tree.write(self.tmp_file, xml_declaration=True, - method='xml', encoding='UTF-8') + + self._write_result_xml_to_file(self.tmp_file, matching_clip) def _clear_tmp_file(self): if os.path.isfile(self.tmp_file): @@ -901,7 +899,7 @@ class OpenClipSolver: self.log.info("Adding feed version: {}".format( self.feed_version_name)) - self._write_result_xml_to_file(xml_data) + self._write_result_xml_to_file(self.out_file, xml_data) self.log.info("openClip Updated: {}".format(self.out_file)) @@ -940,9 +938,11 @@ class OpenClipSolver: self._clear_handler(xml_root) return ET.tostring(xml_root).decode('utf-8') - def _write_result_xml_to_file(self, xml_data): - with open(self.out_file, "w") as f: - f.write(xml_data) + def _write_result_xml_to_file(self, file, xml_data): + # save it as new file + tree = cET.ElementTree(xml_data) + tree.write(file, xml_declaration=True, + method='xml', encoding='UTF-8') def _create_openclip_backup_file(self, file): bck_file = "{}.bak".format(file) From ae36d089690f9acb078cc185e5315667523669dc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 20:54:12 +0200 Subject: [PATCH 087/337] flame: little fixes of loading --- openpype/hosts/flame/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 0eba06a86d..3673dc6671 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -728,13 +728,14 @@ class OpenClipSolver: def _is_valid_tmp_file(self, file): # check if file exists if os.path.isfile(file): - with open(self.tmp_file) as f: + # test also if file is not empty + with open(file) as f: lines = f.readlines() if len(lines) > 2: return True # file is probably corrupted - self._clear_tmp_file() + os.remove(file) return False def make(self): @@ -779,7 +780,7 @@ class OpenClipSolver: xml_clips = new_root.findall("clip") matching_clip = None for xml_clip in xml_clips: - if xml_clip.find("name").text == self.feed_basename: + if xml_clip.find("name").text in self.feed_basename: matching_clip = xml_clip if not matching_clip: From 2bf75d270a3fbfa0054d750159439a52e1f0369f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 21:05:35 +0200 Subject: [PATCH 088/337] flame: fix loaded name to `output` make condition for fix if output is not in context data --- openpype/hosts/flame/plugins/load/load_clip.py | 2 +- openpype/hosts/flame/plugins/load/load_clip_batch.py | 7 ++++++- openpype/settings/defaults/project_settings/flame.json | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 8980f72cb8..b27600db1f 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -22,7 +22,7 @@ class LoadClip(opfapi.ClipLoader): # settings reel_group_name = "OpenPype_Reels" reel_name = "Loaded" - clip_name_template = "{asset}_{subset}_{representation}" + clip_name_template = "{asset}_{subset}_{output}" def load(self, context, name, namespace, options): diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index bf0bbb5168..1f87f94cc6 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -21,7 +21,7 @@ class LoadClipBatch(opfapi.ClipLoader): # settings reel_name = "OP_LoadedReel" - clip_name_template = "{asset}_{subset}_{representation}" + clip_name_template = "{asset}_{subset}_{output}" def load(self, context, name, namespace, options): @@ -34,6 +34,11 @@ class LoadClipBatch(opfapi.ClipLoader): version_data = version.get("data", {}) version_name = version.get("name", None) colorspace = version_data.get("colorspace", None) + + # in case output is not in context replace key to representation + if not context["representation"]["context"].get("output"): + self.clip_name_template.replace("output", "representation") + clip_name = self.clip_name_template.format( **context["representation"]["context"]) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index afd0834c9d..ef7a2a4467 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -91,7 +91,7 @@ ], "reel_group_name": "OpenPype_Reels", "reel_name": "Loaded", - "clip_name_template": "{asset}_{subset}_{representation}" + "clip_name_template": "{asset}_{subset}_{output}" }, "LoadClipBatch": { "enabled": true, @@ -113,7 +113,7 @@ "mp4" ], "reel_name": "OP_LoadedReel", - "clip_name_template": "{asset}_{subset}_{representation}" + "clip_name_template": "{asset}_{subset}_{output}" } } } \ No newline at end of file From eda39b5de29e9bce283a3326427db8508d2cfb05 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Mar 2022 21:12:56 +0200 Subject: [PATCH 089/337] flame: fix write to xml file input args --- openpype/hosts/flame/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 3673dc6671..750609f7d6 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -829,7 +829,7 @@ class OpenClipSolver: xml_data = self._fix_xml_data(tmp_xml) self.log.info("Adding feed version: {}".format(self.feed_basename)) - self._write_result_xml_to_file(xml_data) + self._write_result_xml_to_file(self.out_file, xml_data) self.log.info("openClip Updated: {}".format(self.tmp_file)) From 44257be4863cc0eb0522ef14aa431bb10344c14c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Mar 2022 08:43:38 +0200 Subject: [PATCH 090/337] flame: fix utf8 error `'unicode' object has no attribute 'getiterator'` --- openpype/hosts/flame/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 750609f7d6..d5790d2f10 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -783,7 +783,7 @@ class OpenClipSolver: if xml_clip.find("name").text in self.feed_basename: matching_clip = xml_clip - if not matching_clip: + if matching_clip is not None: # return warning there is missing clip raise ET.ParseError( "Missing clip in `{}`. Available clips {}".format( @@ -937,7 +937,7 @@ class OpenClipSolver: def _fix_xml_data(self, xml_data): xml_root = xml_data.getroot() self._clear_handler(xml_root) - return ET.tostring(xml_root).decode('utf-8') + return xml_root def _write_result_xml_to_file(self, file, xml_data): # save it as new file From 69f5ace08485f0aea46a586602629a21416b779c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Mar 2022 08:46:50 +0200 Subject: [PATCH 091/337] flame: fix condition direction --- openpype/hosts/flame/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index d5790d2f10..464f5ce89b 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -783,7 +783,7 @@ class OpenClipSolver: if xml_clip.find("name").text in self.feed_basename: matching_clip = xml_clip - if matching_clip is not None: + if matching_clip is None: # return warning there is missing clip raise ET.ParseError( "Missing clip in `{}`. Available clips {}".format( From 3459cec3a9adf5537d11c5963e7b33ec9b5d5c2b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Mar 2022 10:29:17 +0200 Subject: [PATCH 092/337] flame: task workdir for .clip when integrating batch --- openpype/hosts/flame/plugins/load/load_clip_batch.py | 2 +- .../flame/plugins/publish/integrate_batch_group.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 1f87f94cc6..252c92516d 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -48,7 +48,7 @@ class LoadClipBatch(opfapi.ClipLoader): colorspace = colorspace # create workfile path - workfile_dir = os.environ["AVALON_WORKDIR"] + workfile_dir = options.get("workdir") or os.environ["AVALON_WORKDIR"] openclip_dir = os.path.join( workfile_dir, clip_name ) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index eaab429111..7c61ed62b5 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -105,7 +105,9 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # load to flame by representation context try: op_pipeline.load.load_with_repre_context( - Loader, repre_context) + Loader, repre_context, **{ + "data": {"workdir": self.task_workdir} + }) except op_pipeline.load.IncompatibleLoaderError as msg: self.log.error( "Check allowed representations for Loader `{}` " @@ -192,12 +194,14 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): anatomy_data = self._get_anamoty_data_with_current_task( instance, task_data) - task_workfile_path = self._get_shot_task_dir_path(instance, task_data) - self.log.debug("__ task_workfile_path: {}".format(task_workfile_path)) + self.task_workdir = self._get_shot_task_dir_path( + instance, task_data) + self.log.debug("__ task_workdir: {}".format( + self.task_workdir)) # TODO: this might be done with template in settings render_dir_path = os.path.join( - task_workfile_path, "render", "flame") + self.task_workdir, "render", "flame") if not os.path.exists(render_dir_path): os.makedirs(render_dir_path, mode=0o777) From 461bf75d660cc60aeae3401bf413496cd71b7e17 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Mar 2022 23:11:29 +0200 Subject: [PATCH 093/337] texture publishing initial commit --- .../validate_simple_unreal_texture_naming.py | 15 +++++++++++++++ openpype/plugins/publish/integrate_new.py | 3 ++- .../defaults/project_anatomy/templates.json | 11 ++++++++++- .../defaults/project_settings/global.json | 11 +++++++++++ .../project_settings/standalonepublisher.json | 11 ++++++++++- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py new file mode 100644 index 0000000000..158a749075 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Validator for correct file naming.""" +import pyblish.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError + + +class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): + label = "Validate Unreal Texture Names" + hosts = ["standalonepublisher"] + families = ["simpleUnrealTexture"] + order = openpype.api.ValidateContentsOrder + + def process(self, instance): + ... \ No newline at end of file diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2304f98713..4025f18cb2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -107,7 +107,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hda", "usd", "usdComposition", - "usdOverride" + "usdOverride", + "simpleUnrealTexture" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index d46d449c77..45611f55b1 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -32,5 +32,14 @@ "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", "path": "{@folder}/{@file}" }, - "others": {} + "others": { + "simpleUnrealTexture": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", + "file": "{original_file}", + "path": "{@folder}/{@file}" + }, + "__dynamic_keys_labels__": { + "simpleUnrealTexture": "Simple Unreal Texture" + } + } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 30a71b044a..f0fa09957e 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -192,6 +192,17 @@ "task_types": [], "tasks": [], "template_name": "render" + }, + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "simpleUnrealTexture" } ], "subset_grouping_profiles": [ diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 6858c4f34d..bc91a5ea8a 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -133,6 +133,14 @@ ], "help": "Texture files with UDIM together with worfile" }, + "create_simple_unreal_texture": { + "name": "simple_unreal_texture", + "label": "Simple Unreal Texture", + "family": "simpleUnrealTexture", + "icon": "Image", + "defaults": [], + "help": "Texture files with Unreal naming convention" + }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", "create_model": "Model", @@ -145,7 +153,8 @@ "create_matchmove": "Matchmove", "create_render": "Render", "create_mov_batch": "Batch Mov", - "create_texture_batch": "Batch Texture" + "create_texture_batch": "Batch Texture", + "create_simple_unreal_texture": "Simple Unreal Texture" } }, "publish": { From fe11ad9868cca57c6bc9ff34e8011beaba7989f4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 10:06:50 +0200 Subject: [PATCH 094/337] Remove unused website docs pages --- website/docs/api.md | 7 ------- website/docs/artist_hosts.md | 17 ----------------- website/docs/hosts-maya.md | 33 --------------------------------- 3 files changed, 57 deletions(-) delete mode 100644 website/docs/api.md delete mode 100644 website/docs/artist_hosts.md delete mode 100644 website/docs/hosts-maya.md diff --git a/website/docs/api.md b/website/docs/api.md deleted file mode 100644 index 7cad92d603..0000000000 --- a/website/docs/api.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -id: api -title: Pype API -sidebar_label: API ---- - -Work in progress diff --git a/website/docs/artist_hosts.md b/website/docs/artist_hosts.md deleted file mode 100644 index 609f6d97c8..0000000000 --- a/website/docs/artist_hosts.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -id: artist_hosts -title: Hosts -sidebar_label: Hosts ---- - -## Maya - -## Houdini - -## Nuke - -## Fusion - -## Unreal - -## System diff --git a/website/docs/hosts-maya.md b/website/docs/hosts-maya.md deleted file mode 100644 index 0ee0c2d86b..0000000000 --- a/website/docs/hosts-maya.md +++ /dev/null @@ -1,33 +0,0 @@ -### Tools -Creator -Publisher -Loader -Scene Inventory -Look assigner -Workfiles - -### Plugins -Deadline -Muster -Yeti -Arnold -Vray -Redshift - -### Families -Model -Look -Rig -Animation -Cache -Camera -Assembly -MayaAscii (generic scene) -Setdress -RenderSetup -Review -arnoldStandin -vrayProxy -vrayScene -yetiCache -yetiRig From ad7578fc7339ad2aaa885d616dc3e07a7a2df937 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 10:10:49 +0200 Subject: [PATCH 095/337] Remove unused `manager_naming.md` --- website/docs/manager_naming.md | 56 ---------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 website/docs/manager_naming.md diff --git a/website/docs/manager_naming.md b/website/docs/manager_naming.md deleted file mode 100644 index bf822fbeb4..0000000000 --- a/website/docs/manager_naming.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -id: manager_naming -title: Naming Conventions -sidebar_label: Naming Conventions ---- - -:::note -This naming convention holds true for most of our pipeline. Please match it as close as possible even for projects and files that might be outside of pipeline scope at this point. Small errors count! The reason for given formatting is to allow people to understand the file at glance and that a script or a program can easily get meaningful information from your files without errors. -::: - -## General rules - -For more detailed rules and different file types, have a look at naming conventions for scenes and assets - -- Every file starts with file code based on a project it belongs to e.g. ‘tst_’, ‘drm_’ -- Optional subversion and comment always comes after the major version. v##.subversion_comment. -- File names can only be composed of letters, numbers, underscores `_` and dots “.” -- You can use snakeCase or CamelCase if you need more words in a section.  thisIsLongerSentenceInComment -- No spaces in filenames. Ever! -- Frame numbers are always separated by a period ”.” -- If you're not sure use this template: - -## Work files - -**`{code}_{shot}_{task}_v001.ext`** - -**`{code}_{asset}_{task}_v001.ext`** - -**Examples:** - - prj_sh010_enviro_v001.ma - prj_sh010_animation_v001.ma - prj_sh010_comp_v001.nk - - prj_bob_modelling_v001.ma - prj_bob_rigging_v001.ma - prj_bob_lookdev_v001.ma - -:::info -In all of the examples anything enclosed in curly brackets  { } is compulsory in the name. -Anything in square brackets [ ] is optional. -::: - -## Published Assets - -**`{code}_{asset}_{family}_{subset}_{version}_[comment].ext`** - -**Examples:** - - prj_bob_model_main_v01.ma - prj_bob_model_hires_v01.ma - prj_bob_model_main_v01_clothes.ma - prj_bob_model_main_v01_body.ma - prj_bob_rig_main_v01.ma - Prj_bob_look_main_v01.ma - Prj_bob_look_wet_v01.ma From 10673544a481e34ebde96dccc3480fb03ff47452 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 11:51:32 +0200 Subject: [PATCH 096/337] Fix broken links --- website/docs/manager_ftrack.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index defbb4b48f..1b1c220c18 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://ftrack.rtd.ftrack.com/en/stable/). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://help.ftrack.com/en/articles/1040483-creating-a-new-project). ## Project management Setting project attributes is the key to properly working pipeline. @@ -31,7 +31,7 @@ This process describes how data from Ftrack will get into Avalon database. ### How to synchronize You can trigger synchronization manually using [Sync To Avalon](manager_ftrack_actions.md#sync-to-avalon) action. -Synchronization can also be automated with OpenPype's [event server](#event-server) and synchronization events. If your Ftrack is [prepared for OpenPype](#prepare-ftrack-for-openpype), the project should have custom attribute `Avalon auto-sync`. Check the custom attribute to allow auto-updates with event server. +Synchronization can also be automated with OpenPype's [event server](#event-server) and synchronization events. If your Ftrack is [prepared for OpenPype](module_ftrack.md#prepare-ftrack-for-openpype), the project should have custom attribute `Avalon auto-sync`. Check the custom attribute to allow auto-updates with event server. :::tip Always use `Sync To Avalon` action before you enable `Avalon auto-sync`! From df6499868bca5b0a3eea579591f309b3e78e1f59 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 12:22:43 +0200 Subject: [PATCH 097/337] flame: cleaning code --- openpype/hosts/flame/api/__init__.py | 4 ---- openpype/hosts/flame/api/lib.py | 17 +---------------- openpype/hosts/flame/api/scripts/wiretap_com.py | 2 +- openpype/hosts/flame/plugins/load/load_clip.py | 2 +- .../hosts/flame/plugins/load/load_clip_batch.py | 2 +- .../plugins/publish/integrate_batch_group.py | 3 +++ 6 files changed, 7 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 561aaab3de..28511458c2 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -11,10 +11,8 @@ from .constants import ( from .lib import ( CTX, FlameAppFramework, - get_project_manager, get_current_project, get_current_sequence, - create_bin, create_segment_data_marker, get_segment_data_marker, set_segment_data_marker, @@ -87,10 +85,8 @@ __all__ = [ # lib "CTX", "FlameAppFramework", - "get_project_manager", "get_current_project", "get_current_sequence", - "create_bin", "create_segment_data_marker", "get_segment_data_marker", "set_segment_data_marker", diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index dd91252a00..7316fa1c5b 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -227,16 +227,6 @@ class FlameAppFramework(object): return True -def get_project_manager(): - # TODO: get_project_manager - return - - -def get_media_storage(): - # TODO: get_media_storage - return - - def get_current_project(): import flame return flame.project.current_project @@ -266,11 +256,6 @@ def get_current_sequence(selection): return process_timeline -def create_bin(name, root=None): - # TODO: create_bin - return - - def rescan_hooks(): import flame try: @@ -724,5 +709,5 @@ def get_batch_group_from_desktop(name): project_desktop = project.current_workspace.desktop for bgroup in project_desktop.batch_groups: - if bgroup.name.get_value() == name: + if bgroup.name.get_value() in name: return bgroup diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index 54993d34eb..14fbcec954 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -254,7 +254,7 @@ class WireTapCom(object): filtered_users = [user for user in used_names if user_name in user] if filtered_users: - # todo: need to find lastly created following regex pattern for + # TODO: need to find lastly created following regex pattern for # date used in name return filtered_users.pop() diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index b27600db1f..e0a7297381 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -39,7 +39,7 @@ class LoadClip(opfapi.ClipLoader): clip_name = self.clip_name_template.format( **context["representation"]["context"]) - # todo: settings in imageio + # TODO: settings in imageio # convert colorspace with ocio to flame mapping # in imageio flame section colorspace = colorspace diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 252c92516d..3c13d88d3a 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -42,7 +42,7 @@ class LoadClipBatch(opfapi.ClipLoader): clip_name = self.clip_name_template.format( **context["representation"]["context"]) - # todo: settings in imageio + # TODO: settings in imageio # convert colorspace with ocio to flame mapping # in imageio flame section colorspace = colorspace diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 7c61ed62b5..253a1d6192 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -166,8 +166,11 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): self.log.info( "Updating batch group: {}".format(batchgroup_name)) # update already created batch group + bgroup.name = batchgroup_name bgroup.start_frame = frame_start bgroup.duration = frame_duration + # TODO: also update write node if there is any + # TODO: also update loaders to start from correct frameStart return bgroup From 5580ef083bd51bba96e11e1d68156d9dbedc4809 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 12:24:12 +0200 Subject: [PATCH 098/337] hound catch --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 253a1d6192..4dd6081170 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -218,7 +218,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # /path/to/file.[0001-0010].exr media_path = render_dir_path # name of file represented by tokens - media_path_pattern = "_v/_v." + media_path_pattern = ( + "_v/_v.") # The Create Open Clip attribute of the Write File node. \ # Determines if an Open Clip is created by the Write File node. create_clip = True From ee885051d915a20fe5e004a27c40a246f1e156de Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 30 Mar 2022 12:35:27 +0200 Subject: [PATCH 099/337] Added compute_resource_sync_sites to sync_server_module This method will be used in integrate_new to logically separate Site Sync parts. --- .../modules/sync_server/sync_server_module.py | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index caf58503f1..7126c17e17 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -157,7 +157,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id, site_name=site_name, force=force) - # public facing API def remove_site(self, collection, representation_id, site_name, remove_local_files=False): """ @@ -184,6 +183,112 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if remove_local_files: self._remove_local_file(collection, representation_id, site_name) + def compute_resource_sync_sites(self, project_name): + """Get available resource sync sites state for publish process. + + Returns dict with prepared state of sync sites for 'project_name'. + It checks if Site Sync is enabled, handles alternative sites. + Publish process stores this dictionary as a part of representation + document in DB. + + Example: + [ + { + 'name': '42abbc09-d62a-44a4-815c-a12cd679d2d7', + 'created_dt': datetime.datetime(2022, 3, 30, 12, 16, 9, 778637) + }, + {'name': 'studio'}, + {'name': 'SFTP'} + ] -- representation is published locally, artist or Settings have set + remote site as 'studio'. 'SFTP' is alternate site to 'studio'. Eg. + whenever file is on 'studio', it is also on 'SFTP'. + """ + + 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 + + if ( + not self.sync_system_settings["enabled"] or + not self.sync_project_settings[project_name]["enabled"]): + return [create_metadata(self.DEFAULT_SITE)] + + local_site = self.get_active_site(project_name) + remote_site = self.get_remote_site(project_name) + + # 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 not in attached_sites: + attached_sites[remote_site] = create_metadata(remote_site, + created=False) + + # add skeleton for sites where it should be always synced to + # usually it would be a backup site which is handled by separate + # background process + for site in self._get_always_accessible_sites(project_name): + if site not in attached_sites: + attached_sites[site] = create_metadata(site, created=False) + + attached_sites = self._add_alternative_sites(attached_sites) + + return list(attached_sites.values()) + + def _get_always_accessible_sites(self, project_name): + """Sites that synced to as a part of background process. + + Artist machine doesn't handle those, explicit Tray with that site name + as a local id must be running. + Example is dropbox site serving as a backup solution + """ + always_accessible_sites = ( + self.get_sync_project_setting(project_name)["config"]. + get("always_accessible_on", []) + ) + return [site.strip() for site in always_accessible_sites] + + def _add_alternative_sites(self, attached_sites): + """Add skeleton document for alternative sites + + Each new configured site in System Setting could serve as a alternative + site, it's a kind of alias. It means that files on 'a site' are + physically accessible also on 'a alternative' site. + Example is sftp site serving studio files via sftp protocol, physically + file is only in studio, sftp server has this location mounted. + """ + additional_sites = self.sync_system_settings.get("sites", {}) + + for site_name, site_info in additional_sites.items(): + # 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 + + return attached_sites + def clear_project(self, collection, site_name): """ Clear 'collection' of 'site_name' and its local files From 62e98546e1826bb96bac90c365bb92ffadfd1d43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 16:03:16 +0200 Subject: [PATCH 100/337] Update website/docs/manager_ftrack.md - add https to url Co-authored-by: Petr Kalis --- website/docs/manager_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index 1b1c220c18..730c57d1f9 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://help.ftrack.com/en/articles/1040483-creating-a-new-project). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/articles/1040483-creating-a-new-project). ## Project management Setting project attributes is the key to properly working pipeline. From 018a48b1fbc0f8f1a2600ea0976744d9c659046a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 30 Mar 2022 16:20:21 +0200 Subject: [PATCH 101/337] OP-2011 - cleanup of unnecessary long query from Setting --- .../plugins/publish/submit_maya_deadline.py | 35 ++++--------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 15a6f8d828..31a7c2f176 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -255,6 +255,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): tile_assembler_plugin = "OpenPypeTileAssembler" asset_dependencies = False limit_groups = [] + jobInfo = None + pluginInfo = None group = "none" def process(self, instance): @@ -272,37 +274,12 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.deadline_url = instance.data.get("deadlineUrl") assert self.deadline_url, "Requires Deadline Webservice URL" - self._job_info = ( - context.data["project_settings"].get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "jobInfo", {}) - ) + # just using existing names from Setting + self._job_info = self.jobInfo - self._plugin_info = ( - context.data["project_settings"].get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "pluginInfo", {}) - ) + self._plugin_info = self.pluginInfo - self.limit_groups = ( - context.data["project_settings"].get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "limit", []) - ) - - self.group = ( - context.data["project_settings"].get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "group", "none") - ) + self.limit_groups = self.limit context = instance.context workspace = context.data["workspaceDir"] From 053748ac2319468a4071db41e3818d4923e105c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 30 Mar 2022 18:54:05 +0200 Subject: [PATCH 102/337] handling validation and hero versions --- .../publish/collect_original_basename.py | 18 ++++++++++++++++++ .../help/validate_simple_texture_naming.xml | 17 +++++++++++++++++ .../validate_simple_unreal_texture_naming.py | 10 +++++++++- .../plugins/publish/integrate_hero_version.py | 5 +++++ openpype/plugins/publish/integrate_new.py | 13 +++++++++++++ .../defaults/project_anatomy/templates.json | 10 ++++++++-- .../defaults/project_settings/global.json | 17 +++++++++++++++-- 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py new file mode 100644 index 0000000000..b83a924d33 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Collect original base name for use in templates.""" +from pathlib import Path + +import pyblish.api + + +class CollectOriginalBasename(pyblish.api.InstancePlugin): + """Collect original file base name.""" + + order = pyblish.api.CollectorOrder + 0.498 + label = "Collect Base Name" + hosts = ["standalonepublisher"] + families = ["simpleUnrealTexture"] + + def process(self, instance): + file_name = Path(instance.data["representations"][0]["files"]) + instance.data["originalBasename"] = file_name.stem diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml new file mode 100644 index 0000000000..1818748407 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml @@ -0,0 +1,17 @@ + + + +Invalid texture name + +## Source files not found + +Submitted file has invalid name: +'{invalid_file}' + +### How to repair? + + Texture file must adhere to naming conventions for Unreal: + T_[ASSET_NAME}_*.ext + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py index 158a749075..05f38159c1 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -2,6 +2,7 @@ """Validator for correct file naming.""" import pyblish.api import openpype.api +import re from openpype.pipeline import PublishXmlValidationError @@ -10,6 +11,13 @@ class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] families = ["simpleUnrealTexture"] order = openpype.api.ValidateContentsOrder + regex = "^T_{asset}.*" def process(self, instance): - ... \ No newline at end of file + file_name = instance.data.get("originalBasename") + self.log.info(file_name) + pattern = self.regex.format(asset=instance.data.get("asset")) + if not re.match(pattern, file_name): + msg = f"Invalid file name {file_name}" + raise PublishXmlValidationError( + self, msg, formatting_data={"invalid_file": file_name}) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index d6df6535d8..ded149bdd0 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -485,6 +485,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] template_data = copy.deepcopy(instance.data["anatomyData"]) + if "originalBasename" in instance.data: + template_data.update({ + "originalBasename": instance.data.get("originalBasename") + }) + if "folder" in anatomy.templates[template_key]: anatomy_filled = anatomy.format(template_data) publish_folder = anatomy_filled[template_key]["folder"] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4025f18cb2..a3b9f0ef4a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -356,6 +356,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if profile: template_name = profile["template_name"] + + published_representations = {} for idx, repre in enumerate(instance.data["representations"]): # reset transfers for next representation @@ -384,6 +386,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 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'] @@ -555,6 +562,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 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 diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 45611f55b1..4be923da11 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -33,12 +33,18 @@ "path": "{@folder}/{@file}" }, "others": { + "simpleUnrealTextureHero": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/hero", + "file": "{originalBasename}.{ext}", + "path": "{@folder}/{@file}" + }, "simpleUnrealTexture": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", - "file": "{original_file}", + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{@version}", + "file": "{originalBasename}_{@version}.{ext}", "path": "{@folder}/{@file}" }, "__dynamic_keys_labels__": { + "simpleUnrealTextureHero": "Simple Unreal Texture - Hero", "simpleUnrealTexture": "Simple Unreal Texture" } } diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0bfd571a79..5443293c93 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -213,9 +213,22 @@ "animation", "setdress", "layout", - "mayaScene" + "mayaScene", + "simpleUnrealTexture" ], - "template_name_profiles": [] + "template_name_profiles": [ + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTextureHero" + } + ] }, "CleanUp": { "paterns": [], From 2520ceca630d5985a9646e0002aaea352445cfd0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 22:12:00 +0200 Subject: [PATCH 103/337] Fix #2946: Avoid ImportError on `hdefereval` when Houdini runs without UI --- openpype/hosts/houdini/api/pipeline.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index d079c9ea81..31c82b1cfd 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -4,7 +4,6 @@ import logging import contextlib import hou -import hdefereval import pyblish.api import avalon.api @@ -305,7 +304,13 @@ def on_new(): start = hou.playbar.playbackRange()[0] hou.setFrame(start) - hdefereval.executeDeferred(_enforce_start_frame) + if hou.isUIAvailable(): + import hdefereval + hdefereval.executeDeferred(_enforce_start_frame) + else: + # Run without execute deferred when no UI is available because + # without UI `hdefereval` is not available to import + _enforce_start_frame() def _set_context_settings(): From 34f5f7d60c2692d99448d535ba0c4a688b844b59 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 31 Mar 2022 10:13:13 +0200 Subject: [PATCH 104/337] fix validator message --- .../plugins/publish/help/validate_simple_texture_naming.xml | 4 ++-- .../plugins/publish/validate_simple_unreal_texture_naming.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml index 1818748407..b65d274fe5 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml @@ -3,7 +3,7 @@ Invalid texture name -## Source files not found +## Invalid file name Submitted file has invalid name: '{invalid_file}' @@ -11,7 +11,7 @@ Submitted file has invalid name: ### How to repair? Texture file must adhere to naming conventions for Unreal: - T_[ASSET_NAME}_*.ext + T_{asset}_*.ext \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py index 05f38159c1..ef8da9f280 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -20,4 +20,7 @@ class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): if not re.match(pattern, file_name): msg = f"Invalid file name {file_name}" raise PublishXmlValidationError( - self, msg, formatting_data={"invalid_file": file_name}) + self, msg, formatting_data={ + "invalid_file": file_name, + "asset": instance.data.get("asset") + }) From f486cef9251f18dcdfb390e8cfbaa840c6cda461 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Mar 2022 12:03:48 +0200 Subject: [PATCH 105/337] Update manager_ftrack.md Update Ftrack Documentation URL --- website/docs/manager_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index 730c57d1f9..b5ca167838 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/articles/1040483-creating-a-new-project). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/). ## Project management Setting project attributes is the key to properly working pipeline. From 2d038efde5122b9c709f1fedb9808eb75343f92e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 31 Mar 2022 13:34:38 +0300 Subject: [PATCH 106/337] fixes parameter name for readability --- openpype/pipeline/farm/patterning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index e1c05df77f..6d3eb3e5ab 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -2,7 +2,7 @@ import re -def match_aov_pattern(app, aov_patterns, render_file_name): +def match_aov_pattern(host_name, aov_patterns, render_file_name): """Matching against a `AOV` pattern in the render files. In order to match the AOV name we must compare @@ -18,7 +18,7 @@ def match_aov_pattern(app, aov_patterns, render_file_name): Returns: bool: Review state for rendered file (render_file_name). """ - aov_pattern = aov_patterns.get(app, []) + aov_pattern = aov_patterns.get(host_name, []) if aov_pattern: if re.match(aov_pattern, render_file_name): preview = True From 995ff7b94ac12ec7c25b81655ebab99095fac529 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Thu, 31 Mar 2022 18:59:44 +0300 Subject: [PATCH 107/337] Updates match_aov_pattern() logic to handle empty regex Using `is not None` to simplify code and handle empty regex cases. Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/farm/patterning.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 6d3eb3e5ab..5ba7a8df4b 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -19,9 +19,6 @@ def match_aov_pattern(host_name, aov_patterns, render_file_name): bool: Review state for rendered file (render_file_name). """ aov_pattern = aov_patterns.get(host_name, []) - if aov_pattern: - if re.match(aov_pattern, render_file_name): - preview = True - return preview - else: - return False + if not aov_pattern: + return False + return re.match(aov_pattern, render_file_name) is not None From eb95e11607483dd5f7b106c4701847f56e36a4f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 18:57:08 +0200 Subject: [PATCH 108/337] OP-2011 - added pulling priority for DL from Settings Default value is from Settings, artist can modify it. --- openpype/hosts/maya/plugins/create/create_render.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 9002ae3876..7ac739b227 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -294,6 +294,12 @@ class CreateRender(plugin.Creator): deadline_url = next(iter(self.deadline_servers.values())) pool_names = self._get_deadline_pools(deadline_url) + priority = self._project_settings.get( + "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( + "priority", 50) + self.data["priority"] = priority if muster_enabled: self.log.info(">>> Loading Muster credentials ...") From 79de61cda50d5e1f6c3897d1a31d780fecbea8b1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 19:01:59 +0200 Subject: [PATCH 109/337] OP-2011 - added separate tile_priority field --- .../hosts/maya/plugins/create/create_render.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 7ac739b227..4f0a394f85 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -252,6 +252,7 @@ class CreateRender(plugin.Creator): """Create instance settings.""" # get pools pool_names = [] + default_priority = 50 self.server_aliases = list(self.deadline_servers.keys()) self.data["deadlineServers"] = self.server_aliases @@ -260,7 +261,8 @@ class CreateRender(plugin.Creator): self.data["extendFrames"] = False self.data["overrideExistingFrame"] = True # self.data["useLegacyRenderLayers"] = True - self.data["priority"] = 50 + self.data["priority"] = default_priority + self.data["tile_priority"] = default_priority self.data["framesPerTask"] = 1 self.data["whitelist"] = False self.data["machineList"] = "" @@ -294,13 +296,17 @@ class CreateRender(plugin.Creator): deadline_url = next(iter(self.deadline_servers.values())) pool_names = self._get_deadline_pools(deadline_url) - priority = self._project_settings.get( + maya_submit_dl = self._project_settings.get( "deadline", {}).get( "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "priority", 50) + "MayaSubmitDeadline", {}) + priority = maya_submit_dl.get("priority", default_priority) self.data["priority"] = priority + tile_priority = maya_submit_dl.get("tile_priority", + default_priority) + self.data["tile_priority"] = tile_priority + if muster_enabled: self.log.info(">>> Loading Muster credentials ...") self._load_credentials() From c345e4aa27b615ad529d5b4a9e20d503a0b27cb8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 19:02:56 +0200 Subject: [PATCH 110/337] OP-2011 - added separate tile_priority to submit to DL --- .../deadline/plugins/publish/submit_maya_deadline.py | 6 ++++-- 1 file changed, 4 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 31a7c2f176..2d2d70758f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -254,6 +254,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): use_published = True tile_assembler_plugin = "OpenPypeTileAssembler" asset_dependencies = False + priority = 50 + tile_priority = 50 limit_groups = [] jobInfo = None pluginInfo = None @@ -442,7 +444,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.payload_skeleton["JobInfo"]["UserName"] = deadline_user # Set job priority self.payload_skeleton["JobInfo"]["Priority"] = \ - self._instance.data.get("priority", 50) + self._instance.data.get("priority", self.priority) if self.group != "none" and self.group: self.payload_skeleton["JobInfo"]["Group"] = self.group @@ -612,7 +614,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): } assembly_payload["JobInfo"].update(output_filenames) assembly_payload["JobInfo"]["Priority"] = self._instance.data.get( - "priority", 50) + "tile_priority", self.tile_priority) assembly_payload["JobInfo"]["UserName"] = deadline_user frame_payloads = [] From 6aa23b5c3946756921d54b7fd52554ad332902c1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 19:06:15 +0200 Subject: [PATCH 111/337] OP-2011 - added separate tile_priority to Settings --- .../defaults/project_settings/deadline.json | 56 ++++++++++--------- .../schema_project_deadline.json | 12 ++++ 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 5bb0a4022e..053c50ce8b 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -15,33 +15,6 @@ "deadline" ] }, - "ProcessSubmittedJobOnFarm": { - "enabled": true, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50, - "publishing_script": "", - "skip_integration_repre_list": [], - "aov_filter": { - "maya": [ - ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" - ], - "nuke": [ - ".*" - ], - "aftereffects": [ - ".*" - ], - "celaction": [ - ".*" - ], - "harmony": [ - ".*" - ] - } - }, "MayaSubmitDeadline": { "enabled": true, "optional": false, @@ -49,6 +22,8 @@ "tile_assembler_plugin": "OpenPypeTileAssembler", "use_published": true, "asset_dependencies": true, + "priority": 50, + "tile_priority": 50, "group": "none", "limit": [], "jobInfo": {}, @@ -95,6 +70,33 @@ "group": "", "department": "", "multiprocess": true + }, + "ProcessSubmittedJobOnFarm": { + "enabled": true, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 40, + "publishing_script": "", + "skip_integration_repre_list": [], + "aov_filter": { + "maya": [ + ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" + ], + "nuke": [ + ".*" + ], + "aftereffects": [ + ".*" + ], + "celaction": [ + ".*" + ], + "harmony": [ + ".*" + ] + } } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index e6097a2b14..0348543c81 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -117,6 +117,18 @@ "key": "asset_dependencies", "label": "Use Asset dependencies" }, + { + "type": "number", + "key": "priority", + "label": "Priority", + "default": 50 + }, + { + "type": "number", + "key": "tile_priority", + "label": "Tile Assembler Priority", + "default": 50 + }, { "type": "text", "key": "group", From 700184cfdc2f3dbabb3b49bbc1d9464fd3939a25 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 19:44:23 +0200 Subject: [PATCH 112/337] OP-2011 - added priority to publish job --- .../deadline/plugins/publish/submit_publish_job.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fad4d14ea0..29a276d3b3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -916,12 +916,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # User is deadline user render_job["Props"]["User"] = context.data.get( "deadlineUser", getpass.getuser()) - # Priority is now not handled at all - - if self.deadline_priority: - render_job["Props"]["Pri"] = self.deadline_priority - else: - render_job["Props"]["Pri"] = instance.data.get("priority") render_job["Props"]["Env"] = { "FTRACK_API_USER": os.environ.get("FTRACK_API_USER"), @@ -937,6 +931,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.deadline_url = instance.data.get("deadlineUrl") assert self.deadline_url, "Requires Deadline Webservice URL" + if self.deadline_priority: + render_job["Props"]["Pri"] = self.deadline_priority + else: + render_job["Props"]["Pri"] = instance.data.get("priority") + self._submit_deadline_post_job(instance, render_job, instances) # publish job file From 1947ccc0f2b8aace0b02e1f8c5581fd35913b673 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 16:47:40 +0200 Subject: [PATCH 113/337] OP-2011 - cleaned up settings --- openpype/settings/defaults/project_settings/deadline.json | 2 +- .../schemas/projects_schema/schema_project_deadline.json | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 053c50ce8b..7311b64046 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -77,7 +77,7 @@ "deadline_pool": "", "deadline_group": "", "deadline_chunk_size": 1, - "deadline_priority": 40, + "deadline_priority": 50, "publishing_script": "", "skip_integration_repre_list": [], "aov_filter": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 0348543c81..e730c42a8a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -120,14 +120,12 @@ { "type": "number", "key": "priority", - "label": "Priority", - "default": 50 + "label": "Priority" }, { "type": "number", "key": "tile_priority", "label": "Tile Assembler Priority", - "default": 50 }, { "type": "text", From ba622d570e5fef83ce0edb460558889fe307e058 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 16:53:24 +0200 Subject: [PATCH 114/337] OP-2011 - changed back default data type --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 ++-- 1 file changed, 2 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 2d2d70758f..34147712bc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -257,8 +257,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): priority = 50 tile_priority = 50 limit_groups = [] - jobInfo = None - pluginInfo = None + jobInfo = {} + pluginInfo = {} group = "none" def process(self, instance): From 087c3e5e5ceb4de4d871b6aa5ffd8d0214399cfa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 16:59:24 +0200 Subject: [PATCH 115/337] OP-2011 - fix typo --- .../schemas/projects_schema/schema_project_deadline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index e730c42a8a..b54d44d659 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -125,7 +125,7 @@ { "type": "number", "key": "tile_priority", - "label": "Tile Assembler Priority", + "label": "Tile Assembler Priority" }, { "type": "text", From 4783737bf9c64dcd6527ea24eb3efbc74ea38e84 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 17:13:58 +0200 Subject: [PATCH 116/337] OP-2011 - refactored priority --- .../deadline/plugins/publish/submit_publish_job.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 29a276d3b3..542c91b676 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -235,6 +235,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if mongo_url: environment["OPENPYPE_MONGO"] = mongo_url + priority = self.deadline_priority or instance.data.get("priority") + args = [ "--headless", 'publish', @@ -254,7 +256,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Department": self.deadline_department, "ChunkSize": self.deadline_chunk_size, - "Priority": job["Props"]["Pri"], + "Priority": priority, "Group": self.deadline_group, "Pool": self.deadline_pool, @@ -931,11 +933,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.deadline_url = instance.data.get("deadlineUrl") assert self.deadline_url, "Requires Deadline Webservice URL" - if self.deadline_priority: - render_job["Props"]["Pri"] = self.deadline_priority - else: - render_job["Props"]["Pri"] = instance.data.get("priority") - self._submit_deadline_post_job(instance, render_job, instances) # publish job file From 26de072f8760e76ee46e864f57578708be20fd43 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 17:42:08 +0200 Subject: [PATCH 117/337] OP-2011 - provide safer default --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 542c91b676..03adc7b168 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -235,7 +235,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if mongo_url: environment["OPENPYPE_MONGO"] = mongo_url - priority = self.deadline_priority or instance.data.get("priority") + priority = self.deadline_priority or instance.data.get("priority", 50) args = [ "--headless", From 4cdcb974b384ec628e83f480a24cdca4ddd1e605 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:07:51 +0200 Subject: [PATCH 118/337] removed usage of config callback in maya --- openpype/hosts/maya/api/pipeline.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index a8834d1ea3..f6f3472eef 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,8 +9,6 @@ import maya.api.OpenMaya as om import pyblish.api import avalon.api -from avalon.lib import find_submodule - import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -20,7 +18,6 @@ from openpype.lib import ( ) from openpype.lib.path_tools import HostDirmap from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, @@ -270,21 +267,8 @@ def ls(): """ container_names = _ls() - - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "maya") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(container_names): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def containerise(name, From b652170492496a7d64caa34b66db334011a1cdde Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:09:05 +0200 Subject: [PATCH 119/337] removed usage of avalon config from houdini --- openpype/hosts/houdini/api/pipeline.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 8e093a89bc..6a69814e2e 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -7,8 +7,6 @@ import hou import hdefereval import pyblish.api -import avalon.api -from avalon.lib import find_submodule from openpype.pipeline import ( register_creator_plugin_path, @@ -215,24 +213,12 @@ def ls(): "pyblish.mindbender.container"): containers += lib.lsattr("id", identifier) - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "houdini") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(containers, # Hou 19+ Python 3 hou.ObjNode are not # sortable due to not supporting greater # than comparisons key=lambda node: node.path()): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def before_save(): From f3f06444ac7ddd193932549d00dc97a2d827d306 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:31:48 +0200 Subject: [PATCH 120/337] created process_context with installation functions --- openpype/pipeline/process_context.py | 333 +++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 openpype/pipeline/process_context.py diff --git a/openpype/pipeline/process_context.py b/openpype/pipeline/process_context.py new file mode 100644 index 0000000000..65e891c100 --- /dev/null +++ b/openpype/pipeline/process_context.py @@ -0,0 +1,333 @@ +"""Core pipeline functionality""" + +import os +import sys +import json +import types +import logging +import inspect +import platform + +import pyblish.api +from pyblish.lib import MessageHandler + +from avalon import io, Session + +import openpype +from openpype.modules import load_modules +from openpype.settings import get_project_settings +from openpype.lib import ( + Anatomy, + register_event_callback, + filter_pyblish_plugins, + change_timer_to_current_context, +) + +from . import ( + register_loader_plugin_path, + register_inventory_action, + register_creator_plugin_path, + deregister_loader_plugin_path, +) + + +_is_installed = False +_registered_root = {"_": ""} +_registered_host = {"_": None} + +log = logging.getLogger(__name__) + +PACKAGE_DIR = os.path.dirname(os.path.abspath(openpype.__file__)) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +# Global plugin paths +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") + + +def register_root(path): + """Register currently active root""" + log.info("Registering root: %s" % path) + _registered_root["_"] = path + + +def registered_root(): + """Return currently registered root""" + root = _registered_root["_"] + if root: + return root + + root = Session.get("AVALON_PROJECTS") + if root: + return os.path.normpath(root) + return "" + + +def install(host): + """Install `host` into the running Python session. + + Args: + host (module): A Python module containing the Avalon + avalon host-interface. + """ + global _is_installed + + io.install() + + missing = list() + for key in ("AVALON_PROJECT", "AVALON_ASSET"): + if key not in Session: + missing.append(key) + + assert not missing, ( + "%s missing from environment, %s" % ( + ", ".join(missing), + json.dumps(Session, indent=4, sort_keys=True) + )) + + project_name = Session["AVALON_PROJECT"] + log.info("Activating %s.." % project_name) + + # Optional host install function + if hasattr(host, "install"): + host.install() + + register_host(host) + + _is_installed = True + + # Make sure modules are loaded + load_modules() + + def modified_emit(obj, record): + """Method replacing `emit` in Pyblish's MessageHandler.""" + record.msg = record.getMessage() + obj.records.append(record) + + MessageHandler.emit = modified_emit + + log.info("Registering global plug-ins..") + pyblish.api.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_discovery_filter(filter_pyblish_plugins) + register_loader_plugin_path(LOAD_PATH) + + project_name = os.environ.get("AVALON_PROJECT") + + # Register studio specific plugins + if project_name: + anatomy = Anatomy(project_name) + anatomy.set_root_environments() + register_root(anatomy.roots) + + project_settings = get_project_settings(project_name) + platform_name = platform.system().lower() + project_plugins = ( + project_settings + .get("global", {}) + .get("project_plugins", {}) + .get(platform_name) + ) or [] + for path in project_plugins: + try: + path = str(path.format(**os.environ)) + except KeyError: + pass + + if not path or not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + register_loader_plugin_path(path) + register_creator_plugin_path(path) + register_inventory_action(path) + + # apply monkey patched discover to original one + log.info("Patching discovery") + + register_event_callback("taskChanged", _on_task_change) + + +def _on_task_change(): + change_timer_to_current_context() + + +def uninstall(): + """Undo all of what `install()` did""" + host = registered_host() + + try: + host.uninstall() + except AttributeError: + pass + + log.info("Deregistering global plug-ins..") + pyblish.api.deregister_plugin_path(PUBLISH_PATH) + pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) + deregister_loader_plugin_path(LOAD_PATH) + log.info("Global plug-ins unregistred") + + deregister_host() + + io.uninstall() + + log.info("Successfully uninstalled Avalon!") + + +def is_installed(): + """Return state of installation + + Returns: + True if installed, False otherwise + + """ + + return _is_installed + + +def register_host(host): + """Register a new host for the current process + + Arguments: + host (ModuleType): A module implementing the + Host API interface. See the Host API + documentation for details on what is + required, or browse the source code. + + """ + signatures = { + "ls": [] + } + + _validate_signature(host, signatures) + _registered_host["_"] = host + + +def _validate_signature(module, signatures): + # Required signatures for each member + + missing = list() + invalid = list() + success = True + + for member in signatures: + if not hasattr(module, member): + missing.append(member) + success = False + + else: + attr = getattr(module, member) + if sys.version_info.major >= 3: + signature = inspect.getfullargspec(attr)[0] + else: + signature = inspect.getargspec(attr)[0] + required_signature = signatures[member] + + assert isinstance(signature, list) + assert isinstance(required_signature, list) + + if not all(member in signature + for member in required_signature): + invalid.append({ + "member": member, + "signature": ", ".join(signature), + "required": ", ".join(required_signature) + }) + success = False + + if not success: + report = list() + + if missing: + report.append( + "Incomplete interface for module: '%s'\n" + "Missing: %s" % (module, ", ".join( + "'%s'" % member for member in missing)) + ) + + if invalid: + report.append( + "'%s': One or more members were found, but didn't " + "have the right argument signature." % module.__name__ + ) + + for member in invalid: + report.append( + " Found: {member}({signature})".format(**member) + ) + report.append( + " Expected: {member}({required})".format(**member) + ) + + raise ValueError("\n".join(report)) + + +def registered_host(): + """Return currently registered host""" + return _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 + + +def debug_host(): + """A debug host, useful to debugging features that depend on a host""" + + host = types.ModuleType("debugHost") + + def ls(): + containers = [ + { + "representation": "ee-ft-a-uuid1", + "schema": "openpype:container-1.0", + "name": "Bruce01", + "objectName": "Bruce01_node", + "namespace": "_bruce01_", + "version": 3, + }, + { + "representation": "aa-bc-s-uuid2", + "schema": "openpype:container-1.0", + "name": "Bruce02", + "objectName": "Bruce01_node", + "namespace": "_bruce02_", + "version": 2, + } + ] + + for container in containers: + yield container + + host.__dict__.update({ + "ls": ls, + "open_file": lambda fname: None, + "save_file": lambda fname: None, + "current_file": lambda: os.path.expanduser("~/temp.txt"), + "has_unsaved_changes": lambda: False, + "work_root": lambda: os.path.expanduser("~/temp"), + "file_extensions": lambda: ["txt"], + }) + + return host From 85e2601022e0f5bf4596293c30f0bc653992013a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 1 Apr 2022 18:36:29 +0200 Subject: [PATCH 121/337] =?UTF-8?q?fix=20=F0=9F=90=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../maya/plugins/publish/collect_unreal_skeletalmesh.py | 2 -- .../maya/plugins/publish/extract_unreal_skeletalmesh.py | 6 ------ .../maya/plugins/publish/extract_unreal_staticmesh.py | 3 +-- .../plugins/publish/validate_unreal_staticmesh_naming.py | 9 +-------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 2b176e3a6d..79693bb35e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api -from avalon.api import Session -from openpype.api import get_project_settings class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 98dbf117dc..4dcad47e8c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -7,10 +7,6 @@ from maya import cmds # noqa import pyblish.api import openpype.api -from openpype.hosts.maya.api.lib import ( - parent_nodes, - maintained_selection -) from openpype.hosts.maya.api import fbx @@ -42,8 +38,6 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): geo = instance.data.get("geometry") joints = instance.data.get("joints") - joints_parent = cmds.listRelatives(joints, p=True) - to_extract = geo + joints # The export requires forward slashes because we need diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 92fa1b5933..69d51f9ff1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -8,8 +8,7 @@ import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( parent_nodes, - maintained_selection, - delete_after + maintained_selection ) from openpype.hosts.maya.api import fbx diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index c0eeb82688..43f6c85827 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +"""Validator for correct naming of Static Meshes.""" from maya import cmds # noqa import pyblish.api import openpype.api @@ -71,13 +71,6 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["collision_prefixes"] ) - static_mesh_prefix = ( - project_settings - ["maya"] - ["create"] - ["CreateUnrealStaticMesh"] - ["static_mesh_prefix"] - ) if cls.validate_mesh: # compile regex for testing names From c49791258a5b714c84085f465e05b43f72f08266 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:36:08 +0200 Subject: [PATCH 122/337] changed function names and separated install in 2 parts --- openpype/pipeline/__init__.py | 33 ++++++++++++++++++++++++++++ openpype/pipeline/process_context.py | 26 ++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 8460d20ef1..914606cc2f 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -68,6 +68,22 @@ from .actions import ( deregister_inventory_action_path, ) +from .process_context import ( + install_openpype_plugins, + install_host, + uninstall_host, + is_installed, + + register_root, + registered_root, + + register_host, + registered_host, + deregister_host, +) +install = install_host +uninstall = uninstall_host + __all__ = ( "AVALON_CONTAINER_ID", @@ -135,4 +151,21 @@ __all__ = ( "register_inventory_action_path", "deregister_inventory_action", "deregister_inventory_action_path", + + # --- Process context --- + "install_openpype_plugins", + "install_host", + "uninstall_host", + "is_installed", + + "register_root", + "registered_root", + + "register_host", + "registered_host", + "deregister_host", + + # Backwards compatible function names + "install", + "uninstall", ) diff --git a/openpype/pipeline/process_context.py b/openpype/pipeline/process_context.py index 65e891c100..1bef260ec9 100644 --- a/openpype/pipeline/process_context.py +++ b/openpype/pipeline/process_context.py @@ -63,7 +63,7 @@ def registered_root(): return "" -def install(host): +def install_host(host): """Install `host` into the running Python session. Args: @@ -72,6 +72,8 @@ def install(host): """ global _is_installed + _is_installed = True + io.install() missing = list() @@ -94,10 +96,7 @@ def install(host): register_host(host) - _is_installed = True - - # Make sure modules are loaded - load_modules() + register_event_callback("taskChanged", _on_task_change) def modified_emit(obj, record): """Method replacing `emit` in Pyblish's MessageHandler.""" @@ -106,12 +105,20 @@ def install(host): MessageHandler.emit = modified_emit + install_openpype_plugins() + + +def install_openpype_plugins(project_name=None): + # Make sure modules are loaded + load_modules() + log.info("Registering global plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) - project_name = os.environ.get("AVALON_PROJECT") + if project_name is None: + project_name = os.environ.get("AVALON_PROJECT") # Register studio specific plugins if project_name: @@ -141,17 +148,12 @@ def install(host): register_creator_plugin_path(path) register_inventory_action(path) - # apply monkey patched discover to original one - log.info("Patching discovery") - - register_event_callback("taskChanged", _on_task_change) - def _on_task_change(): change_timer_to_current_context() -def uninstall(): +def uninstall_host(): """Undo all of what `install()` did""" host = registered_host() From eabe2fe56960a098608b10599e5d0c1cee550f9a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:37:58 +0200 Subject: [PATCH 123/337] changed usage of registered root --- .../vendor/husdoutputprocessors/avalon_uri_processor.py | 3 ++- openpype/lib/usdlib.py | 3 ++- openpype/pipeline/load/utils.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index 499b733570..8cd51e6641 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -134,6 +134,7 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): """ from avalon import api, io + from openpype.pipeline import registered_root PROJECT = api.Session["AVALON_PROJECT"] asset_doc = io.find_one({"name": asset, @@ -141,7 +142,7 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): if not asset_doc: raise RuntimeError("Invalid asset name: '%s'" % asset) - root = api.registered_root() + root = registered_root() path = self._template.format(**{ "root": root, "project": PROJECT, diff --git a/openpype/lib/usdlib.py b/openpype/lib/usdlib.py index 89021156b4..7b3b7112de 100644 --- a/openpype/lib/usdlib.py +++ b/openpype/lib/usdlib.py @@ -9,6 +9,7 @@ except ImportError: from mvpxr import Usd, UsdGeom, Sdf, Kind from avalon import io, api +from openpype.pipeline import registered_root log = logging.getLogger(__name__) @@ -323,7 +324,7 @@ def get_usd_master_path(asset, subset, representation): path = template.format( **{ - "root": api.registered_root(), + "root": registered_root(), "project": api.Session["AVALON_PROJECT"], "asset": asset_doc["name"], "subset": subset, diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 53ac6b626d..cb7c76f133 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -10,7 +10,7 @@ import six from bson.objectid import ObjectId from avalon import io, schema -from avalon.api import Session, registered_root +from avalon.api import Session from openpype.lib import Anatomy @@ -532,6 +532,8 @@ def get_representation_path(representation, root=None, dbcon=None): dbcon = io if root is None: + from openpype.pipeline import registered_root + root = registered_root() def path_from_represenation(): From 729131738a5ef8d618f3877da2bb4635e0c2d8be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:40:23 +0200 Subject: [PATCH 124/337] changed installation of hosts --- openpype/hosts/aftereffects/api/lib.py | 4 ++-- openpype/hosts/blender/api/pipeline.py | 4 ++-- .../hosts/blender/blender_addon/startup/init.py | 4 ++-- openpype/hosts/celaction/api/cli.py | 10 ++++------ openpype/hosts/flame/startup/openpype_in_flame.py | 6 +++--- openpype/hosts/fusion/scripts/fusion_switch_shot.py | 8 ++++++-- .../fusion/utility_scripts/__OpenPype_Menu__.py | 13 ++++++------- openpype/hosts/fusion/utility_scripts/switch_ui.py | 6 +++--- openpype/hosts/harmony/api/lib.py | 4 ++-- openpype/hosts/hiero/api/pipeline.py | 9 +-------- .../hiero/api/startup/Python/Startup/Startup.py | 4 ++-- .../hosts/houdini/startup/python2.7libs/pythonrc.py | 4 ++-- .../hosts/houdini/startup/python3.7libs/pythonrc.py | 4 ++-- openpype/hosts/maya/startup/userSetup.py | 5 ++--- openpype/hosts/nuke/startup/menu.py | 4 ++-- openpype/hosts/photoshop/api/lib.py | 5 ++--- .../utility_scripts/OpenPype_sync_util_scripts.py | 5 +++-- .../resolve/utility_scripts/__OpenPype__Menu__.py | 9 ++------- .../utility_scripts/tests/test_otio_as_edl.py | 10 +++++----- .../tests/testing_create_timeline_item_from_path.py | 8 +++----- .../tests/testing_load_media_pool_item.py | 8 +++----- openpype/hosts/tvpaint/api/launch_script.py | 4 ++-- openpype/hosts/tvpaint/api/pipeline.py | 11 ++++------- .../integration/Content/Python/init_unreal.py | 10 ++-------- openpype/hosts/webpublisher/api/__init__.py | 1 - openpype/lib/remote_publish.py | 7 ++----- openpype/tests/test_avalon_plugin_presets.py | 12 +++++------- 27 files changed, 74 insertions(+), 105 deletions(-) diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index dac6b5d28f..ce4cbf09af 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -6,6 +6,7 @@ import logging from Qt import QtWidgets +from openpype.pipeline import install_host from openpype.lib.remote_publish import headless_publish from openpype.tools.utils import host_tools @@ -22,10 +23,9 @@ def safe_excepthook(*args): def main(*subprocess_args): sys.excepthook = safe_excepthook - import avalon.api from openpype.hosts.aftereffects import api - avalon.api.install(api) + install_host(api) os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" app = QtWidgets.QApplication([]) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index b9ec2cfea4..0ea579970e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -19,6 +19,7 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, + uninstall_host, ) from openpype.api import Logger from openpype.lib import ( @@ -209,11 +210,10 @@ def reload_pipeline(*args): """ - avalon.api.uninstall() + uninstall_host() for module in ( "avalon.io", - "avalon.lib", "avalon.pipeline", "avalon.api", ): diff --git a/openpype/hosts/blender/blender_addon/startup/init.py b/openpype/hosts/blender/blender_addon/startup/init.py index e43373bc6c..13a4b8a7a1 100644 --- a/openpype/hosts/blender/blender_addon/startup/init.py +++ b/openpype/hosts/blender/blender_addon/startup/init.py @@ -1,4 +1,4 @@ -from avalon import pipeline +from openpype.pipeline import install_host from openpype.hosts.blender import api -pipeline.install(api) +install_host(api) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index bc1e3eaf89..85e210f21a 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -3,8 +3,6 @@ import sys import copy import argparse -from avalon import io - import pyblish.api import pyblish.util @@ -13,6 +11,8 @@ import openpype import openpype.hosts.celaction from openpype.hosts.celaction import api as celaction from openpype.tools.utils import host_tools +from openpype.pipeline.process_context import install_openpype_plugins + log = Logger().get_logger("Celaction_cli_publisher") @@ -21,9 +21,6 @@ publish_host = "celaction" HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__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") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def cli(): @@ -74,7 +71,8 @@ def main(): _prepare_publish_environments() # Registers pype's Global pyblish plugins - openpype.install() + # - use fake host + install_openpype_plugins() if os.path.exists(PUBLISH_PATH): log.info(f"Registering path: {PUBLISH_PATH}") diff --git a/openpype/hosts/flame/startup/openpype_in_flame.py b/openpype/hosts/flame/startup/openpype_in_flame.py index 931c5a1b79..7015abc7f4 100644 --- a/openpype/hosts/flame/startup/openpype_in_flame.py +++ b/openpype/hosts/flame/startup/openpype_in_flame.py @@ -3,16 +3,16 @@ import sys from Qt import QtWidgets from pprint import pformat import atexit -import openpype + import avalon import openpype.hosts.flame.api as opfapi +from openpype.pipeline import install_host def openpype_install(): """Registering OpenPype in context """ - openpype.install() - avalon.api.install(opfapi) + install_host(opfapi) print("Avalon registered hosts: {}".format( avalon.api.registered_host())) diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index ca7efb9136..ca8e5c9e37 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -7,6 +7,10 @@ import logging import avalon.api from avalon import io +from openpype.pipeline import ( + install_host, + registered_host, +) from openpype.lib import version_up from openpype.hosts.fusion import api from openpype.hosts.fusion.api import lib @@ -218,7 +222,7 @@ def switch(asset_name, filepath=None, new=True): assert current_comp is not None, ( "Fusion could not load '{}'").format(filepath) - host = avalon.api.registered_host() + host = registered_host() containers = list(host.ls()) assert containers, "Nothing to update" @@ -279,7 +283,7 @@ if __name__ == '__main__': args, unknown = parser.parse_args() - avalon.api.install(api) + install_host(api) switch(args.asset_name, args.file_path) sys.exit(0) diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py index 4b5e8f91a0..aa98563785 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py @@ -1,24 +1,23 @@ import os import sys -import openpype from openpype.api import Logger +from openpype.pipeline import ( + install_host, + registered_host, +) log = Logger().get_logger(__name__) def main(env): - import avalon.api from openpype.hosts.fusion import api from openpype.hosts.fusion.api import menu - # Registers pype's Global pyblish plugins - openpype.install() - # activate resolve from pype - avalon.api.install(api) + install_host(api) - log.info(f"Avalon registered hosts: {avalon.api.registered_host()}") + log.info(f"Avalon registered hosts: {registered_host()}") menu.launch_openpype_menu() diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index d9eeae25ea..37306c7a2a 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -1,14 +1,15 @@ import os +import sys import glob import logging from Qt import QtWidgets, QtCore -import avalon.api from avalon import io import qtawesome as qta from openpype import style +from openpype.pipeline import install_host from openpype.hosts.fusion import api from openpype.lib.avalon_context import get_workdir_from_session @@ -181,8 +182,7 @@ class App(QtWidgets.QWidget): if __name__ == '__main__': - import sys - avalon.api.install(api) + install_host(api) app = QtWidgets.QApplication(sys.argv) window = App() diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index 66eeac1e3a..53fd0f07dd 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -183,10 +183,10 @@ def launch(application_path, *args): application_path (str): Path to Harmony. """ - from avalon import api + from openpype.pipeline import install_host from openpype.hosts.harmony import api as harmony - api.install(harmony) + install_host(harmony) ProcessContext.port = random.randrange(49152, 65535) os.environ["AVALON_HARMONY_PORT"] = str(ProcessContext.port) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index b334102129..616ff53fd8 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -34,14 +34,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" def install(): - """ - Installing Hiero integration for avalon - - Args: - config (obj): avalon config module `pype` in our case, it is not - used but required by avalon.api.install() - - """ + """Installing Hiero integration.""" # adding all events events.register_events() diff --git a/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py b/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py index 21c21cd7c3..2e638c2088 100644 --- a/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py @@ -1,9 +1,9 @@ import traceback # activate hiero from pype -import avalon.api +from openpype.pipeline import install_host import openpype.hosts.hiero.api as phiero -avalon.api.install(phiero) +install_host(phiero) try: __import__("openpype.hosts.hiero.api") diff --git a/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py index eb33b49759..afadbffd3e 100644 --- a/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py +++ b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py @@ -1,10 +1,10 @@ -import avalon.api +from openpype.pipeline import install_host from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - avalon.api.install(api) + install_host(api) main() diff --git a/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py index eb33b49759..afadbffd3e 100644 --- a/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py +++ b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py @@ -1,10 +1,10 @@ -import avalon.api +from openpype.pipeline import install_host from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - avalon.api.install(api) + install_host(api) main() diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b89244817a..a3ab483add 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,11 +1,10 @@ import os -import avalon.api from openpype.api import get_project_settings +from openpype.pipeline import install_host from openpype.hosts.maya import api -import openpype.hosts.maya.api.lib as mlib from maya import cmds -avalon.api.install(api) +install_host(api) print("starting OpenPype usersetup") diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 2cac6d09e7..9ed43b2110 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,7 +1,7 @@ import nuke -import avalon.api from openpype.api import Logger +from openpype.pipeline import install_host from openpype.hosts.nuke import api from openpype.hosts.nuke.api.lib import ( on_script_load, @@ -13,7 +13,7 @@ from openpype.hosts.nuke.api.lib import ( log = Logger.get_logger(__name__) -avalon.api.install(api) +install_host(api) # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 6d2a493a94..2f57d64464 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -5,9 +5,8 @@ import traceback from Qt import QtWidgets -import avalon.api - from openpype.api import Logger +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 @@ -24,7 +23,7 @@ def safe_excepthook(*args): def main(*subprocess_args): from openpype.hosts.photoshop import api - avalon.api.install(api) + install_host(api) sys.excepthook = safe_excepthook # coloring in StdOutBroker 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 ac66916b91..3a16b9c966 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py @@ -1,13 +1,14 @@ #!/usr/bin/env python import os import sys -import openpype + +from openpype.pipeline import install_host def main(env): import openpype.hosts.resolve as bmdvr # Registers openpype's Global pyblish plugins - openpype.install() + install_host(bmdvr) bmdvr.setup(env) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py index b0cef1838a..89ade9238b 100644 --- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py +++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py @@ -1,8 +1,7 @@ import os import sys -import avalon.api as avalon -import openpype +from openpype.pipeline import install_host from openpype.api import Logger log = Logger().get_logger(__name__) @@ -10,13 +9,9 @@ log = Logger().get_logger(__name__) def main(env): import openpype.hosts.resolve as bmdvr - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) - - log.info(f"Avalon registered hosts: {avalon.registered_host()}") + install_host(bmdvr) bmdvr.launch_pype_menu() 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 5430ad32df..8433bd9172 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 @@ -1,9 +1,11 @@ #! python3 import os import sys -import avalon.api as avalon -import openpype + import opentimelineio as otio + +from openpype.pipeline import install_host + from openpype.hosts.resolve import TestGUI import openpype.hosts.resolve as bmdvr from openpype.hosts.resolve.otio import davinci_export as otio_export @@ -14,10 +16,8 @@ class ThisTestGUI(TestGUI): def __init__(self): super(ThisTestGUI, self).__init__() - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) def _open_dir_button_pressed(self, event): # selected_path = self.fu.RequestFile(os.path.expanduser("~")) 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 afa311e0b8..477955d527 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 @@ -1,8 +1,8 @@ #! python3 import os import sys -import avalon.api as avalon -import openpype + +from openpype.pipeline import install_host from openpype.hosts.resolve import TestGUI import openpype.hosts.resolve as bmdvr import clique @@ -13,10 +13,8 @@ class ThisTestGUI(TestGUI): def __init__(self): super(ThisTestGUI, self).__init__() - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) def _open_dir_button_pressed(self, event): # selected_path = self.fu.RequestFile(os.path.expanduser("~")) 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 cfdbe890e5..872d620162 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,6 +1,5 @@ #! python3 -import avalon.api as avalon -import openpype +from openpype.pipeline import install_host import openpype.hosts.resolve as bmdvr @@ -15,8 +14,7 @@ def file_processing(fpath): if __name__ == "__main__": path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) - file_processing(path) \ No newline at end of file + file_processing(path) diff --git a/openpype/hosts/tvpaint/api/launch_script.py b/openpype/hosts/tvpaint/api/launch_script.py index e66bf61df6..0b25027fc6 100644 --- a/openpype/hosts/tvpaint/api/launch_script.py +++ b/openpype/hosts/tvpaint/api/launch_script.py @@ -8,8 +8,8 @@ import logging from Qt import QtWidgets, QtCore, QtGui -from avalon import api from openpype import style +from openpype.pipeline import install_host from openpype.hosts.tvpaint.api.communication_server import ( CommunicationWrapper ) @@ -31,7 +31,7 @@ def main(launch_args): qt_app = QtWidgets.QApplication([]) # Execute pipeline installation - api.install(tvpaint_host) + install_host(tvpaint_host) # Create Communicator object and trigger launch # - this must be done before anything is processed diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index cafdf0701d..78c10c3dae 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -67,11 +67,8 @@ instances=2 def install(): - """Install Maya-specific functionality of avalon-core. + """Install TVPaint-specific functionality.""" - This function is called automatically on calling `api.install(maya)`. - - """ log.info("OpenPype - Installing TVPaint integration") io.install() @@ -96,11 +93,11 @@ def install(): def uninstall(): - """Uninstall TVPaint-specific functionality of avalon-core. - - This function is called automatically on calling `api.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) diff --git a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py index 2ecd301c25..4bb03b07ed 100644 --- a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py @@ -2,13 +2,7 @@ import unreal openpype_detected = True try: - from avalon import api -except ImportError as exc: - api = None - openpype_detected = False - unreal.log_error("Avalon: cannot load Avalon [ {} ]".format(exc)) - -try: + from openpype.pipeline import install_host from openpype.hosts.unreal import api as openpype_host except ImportError as exc: openpype_host = None @@ -16,7 +10,7 @@ except ImportError as exc: unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) if openpype_detected: - api.install(openpype_host) + install_host(openpype_host) @unreal.uclass() diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index dbeb628073..72bbffd099 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -1,7 +1,6 @@ import os import logging -from avalon import api as avalon from avalon import io from pyblish import api as pyblish import openpype.hosts.webpublisher diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 9d97671a61..8a42daf4e9 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -1,13 +1,12 @@ import os from datetime import datetime -import sys -from bson.objectid import ObjectId import collections +from bson.objectid import ObjectId + import pyblish.util import pyblish.api -from openpype import uninstall from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json @@ -81,7 +80,6 @@ def publish(log, close_plugin_name=None): if result["error"]: log.error(error_format.format(**result)) - uninstall() if close_plugin: # close host app explicitly after error context = pyblish.api.Context() close_plugin().process(context) @@ -118,7 +116,6 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): if result["error"]: log.error(error_format.format(**result)) - uninstall() log_lines = [error_format.format(**result)] + log_lines dbcon.update_one( {"_id": _id}, diff --git a/openpype/tests/test_avalon_plugin_presets.py b/openpype/tests/test_avalon_plugin_presets.py index c491be1c05..464c216d6f 100644 --- a/openpype/tests/test_avalon_plugin_presets.py +++ b/openpype/tests/test_avalon_plugin_presets.py @@ -1,6 +1,5 @@ -import avalon.api as api -import openpype from openpype.pipeline import ( + install_host, LegacyCreator, register_creator_plugin, discover_creator_plugins, @@ -23,15 +22,14 @@ class Test: __name__ = "test" ls = len - def __call__(self): - pass + @staticmethod + def install(): + register_creator_plugin(MyTestCreator) def test_avalon_plugin_presets(monkeypatch, printer): + install_host(Test) - openpype.install() - api.register_host(Test()) - register_creator_plugin(MyTestCreator) plugins = discover_creator_plugins() printer("Test if we got our test plugin") assert MyTestCreator in plugins From 331f87bd15c15099eb30d678c871fe3809dba885 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:18 +0200 Subject: [PATCH 125/337] changed usages of registered host and config --- openpype/hosts/testhost/run_publish.py | 4 ++-- openpype/pype_commands.py | 10 ++++++---- openpype/tools/loader/app.py | 10 ---------- openpype/tools/utils/host_tools.py | 3 ++- openpype/tools/utils/lib.py | 15 ++++++++------- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/testhost/run_publish.py b/openpype/hosts/testhost/run_publish.py index 44860a30e4..cc80bdc604 100644 --- a/openpype/hosts/testhost/run_publish.py +++ b/openpype/hosts/testhost/run_publish.py @@ -48,8 +48,8 @@ from openpype.tools.publisher.window import PublisherWindow def main(): """Main function for testing purposes.""" - import avalon.api import pyblish.api + from openpype.pipeline import install_host from openpype.modules import ModulesManager from openpype.hosts.testhost import api as testhost @@ -57,7 +57,7 @@ def main(): for plugin_path in manager.collect_plugin_paths()["publish"]: pyblish.api.register_plugin_path(plugin_path) - avalon.api.install(testhost) + install_host(testhost) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) app = QtWidgets.QApplication([]) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c05eece2be..e0c8847040 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -101,7 +101,8 @@ class PypeCommands: RuntimeError: When there is no path to process. """ from openpype.modules import ModulesManager - from openpype import install, uninstall + 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 @@ -112,7 +113,7 @@ class PypeCommands: log = Logger.get_logger() - install() + install_openpype_plugins() manager = ModulesManager() @@ -294,7 +295,8 @@ class PypeCommands: # Register target and host import pyblish.api import pyblish.util - import avalon.api + + from openpype.pipeline import install_host from openpype.hosts.webpublisher import api as webpublisher log = PypeLogger.get_logger() @@ -315,7 +317,7 @@ class PypeCommands: for target in targets: pyblish.api.register_target(target) - avalon.api.install(webpublisher) + install_host(webpublisher) log.info("Running publish ...") diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 923a1fabdb..23c0909f2b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -608,14 +608,4 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project - from avalon import pipeline - - # Find the set config - _config = pipeline.find_config() - if hasattr(_config, "install"): - _config.install() - else: - print("Config `%s` has no function `install`" % - _config.__name__) - show() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 2d9733ec94..b0c30f6dfb 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -6,6 +6,7 @@ use singleton approach with global functions (using helper anyway). import os import avalon.api import pyblish.api +from openpype.pipeline import registered_host from .lib import qt_app_context @@ -47,7 +48,7 @@ class HostToolsHelper: Window, validate_host_requirements ) # Host validation - host = avalon.api.registered_host() + host = registered_host() validate_host_requirements(host) workfiles_window = Window(parent=parent) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 422d0f5389..12dd637e6a 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -6,16 +6,17 @@ import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome -import avalon.api - -from openpype.style import get_default_entity_icon_color +from openpype.style import ( + get_default_entity_icon_color, + get_objected_colors, +) +from openpype.resources import get_image_path +from openpype.lib import filter_profiles from openpype.api import ( get_project_settings, Logger ) -from openpype.lib import filter_profiles -from openpype.style import get_objected_colors -from openpype.resources import get_image_path +from openpype.pipeline import registered_host log = Logger.get_logger(__name__) @@ -402,7 +403,7 @@ class FamilyConfigCache: self.family_configs.clear() # Skip if we're not in host context - if not avalon.api.registered_host(): + if not registered_host(): return # Update the icons from the project configuration From f9043329b49573a3c3c89406a25bb266e2ca0106 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:29 +0200 Subject: [PATCH 126/337] removed unused imports --- .../ftrack/event_handlers_user/action_create_folders.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py index d15a865124..0ed12bd03e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py @@ -1,11 +1,6 @@ import os from openpype_modules.ftrack.lib import BaseAction, statics_icon -from avalon import lib as avalonlib -from openpype.api import ( - Anatomy, - get_project_settings -) -from openpype.lib import ApplicationManager +from openpype.api import Anatomy class CreateFolders(BaseAction): From adc27e5186d30ba87c41b1f973a69d225f9f1174 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:46 +0200 Subject: [PATCH 127/337] changed how library loader is shown in tray --- openpype/modules/avalon_apps/avalon_app.py | 47 ++++++++++++---------- openpype/tools/libraryloader/app.py | 10 ----- openpype/tools/libraryloader/lib.py | 21 ---------- 3 files changed, 26 insertions(+), 52 deletions(-) delete mode 100644 openpype/tools/libraryloader/lib.py diff --git a/openpype/modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py index 51a22323f1..1d21de129b 100644 --- a/openpype/modules/avalon_apps/avalon_app.py +++ b/openpype/modules/avalon_apps/avalon_app.py @@ -1,5 +1,5 @@ import os -import openpype + from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule @@ -26,7 +26,8 @@ class AvalonModule(OpenPypeModule, ITrayModule): self.avalon_mongo_timeout = avalon_mongo_timeout # Tray attributes - self.libraryloader = None + self._library_loader_imported = None + self._library_loader_window = None self.rest_api_obj = None def get_global_environments(self): @@ -41,21 +42,11 @@ class AvalonModule(OpenPypeModule, ITrayModule): def tray_init(self): # Add library tool + self._library_loader_imported = False try: - from Qt import QtCore from openpype.tools.libraryloader import LibraryLoaderWindow - libraryloader = LibraryLoaderWindow( - show_projects=True, - show_libraries=True - ) - # Remove always on top flag for tray - window_flags = libraryloader.windowFlags() - if window_flags | QtCore.Qt.WindowStaysOnTopHint: - window_flags ^= QtCore.Qt.WindowStaysOnTopHint - libraryloader.setWindowFlags(window_flags) - self.libraryloader = libraryloader - + self._library_loader_imported = True except Exception: self.log.warning( "Couldn't load Library loader tool for tray.", @@ -64,7 +55,7 @@ class AvalonModule(OpenPypeModule, ITrayModule): # Definition of Tray menu def tray_menu(self, tray_menu): - if self.libraryloader is None: + if not self._library_loader_imported: return from Qt import QtWidgets @@ -84,17 +75,31 @@ class AvalonModule(OpenPypeModule, ITrayModule): return def show_library_loader(self): - if self.libraryloader is None: - return + if self._library_loader_window is None: + from Qt import QtCore + from openpype.tools.libraryloader import LibraryLoaderWindow + from openpype.pipeline import install_openpype_plugins - self.libraryloader.show() + libraryloader = LibraryLoaderWindow( + show_projects=True, + show_libraries=True + ) + # Remove always on top flag for tray + window_flags = libraryloader.windowFlags() + if window_flags | QtCore.Qt.WindowStaysOnTopHint: + window_flags ^= QtCore.Qt.WindowStaysOnTopHint + libraryloader.setWindowFlags(window_flags) + self._library_loader_window = libraryloader + + install_openpype_plugins() + + self._library_loader_window.show() # Raise and activate the window # for MacOS - self.libraryloader.raise_() + self._library_loader_window.raise_() # for Windows - self.libraryloader.activateWindow() - self.libraryloader.refresh() + self._library_loader_window.activateWindow() # Webserver module implementation def webserver_initialization(self, server_manager): diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index b73b415128..328e16205c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -16,8 +16,6 @@ from openpype.tools.utils.assets_widget import MultiSelectAssetsWidget from openpype.modules import ModulesManager -from . import lib - module = sys.modules[__name__] module.window = None @@ -260,14 +258,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.dbcon.Session["AVALON_PROJECT"] = project_name - _config = lib.find_config() - if hasattr(_config, "install"): - _config.install() - else: - print( - "Config `%s` has no function `install`" % _config.__name__ - ) - self._subsets_widget.on_project_change(project_name) if self._repres_widget: self._repres_widget.on_project_change(project_name) diff --git a/openpype/tools/libraryloader/lib.py b/openpype/tools/libraryloader/lib.py deleted file mode 100644 index 182b48893a..0000000000 --- a/openpype/tools/libraryloader/lib.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import importlib -import logging - -log = logging.getLogger(__name__) - - -# `find_config` from `pipeline` -def find_config(): - log.info("Finding configuration for project..") - - config = os.environ["AVALON_CONFIG"] - - if not config: - raise EnvironmentError( - "No configuration found in " - "the project nor environment" - ) - - log.info("Found %s, loading.." % config) - return importlib.import_module(config) From 4364cd55c39e686348818ff0ea1b4de49631e396 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:42:07 +0200 Subject: [PATCH 128/337] cleaned up openpype init file --- openpype/__init__.py | 97 -------------------------------------------- 1 file changed, 97 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 7fc7e63e61..810664707a 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -1,102 +1,5 @@ -# -*- coding: utf-8 -*- -"""Pype module.""" import os -import platform -import logging - -from .settings import get_project_settings -from .lib import ( - Anatomy, - filter_pyblish_plugins, - change_timer_to_current_context, - register_event_callback, -) - -log = logging.getLogger(__name__) PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") - -# Global plugin paths -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") - - -def install(): - """Install OpenPype to Avalon.""" - import avalon.api - import pyblish.api - from pyblish.lib import MessageHandler - from openpype.modules import load_modules - from openpype.pipeline import ( - register_loader_plugin_path, - register_inventory_action, - register_creator_plugin_path, - ) - - # Make sure modules are loaded - load_modules() - - def modified_emit(obj, record): - """Method replacing `emit` in Pyblish's MessageHandler.""" - record.msg = record.getMessage() - obj.records.append(record) - - MessageHandler.emit = modified_emit - - log.info("Registering global plug-ins..") - pyblish.api.register_plugin_path(PUBLISH_PATH) - pyblish.api.register_discovery_filter(filter_pyblish_plugins) - register_loader_plugin_path(LOAD_PATH) - - project_name = os.environ.get("AVALON_PROJECT") - - # Register studio specific plugins - if project_name: - anatomy = Anatomy(project_name) - anatomy.set_root_environments() - avalon.api.register_root(anatomy.roots) - - project_settings = get_project_settings(project_name) - platform_name = platform.system().lower() - project_plugins = ( - project_settings - .get("global", {}) - .get("project_plugins", {}) - .get(platform_name) - ) or [] - for path in project_plugins: - try: - path = str(path.format(**os.environ)) - except KeyError: - pass - - if not path or not os.path.exists(path): - continue - - pyblish.api.register_plugin_path(path) - register_loader_plugin_path(path) - register_creator_plugin_path(path) - register_inventory_action(path) - - # apply monkey patched discover to original one - log.info("Patching discovery") - - register_event_callback("taskChanged", _on_task_change) - - -def _on_task_change(): - change_timer_to_current_context() - - -def uninstall(): - """Uninstall Pype from Avalon.""" - import pyblish.api - from openpype.pipeline import deregister_loader_plugin_path - - log.info("Deregistering global plug-ins..") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) - deregister_loader_plugin_path(LOAD_PATH) - log.info("Global plug-ins unregistred") From 986c3287494edb194724c402ae50615c38c07a42 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Sat, 2 Apr 2022 12:04:25 +0200 Subject: [PATCH 129/337] Resolve environment variable in credential path with accre --- openpype/modules/sync_server/providers/gdrive.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index 0b586613b5..6a8d2b3422 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -3,7 +3,7 @@ import os.path import time import sys import six -import platform +import acre from openpype.api import Logger from openpype.api import get_system_settings @@ -73,8 +73,13 @@ class GDriveHandler(AbstractProvider): format(site_name)) return - cred_path = self.presets.get("credentials_url", {}).\ - get(platform.system().lower()) or '' + cred_data = { + 'cred_path': self.presets.get("credentials_url", {}) + } + cred_data = acre.parse(cred_data) + cred_data = acre.merge(cred_data, current_env=os.environ) + cred_path = cred_data['cred_path'] + if not os.path.exists(cred_path): msg = "Sync Server: No credentials for gdrive provider " + \ "for '{}' on path '{}'!".format(site_name, cred_path) From 8ec4d9c8d4bc203c820910ac3b5bd879cfa4b210 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 10:59:43 +0200 Subject: [PATCH 130/337] remove irrelevant comment --- openpype/hosts/celaction/api/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index 85e210f21a..ef73c7457a 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -71,7 +71,6 @@ def main(): _prepare_publish_environments() # Registers pype's Global pyblish plugins - # - use fake host install_openpype_plugins() if os.path.exists(PUBLISH_PATH): From c8a886d6ce575f1cedccecd1429876746bbaf0a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 17:52:23 +0200 Subject: [PATCH 131/337] added install_openpype_plugins into load cli --- openpype/tools/loader/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 23c0909f2b..ab57f63c38 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -5,6 +5,7 @@ from avalon import api, io from openpype import style from openpype.lib import register_event_callback +from openpype.pipeline import install_openpype_plugins from openpype.tools.utils import ( lib, PlaceholderLineEdit @@ -608,4 +609,6 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project + install_openpype_plugins() + show() From 578a0469c9a487948494f30a036bc14b8882323d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 17:57:27 +0200 Subject: [PATCH 132/337] pass project name to plugin install --- openpype/tools/loader/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index ab57f63c38..fad284d82b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -609,6 +609,6 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project - install_openpype_plugins() + install_openpype_plugins(project) show() From 4235a7674f6b37ff3b13fa9083a0114f558acb06 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 4 Apr 2022 19:39:24 +0200 Subject: [PATCH 133/337] Use format to fill cred_path --- .../modules/sync_server/providers/gdrive.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index 6a8d2b3422..f7bb2d36df 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -3,7 +3,7 @@ import os.path import time import sys import six -import acre +import platform from openpype.api import Logger from openpype.api import get_system_settings @@ -73,12 +73,22 @@ class GDriveHandler(AbstractProvider): format(site_name)) return - cred_data = { - 'cred_path': self.presets.get("credentials_url", {}) - } - cred_data = acre.parse(cred_data) - cred_data = acre.merge(cred_data, current_env=os.environ) - cred_path = cred_data['cred_path'] + current_platform = platform.system().lower() + cred_path = self.presets.get("credentials_url", {}). \ + get(current_platform) or '' + + if not cred_path: + msg = "Sync Server: Please, fill the credentials for gdrive "\ + "provider for platform '{}' !".format(current_platform) + log.info(msg) + return + + try: + cred_path = cred_path.format(**os.environ) + except KeyError as e: + log.info("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 " + \ From 8896b36ef0c1a0295c09ef69097589aad765245b Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 4 Apr 2022 19:41:27 +0200 Subject: [PATCH 134/337] Replace t by T in log message --- openpype/modules/sync_server/providers/gdrive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index f7bb2d36df..d6369d39e6 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -86,7 +86,7 @@ class GDriveHandler(AbstractProvider): try: cred_path = cred_path.format(**os.environ) except KeyError as e: - log.info("the key(s) {} does not exist in the environment " + log.info("The key(s) {} does not exist in the environment " "variables".format(" ".join(e.args))) return From 57ecd9adfaadc7e81e686a0bb74d68efaaf85b61 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 4 Apr 2022 19:43:13 +0200 Subject: [PATCH 135/337] Better log message with Sync Server --- openpype/modules/sync_server/providers/gdrive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index d6369d39e6..b783f7958b 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -86,8 +86,8 @@ class GDriveHandler(AbstractProvider): try: cred_path = cred_path.format(**os.environ) except KeyError as e: - log.info("The key(s) {} does not exist in the environment " - "variables".format(" ".join(e.args))) + 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): From 1926e107659790d10a63770f70b72a6f7cf88ef1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 20:50:58 +0200 Subject: [PATCH 136/337] flame: redundant code --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 4dd6081170..fc5f4cfcd0 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -7,7 +7,7 @@ import openpype.hosts.flame.api as opfapi import openpype.pipeline as op_pipeline -@pyblish.api.log + class IntegrateBatchGroup(pyblish.api.InstancePlugin): """Integrate published shot to batch group""" From 54897163caee3dbb783bcafd68b7648c99434c9d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 20:52:09 +0200 Subject: [PATCH 137/337] haunch catch --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index fc5f4cfcd0..a9ccd6b4a1 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -7,7 +7,6 @@ import openpype.hosts.flame.api as opfapi import openpype.pipeline as op_pipeline - class IntegrateBatchGroup(pyblish.api.InstancePlugin): """Integrate published shot to batch group""" From ee7ae9edfb9657320e1b2e0610ae9df07f0182bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:27:47 +0200 Subject: [PATCH 138/337] fix registration of creator paths --- openpype/hosts/traypublisher/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/pipeline.py b/openpype/hosts/traypublisher/api/pipeline.py index a39e5641ae..24175883d9 100644 --- a/openpype/hosts/traypublisher/api/pipeline.py +++ b/openpype/hosts/traypublisher/api/pipeline.py @@ -7,7 +7,7 @@ from avalon import io import avalon.api import pyblish.api -from openpype.pipeline import BaseCreator +from openpype.pipeline import register_creator_plugin_path ROOT_DIR = os.path.dirname(os.path.dirname( os.path.abspath(__file__) @@ -169,7 +169,7 @@ def install(): pyblish.api.register_host("traypublisher") pyblish.api.register_plugin_path(PUBLISH_PATH) - avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) def set_project_name(project_name): From 38652d2c6a620634d5b5ec3ebc58169de94beed3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:28:03 +0200 Subject: [PATCH 139/337] added some titles to validation errors --- .../plugins/publish/validate_workfile.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py index e8eeb46065..7501051669 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py @@ -14,11 +14,22 @@ class ValidateWorkfilePath(pyblish.api.InstancePlugin): def process(self, instance): filepath = instance.data["sourceFilepath"] if not filepath: - raise PublishValidationError(( - "Filepath of 'workfile' instance \"{}\" is not set" - ).format(instance.data["name"])) + raise PublishValidationError( + ( + "Filepath of 'workfile' instance \"{}\" is not set" + ).format(instance.data["name"]), + "File not filled", + "## Missing file\nYou are supposed to fill the path." + ) if not os.path.exists(filepath): - raise PublishValidationError(( - "Filepath of 'workfile' instance \"{}\" does not exist: {}" - ).format(instance.data["name"], filepath)) + raise PublishValidationError( + ( + "Filepath of 'workfile' instance \"{}\" does not exist: {}" + ).format(instance.data["name"], filepath), + "File not found", + ( + "## File was not found\nFile \"{}\" was not found." + " Check if the path is still available." + ).format(filepath) + ) From edac05b8507ee53cd485750b8821bfd49d7e72a1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Apr 2022 11:04:32 +0200 Subject: [PATCH 140/337] Added default subset template {family}{Task} for workfile family --- openpype/settings/defaults/project_settings/global.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 24334b0045..ffa63a8d81 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -243,6 +243,15 @@ "tasks": [], "template": "{family}{variant}" }, + { + "families": [ + "workfile" + ], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "{family}{Task}" + }, { "families": [ "render" From 77d017bae2d97b1f22dda8e1bcf50ea9179adb9d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Apr 2022 11:06:26 +0200 Subject: [PATCH 141/337] Updated assert message for comparing results --- tests/lib/assert_classes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/lib/assert_classes.py b/tests/lib/assert_classes.py index 98f758767d..7f4d8efc10 100644 --- a/tests/lib/assert_classes.py +++ b/tests/lib/assert_classes.py @@ -24,16 +24,18 @@ class DBAssert: else: args[key] = val - msg = None - no_of_docs = dbcon.count_documents(args) - if expected != no_of_docs: - msg = "Not expected no of versions. "\ - "Expected {}, found {}".format(expected, no_of_docs) - args.pop("type") detail_str = " " if args: - detail_str = " with {}".format(args) + detail_str = " with '{}'".format(args) + + msg = None + no_of_docs = dbcon.count_documents(args) + if expected != no_of_docs: + msg = "Not expected no of '{}'{}."\ + "Expected {}, found {}".format(queried_type, + detail_str, + expected, no_of_docs) status = "successful" if msg: @@ -42,7 +44,5 @@ class DBAssert: print("Comparing count of {}{} {}".format(queried_type, detail_str, status)) - if msg: - print(msg) return msg From 0dfca2ff4589e996708c9d27108e282d9107d847 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 11:38:00 +0200 Subject: [PATCH 142/337] Flame: refining the code for better understanding of flow --- .../plugins/publish/integrate_batch_group.py | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index a9ccd6b4a1..979134bbfe 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -38,44 +38,40 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): def _load_clip_to_context(self, instance, bgroup): # get all loaders for host - loaders = op_pipeline.discover_loader_plugins() + loaders_by_name = { + loader.__name__: loader + for loader in op_pipeline.discover_loader_plugins() + } # get all published representations published_representations = instance.data["published_representations"] + repres_db_id_by_name = { + repre_info["representation"]["name"]: repre_id + for repre_id, repre_info in published_representations.items() + } # get all loadable representations - representations = instance.data["representations"] + repres_by_name = { + repre["name"]: repre for repre in instance.data["representations"] + } # get repre_id for the loadable representations - loadable_representations = [ - { - "name": _repr["name"], - "loader": _repr.get("batch_group_loader_name"), - # match loader to the loadable representation - "_id": next( - ( - id - for id, repr in published_representations.items() - if repr["representation"]["name"] == _repr["name"] - ), - None - ) + loader_name_by_repre_id = { + repres_db_id_by_name[repr_name]: { + "loader": repr_data["batch_group_loader_name"], + # add repre data for exception logging + "_repre_data": repr_data } - for _repr in representations - if _repr.get("load_to_batch_group") is not None - ] + for repr_name, repr_data in repres_by_name.items() + if repr_data.get("load_to_batch_group") + } - self.log.debug("__ loadable_representations: {}".format(pformat( - loadable_representations))) + self.log.debug("__ loader_name_by_repre_id: {}".format(pformat( + loader_name_by_repre_id))) # get representation context from the repre_id - representation_ids = [ - repre["_id"] - for repre in loadable_representations - if repre["_id"] is not None - ] repre_contexts = op_pipeline.load.get_repres_contexts( - representation_ids) + loader_name_by_repre_id.keys()) self.log.debug("__ repre_contexts: {}".format(pformat( repre_contexts))) @@ -84,45 +80,37 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): for repre_id, repre_context in repre_contexts.items(): self.log.debug("__ repre_id: {}".format(repre_id)) # get loader name by representation id - loader_name = next( - ( - repr["loader"] - for repr in loadable_representations - if repr["_id"] == repre_id - )) or self.default_loader + loader_name = ( + loader_name_by_repre_id[repre_id]["loader"] + # if nothing was added to settings fallback to default + or self.default_loader + ) # get loader plugin - Loader = next( - ( - loader_plugin - for loader_plugin in loaders - if loader_plugin.__name__ == loader_name - ), - None - ) - if Loader: + loader_plugin = loaders_by_name.get(loader_name) + if loader_plugin: # load to flame by representation context try: op_pipeline.load.load_with_repre_context( - Loader, repre_context, **{ + loader_plugin, repre_context, **{ "data": {"workdir": self.task_workdir} }) except op_pipeline.load.IncompatibleLoaderError as msg: self.log.error( "Check allowed representations for Loader `{}` " "in settings > error: {}".format( - Loader.__name__, msg)) + loader_plugin.__name__, msg)) self.log.error( "Representaton context >>{}<< is not compatible " "with loader `{}`".format( - pformat(repre_context), Loader.__name__ + pformat(repre_context), loader_plugin.__name__ ) ) else: self.log.warning( "Something got wrong and there is not Loader found for " "following data: {}".format( - pformat(loadable_representations)) + pformat(loader_name_by_repre_id)) ) def _get_batch_group(self, instance, task_data): From 0437d0a1304347d9bf803083900a78f8e8a8a1fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 13:54:26 +0200 Subject: [PATCH 143/337] general: adding `hasReviewableRepresentations` to skeleton data --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 5755619292..a8f4fec563 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -729,7 +729,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), "multipartExr": data.get("multipartExr", False), - "jobBatchName": data.get("jobBatchName", "") + "jobBatchName": data.get("jobBatchName", ""), + "hasReviewableRepresentations": data.get( + "hasReviewableRepresentations") } if "prerender" in instance.data["families"]: From 6893fa22e90e5d36d6a0bcd5726636534a903bec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 14:01:35 +0200 Subject: [PATCH 144/337] ftrack: improving code logic it was not correctly distributing components --- .../publish/integrate_ftrack_instances.py | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index c11d5b9c68..b54db918a6 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -175,33 +175,40 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Create copy of base comp item and append it review_item = copy.deepcopy(base_component_item) - # condition for multiple reviewable representations - # expand name to better label componenst + # get asset name and define extended name variant + asset_name = review_item["asset_data"]["name"] + extended_asset_name = "_".join( + (asset_name, repre["name"]) + ) + + # reset extended if no need for extended asset name if ( - not self.keep_first_subset_name_for_review - and multiple_reviewable + self.keep_first_subset_name_for_review + and is_first_review_repre ): - asset_name = review_item["asset_data"]["name"] - # define new extended name - extended_asset_name = "_".join( - (asset_name, repre["name"]) - ) - review_item["asset_data"]["name"] = extended_asset_name + extended_asset_name = "" + else: + # only rename if multiple reviewable + if multiple_reviewable: + review_item["asset_data"]["name"] = extended_asset_name + else: + extended_asset_name = "" - # rename asset name only if multiple reviewable repre - if is_first_review_repre: - # and rename all already created components - for _ci in component_list: - _ci["asset_data"]["name"] = extended_asset_name + # rename all already created components + # only if first repre and extended name available + if is_first_review_repre and extended_asset_name: + # and rename all already created components + for _ci in component_list: + _ci["asset_data"]["name"] = extended_asset_name - # and rename all already created src components - for _sci in src_components_to_add: - _sci["asset_data"]["name"] = extended_asset_name + # and rename all already created src components + for _sci in src_components_to_add: + _sci["asset_data"]["name"] = extended_asset_name - # rename also first thumbnail component if any - if first_thumbnail_component is not None: - first_thumbnail_component[ - "asset_data"]["name"] = extended_asset_name + # rename also first thumbnail component if any + if first_thumbnail_component is not None: + first_thumbnail_component[ + "asset_data"]["name"] = extended_asset_name frame_start = repre.get("frameStartFtrack") frame_end = repre.get("frameEndFtrack") @@ -236,6 +243,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if is_first_review_repre: is_first_review_repre = False else: + # later detection for thumbnail duplication not_first_components.append(review_item) # Create copy of item before setting location @@ -280,7 +288,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_item = copy.deepcopy(base_component_item) # add extended name if any - if extended_asset_name: + if ( + not self.keep_first_subset_name_for_review + and extended_asset_name + ): other_item["asset_data"]["name"] = extended_asset_name other_item["component_data"] = { From 557aafdae326e5799f9258f02e5f27ef5ecb5f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 5 Apr 2022 14:36:11 +0200 Subject: [PATCH 145/337] fixed skeletal root --- .../publish/extract_unreal_skeletalmesh.py | 16 +++++----------- openpype/plugins/publish/integrate_new.py | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 4dcad47e8c..7ef7f2f181 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -55,25 +55,19 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): # variant we extract we need to rename top node of the rig correctly. # It is finally done in context manager so it won't affect current # scene. - parent = "{}{}".format( - instance.data["asset"], - instance.data.get("variant", "") - ) - joints_parents = cmds.ls(joints, long=True) - geo_parents = cmds.ls(geo, long=True) + # we rely on hierarchy under one root. + original_parent = to_extract[0].split("|")[1] - parent_node = { - parent.split("|")[1] for parent in (joints_parents + geo_parents) - }.pop() + parent_node = instance.data.get("asset") renamed_to_extract = [] for node in to_extract: node_path = node.split("|") - node_path[1] = parent + node_path[1] = parent_node renamed_to_extract.append("|".join(node_path)) - with renamed(parent_node, parent): + with renamed(original_parent, parent_node): self.log.info("Extracting: {}".format(renamed_to_extract, path)) fbx_exporter.export(renamed_to_extract, path) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index afa4e0a9cf..acdb05dd93 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -107,7 +107,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hda", "usd", "staticMesh", - "skeletalMesh" + "skeletalMesh", "usdComposition", "usdOverride" ] From 5b260afc6af67bbcda403e7ce35922e7a8350ff6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 15:47:57 +0200 Subject: [PATCH 146/337] flame: avoid hidden segment processing --- openpype/hosts/flame/otio/flame_export.py | 43 +++++++++++------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 8c240fc9d5..f9dbe68421 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -401,8 +401,10 @@ def get_clips_in_reels(project): version = clip.versions[-1] track = version.tracks[-1] + # each reel clip is also having one segment for segment in track.segments: - segment_data = _get_segment_attributes(segment) + segment_data = _get_segment_attributes( + segment, from_clip=True) clip_data.update(segment_data) output_clips.append(clip_data) @@ -489,12 +491,14 @@ def add_otio_metadata(otio_item, item, **kwargs): otio_item.metadata.update({key: value}) -def _get_shot_tokens_values(clip, tokens): +def _get_shot_tokens_values(clip, tokens, from_clip=False): old_value = None output = {} - if not clip.shot_name: - return output + # in case it is segment from reel clip + # avoiding duplicity of segement data + if from_clip: + return {} old_value = clip.shot_name.get_value() @@ -512,16 +516,19 @@ def _get_shot_tokens_values(clip, tokens): return output -def _get_segment_attributes(segment): +def _get_segment_attributes(segment, from_clip=False): # log.debug(dir(segment)) - - if str(segment.name)[1:-1] == "": + if ( + segment.name.get_value() == "" + or segment.hidden + ): return None # Add timeline segment to tree clip_data = { "segment_name": segment.name.get_value(), "segment_comment": segment.comment.get_value(), + "shot_name": segment.shot_name.get_value(), "tape_name": segment.tape_name, "source_name": segment.source_name, "fpath": segment.file_path, @@ -531,7 +538,7 @@ def _get_segment_attributes(segment): # add all available shot tokens shot_tokens = _get_shot_tokens_values(segment, [ "", "", "", "", - ]) + ], from_clip) clip_data.update(shot_tokens) # populate shot source metadata @@ -597,11 +604,7 @@ def create_otio_timeline(sequence): continue all_segments.append(clip_data) - segments_ordered = { - itemindex: clip_data - for itemindex, clip_data in enumerate( - all_segments) - } + segments_ordered = dict(enumerate(all_segments)) log.debug("_ segments_ordered: {}".format( pformat(segments_ordered) )) @@ -612,15 +615,11 @@ def create_otio_timeline(sequence): log.debug("_ itemindex: {}".format(itemindex)) # Add Gap if needed - if itemindex == 0: - # if it is first track item at track then add - # it to previous item - prev_item = segment_data - - else: - # get previous item - prev_item = segments_ordered[itemindex - 1] - + prev_item = ( + segment_data + if itemindex == 0 + else segments_ordered[itemindex - 1] + ) log.debug("_ segment_data: {}".format(segment_data)) # calculate clip frame range difference from each other From 575898490f4aad68256207da0dea4f9960e2948f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 15:57:16 +0200 Subject: [PATCH 147/337] flame: fixing broken get_clips_in_reels --- openpype/hosts/flame/otio/flame_export.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index f9dbe68421..78e5ceecb6 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -405,7 +405,8 @@ def get_clips_in_reels(project): for segment in track.segments: segment_data = _get_segment_attributes( segment, from_clip=True) - clip_data.update(segment_data) + if segment_data: + clip_data.update(segment_data) output_clips.append(clip_data) From 8d4541d68da458f5121029353119ab8ae7ff4791 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 16:29:04 +0200 Subject: [PATCH 148/337] flame: hidden attribute is PyAttribute so need to get value --- openpype/hosts/flame/otio/flame_export.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 78e5ceecb6..1b5980b40a 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -518,10 +518,13 @@ def _get_shot_tokens_values(clip, tokens, from_clip=False): def _get_segment_attributes(segment, from_clip=False): - # log.debug(dir(segment)) + + log.debug("Segment name|hidden: {}|{}".format( + segment.name.get_value(), segment.hidden + )) if ( segment.name.get_value() == "" - or segment.hidden + or segment.hidden.get_value() ): return None @@ -591,7 +594,12 @@ def create_otio_timeline(sequence): # create otio tracks and clips for ver in sequence.versions: for track in ver.tracks: - if len(track.segments) == 0 and track.hidden: + # avoid all empty tracks + # or hidden tracks + if ( + len(track.segments) == 0 + or track.hidden.get_value() + ): return None # convert track to otio From 60118298b6c3dcf2f41488624a3b6d3bf9166990 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 16:29:31 +0200 Subject: [PATCH 149/337] flame: make reel clip validation optional --- openpype/hosts/flame/plugins/publish/validate_source_clip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/validate_source_clip.py b/openpype/hosts/flame/plugins/publish/validate_source_clip.py index 9ff015f628..345c00e05a 100644 --- a/openpype/hosts/flame/plugins/publish/validate_source_clip.py +++ b/openpype/hosts/flame/plugins/publish/validate_source_clip.py @@ -9,6 +9,8 @@ class ValidateSourceClip(pyblish.api.InstancePlugin): label = "Validate Source Clip" hosts = ["flame"] families = ["clip"] + optional = True + active = False def process(self, instance): flame_source_clip = instance.data["flameSourceClip"] From 818c3fe91f420de9b08ff4ebfc1bdae301a42927 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 16:58:25 +0200 Subject: [PATCH 150/337] flame: fallback if reel clip is not available --- .../publish/extract_subset_resources.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 31f7b6d574..341f12be16 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -63,7 +63,10 @@ class ExtractSubsetResources(openpype.api.Extractor): segment = instance.data["item"] sequence_clip = instance.context.data["flameSequence"] clip_data = instance.data["flameSourceClip"] - clip = clip_data["PyClip"] + + reel_clip = None + if clip_data: + reel_clip = clip_data["PyClip"] # segment's parent track name s_track_name = segment.parent.name.get_value() @@ -127,8 +130,20 @@ class ExtractSubsetResources(openpype.api.Extractor): in_mark = (source_start_handles - source_first_frame) + 1 out_mark = in_mark + source_duration_handles + # make test for type of preset and available reel_clip + if ( + not reel_clip + and export_type != "Sequence Publish" + ): + self.log.warning(( + "Skipping preset {}. Not available " + "reel clip for {}").format( + preset_file, segment.name.get_value() + )) + continue + # by default export source clips - exporting_clip = clip + exporting_clip = reel_clip if export_type == "Sequence Publish": # change export clip to sequence @@ -344,7 +359,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # create otio tracks and clips for ver in sequence_clip.versions: for track in ver.tracks: - if len(track.segments) == 0 and track.hidden: + if len(track.segments) == 0 and track.hidden.get_value(): continue if track.name.get_value() != track_name: From 246127c73b7d06bf5e7a2b68a7fb0bd31e949b22 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 17:20:41 +0200 Subject: [PATCH 151/337] flame: hidding all unrelated segments and tracks --- .../plugins/publish/extract_subset_resources.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 341f12be16..a780f8c9e5 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -61,6 +61,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # flame objects segment = instance.data["item"] + segment_name = segment.name.get_value() sequence_clip = instance.context.data["flameSequence"] clip_data = instance.data["flameSourceClip"] @@ -138,7 +139,7 @@ class ExtractSubsetResources(openpype.api.Extractor): self.log.warning(( "Skipping preset {}. Not available " "reel clip for {}").format( - preset_file, segment.name.get_value() + preset_file, segment_name )) continue @@ -175,7 +176,7 @@ class ExtractSubsetResources(openpype.api.Extractor): if export_type == "Sequence Publish": # only keep visible layer where instance segment is child - self.hide_other_tracks(duplclip, s_track_name) + self.hide_others(duplclip, segment_name, s_track_name) # validate xml preset file is filled if preset_file == "": @@ -349,11 +350,12 @@ class ExtractSubsetResources(openpype.api.Extractor): return new_stage_dir, new_files_list - def hide_other_tracks(self, sequence_clip, track_name): + def hide_others(self, sequence_clip, segment_name, track_name): """Helper method used only if sequence clip is used Args: sequence_clip (flame.Clip): sequence clip + segment_name (str): segment name track_name (str): track name """ # create otio tracks and clips @@ -362,5 +364,12 @@ class ExtractSubsetResources(openpype.api.Extractor): if len(track.segments) == 0 and track.hidden.get_value(): continue + # hide tracks which are not parent track if track.name.get_value() != track_name: track.hidden = True + continue + + # hidde all other segments + for segment in track.segments: + if segment.name.get_value() != segment_name: + segment.hidden = True From b8d7dc0d6245256234ff4136181287b66adbb084 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 18:36:17 +0200 Subject: [PATCH 152/337] changed hasReviewableRepresentations to useSequenceForReview --- .../publish/extract_review_data_mov.py | 2 +- .../plugins/publish/submit_publish_job.py | 46 ++++++++++--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 22b371d8e9..2e8843d2e0 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -123,7 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): if generated_repres: # assign to representations instance.data["representations"] += generated_repres - instance.data["hasReviewableRepresentations"] = True + instance.data["useSequenceForReview"] = False else: instance.data["families"].remove("review") self.log.info(( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index a8f4fec563..586d0f975a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -524,26 +524,31 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): for collection in collections: ext = collection.tail.lstrip(".") preview = False - # if filtered aov name is found in filename, toggle it for - # preview video rendering - for app in self.aov_filter.keys(): - if os.environ.get("AVALON_APP", "") == app: - # no need to add review if `hasReviewableRepresentations` - if instance.get("hasReviewableRepresentations"): - break + # TODO 'useSequenceForReview' is temporary solution which does + # not work for 100% of cases. We must be able to tell what + # expected files contains more explicitly and from what + # should be review made. + # - "review" tag is never added when is set to 'False' + use_sequence_for_review = instance.get( + "useSequenceForReview", True + ) + if use_sequence_for_review: + # if filtered aov name is found in filename, toggle it for + # preview video rendering + for app in self.aov_filter.keys(): + if os.environ.get("AVALON_APP", "") == app: + # iteratre all aov filters + for aov in self.aov_filter[app]: + if re.match( + aov, + list(collection)[0] + ): + preview = True + break - # iteratre all aov filters - for aov in self.aov_filter[app]: - if re.match( - aov, - list(collection)[0] - ): - preview = True - break - - # toggle preview on if multipart is on - if instance.get("multipartExr", False): - preview = True + # toggle preview on if multipart is on + if instance.get("multipartExr", False): + preview = True staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( @@ -730,8 +735,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionHeight": data.get("resolutionHeight", 1080), "multipartExr": data.get("multipartExr", False), "jobBatchName": data.get("jobBatchName", ""), - "hasReviewableRepresentations": data.get( - "hasReviewableRepresentations") + "useSequenceForReview": data.get("useSequenceForReview") } if "prerender" in instance.data["families"]: From 7eb37b9e302c8894ed16140a1e44fab3893b7562 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 18:46:38 +0200 Subject: [PATCH 153/337] changed how console splitter sizez are reused on show --- .../window/widgets.py | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/openpype/modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py index ecf41eaf3e..9c6717ed17 100644 --- a/openpype/modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/python_console_interpreter/window/widgets.py @@ -389,7 +389,8 @@ class PythonInterpreterWidget(QtWidgets.QWidget): self._append_lines([openpype_art]) - self.setStyleSheet(load_stylesheet()) + self._first_show = True + self._splitter_size_ratio = None self._init_from_registry() @@ -416,9 +417,9 @@ class PythonInterpreterWidget(QtWidgets.QWidget): self.resize(width, height) try: - sizes = setting_registry.get_item("splitter_sizes") - if len(sizes) == len(self._widgets_splitter.sizes()): - self._widgets_splitter.setSizes(sizes) + self._splitter_size_ratio = ( + setting_registry.get_item("splitter_sizes") + ) except ValueError: pass @@ -627,8 +628,45 @@ class PythonInterpreterWidget(QtWidgets.QWidget): def showEvent(self, event): self._line_check_timer.start() super(PythonInterpreterWidget, self).showEvent(event) + # First show setup + if self._first_show: + self._first_show = False + self._on_first_show() + self._output_widget.scroll_to_bottom() + def _on_first_show(self): + # Change stylesheet + self.setStyleSheet(load_stylesheet()) + # Check if splitter size raio is set + # - first store value to local variable and then unset it + splitter_size_ratio = self._splitter_size_ratio + self._splitter_size_ratio = None + # Skip if is not set + if not splitter_size_ratio: + return + + # Skip if number of size items does not match to splitter + splitters_count = len(self._widgets_splitter.sizes()) + if len(splitter_size_ratio) != splitters_count: + return + + # Don't use absolute sizes but ratio of last stored sizes + ratio_sum = sum(splitter_size_ratio) + sizes = [] + max_size = self._widgets_splitter.height() + cur_size = 0 + ratio = max_size / ratio_sum + for size in splitter_size_ratio: + item_size = int(ratio * size) + cur_size += item_size + if cur_size > max_size: + item_size -= cur_size - max_size + if not item_size: + item_size = 1 + sizes.append(item_size) + self._widgets_splitter.setSizes(sizes) + def closeEvent(self, event): self.save_registry() super(PythonInterpreterWidget, self).closeEvent(event) From d740845a99e06073a905fee870bb923cfc862b95 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 6 Apr 2022 00:13:56 +0200 Subject: [PATCH 154/337] simplified set sizes --- .../window/widgets.py | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py index 9c6717ed17..6fdbc3ba2a 100644 --- a/openpype/modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/python_console_interpreter/window/widgets.py @@ -648,24 +648,8 @@ class PythonInterpreterWidget(QtWidgets.QWidget): # Skip if number of size items does not match to splitter splitters_count = len(self._widgets_splitter.sizes()) - if len(splitter_size_ratio) != splitters_count: - return - - # Don't use absolute sizes but ratio of last stored sizes - ratio_sum = sum(splitter_size_ratio) - sizes = [] - max_size = self._widgets_splitter.height() - cur_size = 0 - ratio = max_size / ratio_sum - for size in splitter_size_ratio: - item_size = int(ratio * size) - cur_size += item_size - if cur_size > max_size: - item_size -= cur_size - max_size - if not item_size: - item_size = 1 - sizes.append(item_size) - self._widgets_splitter.setSizes(sizes) + if len(splitter_size_ratio) == splitters_count: + self._widgets_splitter.setSizes(splitter_size_ratio) def closeEvent(self, event): self.save_registry() From e8a59ac056cbc91bdf57305d50e10d6f276ed773 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Apr 2022 00:16:12 +0200 Subject: [PATCH 155/337] Fix typo in comment Co-authored-by: Roy Nieterau --- openpype/modules/python_console_interpreter/window/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py index 6fdbc3ba2a..36ce1b61a2 100644 --- a/openpype/modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/python_console_interpreter/window/widgets.py @@ -638,7 +638,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget): def _on_first_show(self): # Change stylesheet self.setStyleSheet(load_stylesheet()) - # Check if splitter size raio is set + # Check if splitter size ratio is set # - first store value to local variable and then unset it splitter_size_ratio = self._splitter_size_ratio self._splitter_size_ratio = None From 64036c77d47fe84bd7b16a9c467dec73161d0cda Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 6 Apr 2022 03:40:50 +0000 Subject: [PATCH 156/337] [Automated] Bump version --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++----------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88623cee3d..9a53311d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,37 @@ # Changelog +## [3.9.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...HEAD) + +### 📖 Documentation + +- Website Docs: Manager Ftrack fix broken links [\#2979](https://github.com/pypeclub/OpenPype/pull/2979) + +**🆕 New features** + +- Publishing textures for Unreal [\#2988](https://github.com/pypeclub/OpenPype/pull/2988) +- Maya to Unreal \> Static and Skeletal Meshes [\#2978](https://github.com/pypeclub/OpenPype/pull/2978) + +**🚀 Enhancements** + +- Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) +- Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) + +**🐛 Bug fixes** + +- Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) +- Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) +- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) +- Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) + +**Merged pull requests:** + +- General: adding limitations for pyright [\#2994](https://github.com/pypeclub/OpenPype/pull/2994) + ## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.2-nightly.4...3.9.2) ### 📖 Documentation @@ -29,7 +58,7 @@ - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) +- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) **🐛 Bug fixes** @@ -63,6 +92,7 @@ - General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) - General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) - General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918) +- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914) - General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912) **Merged pull requests:** @@ -79,8 +109,8 @@ **🚀 Enhancements** - General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) +- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) - nuke: imageio adding ocio config version 1.2 [\#2897](https://github.com/pypeclub/OpenPype/pull/2897) -- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) - Nuke: ExtractReviewSlate can handle more codes and profiles [\#2879](https://github.com/pypeclub/OpenPype/pull/2879) - Flame: sequence used for reference video [\#2869](https://github.com/pypeclub/OpenPype/pull/2869) @@ -112,8 +142,6 @@ - General: Subset name filtering in ExtractReview outpus [\#2872](https://github.com/pypeclub/OpenPype/pull/2872) - NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867) -- NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863) -- TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859) **🐛 Bug fixes** @@ -121,16 +149,10 @@ - Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) - Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) - General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) -- General: ffmpeg was crashing on slate merge [\#2860](https://github.com/pypeclub/OpenPype/pull/2860) -- WebPublisher: Video file was published with one too many frame [\#2858](https://github.com/pypeclub/OpenPype/pull/2858) -- New Publisher: Error dialog got right styles [\#2857](https://github.com/pypeclub/OpenPype/pull/2857) -- General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855) -- Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853) **🔀 Refactored code** - Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876) -- General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854) ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) diff --git a/openpype/version.py b/openpype/version.py index 3d759096c8..c314151e9b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.2" +__version__ = "3.9.3-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 1084382d9a..dd1a666dea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.2" # OpenPype +version = "3.9.3-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 60df02602b3a27e6b8755feb0762e3639375ae2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:07:51 +0200 Subject: [PATCH 157/337] removed usage of config callback in maya --- openpype/hosts/maya/api/pipeline.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index a8834d1ea3..f6f3472eef 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,8 +9,6 @@ import maya.api.OpenMaya as om import pyblish.api import avalon.api -from avalon.lib import find_submodule - import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -20,7 +18,6 @@ from openpype.lib import ( ) from openpype.lib.path_tools import HostDirmap from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, @@ -270,21 +267,8 @@ def ls(): """ container_names = _ls() - - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "maya") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(container_names): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def containerise(name, From 7bd1f630e732ca0ea92ebbc1ecf8646b90de0c7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 12:05:41 +0200 Subject: [PATCH 158/337] moved check of representations earlier --- .../plugins/publish/integrate_ftrack_instances.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index b54db918a6..b9a486d9da 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -40,6 +40,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): def process(self, instance): self.log.debug("instance {}".format(instance)) + instance_repres = instance.data.get("representations") + if not instance_repres: + self.log.info(( + "Skipping instance. Does not have any representations {}" + ).format(str(instance))) + return + instance_version = instance.data.get("version") if instance_version is None: raise ValueError("Instance version not set") @@ -64,13 +71,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ).format(family)) return - instance_repres = instance.data.get("representations") - if not instance_repres: - self.log.info(( - "Skipping instance. Does not have any representations {}" - ).format(str(instance))) - return - # Prepare FPS instance_fps = instance.data.get("fps") if instance_fps is None: From 66209b27cd0ad2efd90655ad352b66a2a043100a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 12:05:56 +0200 Subject: [PATCH 159/337] added default asset type into integrate ftrack instances --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 8 ++++++-- 1 file changed, 6 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 b9a486d9da..5ea0469bce 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -60,8 +60,12 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if not asset_type and family_low in self.family_mapping: asset_type = self.family_mapping[family_low] - self.log.debug(self.family_mapping) - self.log.debug(family_low) + if not asset_type: + asset_type = "upload" + + self.log.debug( + "Family: {}\nMapping: {}".format(family_low, self.family_mapping) + ) # Ignore this instance if neither "ftrackFamily" or a family mapping is # found. From b9dc19a046f061c9b495fe39fb812154e8cdaf5b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 12:14:37 +0200 Subject: [PATCH 160/337] prepared functions to separate process function --- .../plugins/publish/integrate_ftrack_api.py | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 6c25b9191e..8ea2d8411b 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -464,3 +464,349 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): for asset_version in used_asset_versions: if asset_version not in instance.data[asset_versions_key]: instance.data[asset_versions_key].append(asset_version) + + def _ensure_asset_types_exists(self, session, component_list): + """Make sure that all AssetType entities exists for integration. + + Returns: + dict: All asset types by short name. + """ + # Query existing asset types + asset_types = session.query("select id, short from AssetType").all() + # Stpore all existing short names + asset_type_shorts = {asset_type["short"] for asset_type in asset_types} + # Check which asset types are missing and store them + asset_type_names_by_missing_shorts = {} + default_short_name = "upload" + for data in component_list: + asset_type_data = data.get("assettype_data") or {} + asset_type_short = asset_type_data.get("short") + if not asset_type_short: + # Use default asset type name if not set and change the + # input data + asset_type_short = default_short_name + asset_type_data["short"] = asset_type_short + data["assettype_data"] = asset_type_data + + if ( + # Skip if short name exists + asset_type_short in asset_type_shorts + # Skip if short name was already added to missing types + # and asset type name is filled + # - if asset type name is missing then try use name from other + # data + or asset_type_names_by_missing_shorts.get(asset_type_short) + ): + continue + + asset_type_names_by_missing_shorts[asset_type_short] = ( + asset_type_data.get("name") + ) + + # Create missing asset types if there are any + if asset_type_names_by_missing_shorts: + self.log.info("Creating asset types with short names: {}".format( + ", ".join(asset_type_names_by_missing_shorts.keys()) + )) + for missing_short, type_name in asset_type_names_by_missing_shorts: + # Use short for name if name is not defined + if not type_name: + type_name = missing_short + # Use short name also for name + # - there is not other source for 'name' + session.create( + "AssetType", + { + "short": missing_short, + "name": type_name + } + ) + + # Commit creation + session.commit() + # Requery asset types + asset_types = session.query( + "select id, short from AssetType" + ).all() + + return {asset_type["short"]: asset_type for asset_type in asset_types} + + def _ensure_asset_exists( + self, session, asset_data, asset_type_id, parent_id + ): + asset_name = asset_data["name"] + asset_entity = self._query_asset( + session, asset_name, asset_type_id, parent_id + ) + if asset_entity is not None: + return asset_entity + + asset_data = { + "name": asset_name, + "type_id": asset_type_id, + "context_id": parent_id + } + self.log.info("Created new Asset with data: {}.".format(asset_data)) + session.create("Asset", asset_data) + session.commit() + return self._query_asset(session, asset_name, asset_type_id, parent_id) + + def _query_asset(self, session, asset_name, asset_type_id, parent_id): + return session.query( + ( + "select id from Asset" + " where name is \"{}\"" + " and type_id is \"{}\"" + " and context_id is \"{}\"" + ).format(asset_name, asset_type_id, parent_id) + ).first() + + def _ensure_asset_version_exists( + self, session, asset_version_data, asset_id, task_entity + ): + task_id = None + if task_entity: + task_id = task_entity["id"] + + # Try query asset version by criteria (asset id and version) + version = asset_version_data.get("version") or 0 + asset_version_entity = self._query_asset_version( + session, version, asset_id + ) + + # Prepare comment value + comment = asset_version_data.get("comment") or "" + if asset_version_entity is not None: + changed = False + if comment != asset_version_entity["comment"]: + asset_version_entity["comment"] = comment + changed = True + + if task_id != asset_version_entity["task_id"]: + asset_version_entity["task_id"] = task_id + changed = True + + if changed: + session.commit() + + else: + new_asset_version_data = { + "version": version, + "asset_id": asset_id + } + if task_id: + new_asset_version_data["task_id"] = task_id + + if comment: + new_asset_version_data["comment"] = comment + + self.log.info("Created new AssetVersion with data {}".format( + new_asset_version_data + )) + session.create("AssetVersion", new_asset_version_data) + session.commit() + asset_version_entity = self._query_asset_version( + session, version, asset_id + ) + + # Set custom attributes if there were any set + custom_attrs = asset_version_data.get("custom_attributes") or {} + for attr_key, attr_value in custom_attrs.items(): + if attr_key in asset_version_entity["custom_attributes"]: + try: + asset_version_entity["custom_attributes"][attr_key] = ( + attr_value + ) + session.commit() + continue + except Exception: + session.rollback() + session._configure_locations() + + self.log.warning( + ( + "Custom Attrubute \"{0}\" is not available for" + " AssetVersion <{1}>. Can't set it's value to: \"{2}\"" + ).format( + attr_key, asset_version_entity["id"], str(attr_value) + ) + ) + + return asset_version_entity + + def _query_asset_version(self, session, version, asset_id): + return session.query( + ( + "select id, task_id, comment from AssetVersion" + " where version is \"{}\" and asset_id is \"{}\"" + ).format(version, asset_id) + ).first() + + def create_component(self, session, asset_version_entity, data): + component_data = data.get("component_data") or {} + + if not component_data.get("name"): + component_data["name"] = "main" + + version_id = asset_version_entity["id"] + component_data["version_id"] = version_id + component_entity = session.query( + ( + "select id, name from Component where name is \"{}\"" + " and version_id is \"{}\"" + ).format(component_data["name"], version_id) + ).first() + + component_overwrite = data.get("component_overwrite", False) + location = data.get("component_location", session.pick_location()) + + # Overwrite existing component data if requested. + if component_entity and component_overwrite: + origin_location = session.query( + "Location where name is \"ftrack.origin\"" + ).one() + + # Removing existing members from location + components = list(component_entity.get("members", [])) + components += [component_entity] + for component in components: + for loc in component["component_locations"]: + if location["id"] == loc["location_id"]: + location.remove_component( + component, recursive=False + ) + + # Deleting existing members on component entity + for member in component_entity.get("members", []): + session.delete(member) + del(member) + + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) + + # Reset members in memory + if "members" in component_entity.keys(): + component_entity["members"] = [] + + # Add components to origin location + try: + collection = clique.parse(data["component_path"]) + except ValueError: + # Assume its a single file + # Changing file type + name, ext = os.path.splitext(data["component_path"]) + component_entity["file_type"] = ext + + origin_location.add_component( + component_entity, data["component_path"] + ) + else: + # Changing file type + component_entity["file_type"] = collection.format("{tail}") + + # Create member components for sequence. + for member_path in collection: + + size = 0 + try: + size = os.path.getsize(member_path) + except OSError: + pass + + name = collection.match(member_path).group("index") + + member_data = { + "name": name, + "container": component_entity, + "size": size, + "file_type": os.path.splitext(member_path)[-1] + } + + component = session.create( + "FileComponent", member_data + ) + origin_location.add_component( + component, member_path, recursive=False + ) + component_entity["members"].append(component) + + # Add components to location. + location.add_component( + component_entity, origin_location, recursive=True + ) + + data["component"] = component_entity + self.log.info( + ( + "Overwriting Component with path: {0}, data: {1}," + " location: {2}" + ).format( + data["component_path"], + component_data, + location + ) + ) + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + component_metadata = component_data.pop("metadata", {}) + + # Create new component if none exists. + new_component = False + if not component_entity: + component_entity = asset_version_entity.create_component( + data["component_path"], + data=component_data, + location=location + ) + data["component"] = component_entity + self.log.info( + ( + "Created new Component with path: {0}, data: {1}," + " metadata: {2}, location: {3}" + ).format( + data["component_path"], + component_data, + component_metadata, + location + ) + ) + new_component = True + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + + # if component_data['name'] = 'ftrackreview-mp4-mp4': + # assetversion_entity["thumbnail_id"] + + # Setting assetversion thumbnail + if data.get("thumbnail"): + asset_version_entity["thumbnail_id"] = component_entity["id"] + + # Inform user about no changes to the database. + if ( + component_entity + and not component_overwrite + and not new_component + ): + data["component"] = component_entity + self.log.info( + "Found existing component, and no request to overwrite. " + "Nothing has been changed." + ) + else: + # Commit changes. + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) From ad4fe059e8b90d4e2bda589c319b3ed2e5d94812 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 12:15:40 +0200 Subject: [PATCH 161/337] simplified process function of integrate ftrack api --- .../plugins/publish/integrate_ftrack_api.py | 468 ++++-------------- 1 file changed, 103 insertions(+), 365 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 8ea2d8411b..7bba93c7cd 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -1,3 +1,15 @@ +"""Integrate components into ftrack + +Requires: + context -> ftrackSession - connected ftrack.Session + instance -> ftrackComponentsList - list of components to integrate + +Provides: + instance -> ftrackIntegratedAssetVersionsData + # legacy + instance -> ftrackIntegratedAssetVersions +""" + import os import sys import six @@ -54,6 +66,97 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): self.log.debug(query) return query + 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( + "Instance don't have components to integrate to Ftrack." + " Skipping." + ) + return + + session = instance.context.data["ftrackSession"] + context = instance.context + + parent_entity = None + default_asset_name = None + # If instance has set "ftrackEntity" or "ftrackTask" then use them from + # instance. Even if they are set to None. If they are set to None it + # has a reason. (like has different context) + if "ftrackEntity" in instance.data or "ftrackTask" in instance.data: + task_entity = instance.data.get("ftrackTask") + parent_entity = instance.data.get("ftrackEntity") + + elif "ftrackEntity" in context.data or "ftrackTask" in context.data: + task_entity = context.data.get("ftrackTask") + parent_entity = context.data.get("ftrackEntity") + + if task_entity: + default_asset_name = task_entity["name"] + parent_entity = task_entity["parent"] + + if parent_entity is None: + self.log.info(( + "Skipping ftrack integration. Instance \"{}\" does not" + " have specified ftrack entities." + ).format(str(instance))) + return + + if not default_asset_name: + default_asset_name = parent_entity["name"] + + # Change status on task + self._set_task_status(instance, task_entity, session) + + # Prepare AssetTypes + asset_types_by_short = self._ensure_asset_types_exists( + session, component_list + ) + + used_asset_versions = [] + # Iterate over components and publish + for data in component_list: + self.log.debug("data: {}".format(data)) + + # AssetType + asset_type_short = data["assettype_data"]["short"] + asset_type_entity = asset_types_by_short[asset_type_short] + + # Asset + asset_data = data.get("asset_data") or {} + if "name" not in asset_data: + asset_data["name"] = default_asset_name + asset_entity = self._ensure_asset_exists( + session, + asset_data, + asset_type_entity["id"], + parent_entity["id"] + ) + + # Asset Version + asset_version_data = data.get("assetversion_data") or {} + asset_version_entity = self._ensure_asset_version_exists( + session, asset_version_data, asset_entity["id"], task_entity + ) + + # Component + self.create_component(session, asset_version_entity, data) + + + # Backwards compatibility + if asset_version_entity not in used_asset_versions: + used_asset_versions.append(asset_version_entity) + + asset_versions_key = "ftrackIntegratedAssetVersions" + if asset_versions_key not in instance.data: + instance.data[asset_versions_key] = [] + + for asset_version in used_asset_versions: + if asset_version not in instance.data[asset_versions_key]: + instance.data[asset_versions_key].append(asset_version) + def _set_task_status(self, instance, task_entity, session): project_entity = instance.context.data.get("ftrackProject") if not project_entity: @@ -100,371 +203,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session._configure_locations() six.reraise(tp, value, tb) - def process(self, instance): - session = instance.context.data["ftrackSession"] - context = instance.context - - name = None - # If instance has set "ftrackEntity" or "ftrackTask" then use them from - # instance. Even if they are set to None. If they are set to None it - # has a reason. (like has different context) - if "ftrackEntity" in instance.data or "ftrackTask" in instance.data: - task = instance.data.get("ftrackTask") - parent = instance.data.get("ftrackEntity") - - elif "ftrackEntity" in context.data or "ftrackTask" in context.data: - task = context.data.get("ftrackTask") - parent = context.data.get("ftrackEntity") - - if task: - parent = task["parent"] - name = task - elif parent: - name = parent["name"] - - if not name: - self.log.info(( - "Skipping ftrack integration. Instance \"{}\" does not" - " have specified ftrack entities." - ).format(str(instance))) - return - - info_msg = ( - "Created new {entity_type} with data: {data}" - ", metadata: {metadata}." - ) - - used_asset_versions = [] - - self._set_task_status(instance, task, session) - - # Iterate over components and publish - for data in instance.data.get("ftrackComponentsList", []): - # AssetType - # Get existing entity. - assettype_data = {"short": "upload"} - assettype_data.update(data.get("assettype_data", {})) - self.log.debug("data: {}".format(data)) - - assettype_entity = session.query( - self.query("AssetType", assettype_data) - ).first() - - # Create a new entity if none exits. - if not assettype_entity: - assettype_entity = session.create("AssetType", assettype_data) - self.log.debug("Created new AssetType with data: {}".format( - assettype_data - )) - - # Asset - # Get existing entity. - asset_data = { - "name": name, - "type": assettype_entity, - "parent": parent, - } - asset_data.update(data.get("asset_data", {})) - - asset_entity = session.query( - self.query("Asset", asset_data) - ).first() - - self.log.info("asset entity: {}".format(asset_entity)) - - # Extracting metadata, and adding after entity creation. This is - # due to a ftrack_api bug where you can't add metadata on creation. - asset_metadata = asset_data.pop("metadata", {}) - - # Create a new entity if none exits. - if not asset_entity: - asset_entity = session.create("Asset", asset_data) - self.log.debug( - info_msg.format( - entity_type="Asset", - data=asset_data, - metadata=asset_metadata - ) - ) - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - # Adding metadata - existing_asset_metadata = asset_entity["metadata"] - existing_asset_metadata.update(asset_metadata) - asset_entity["metadata"] = existing_asset_metadata - - # AssetVersion - # Get existing entity. - assetversion_data = { - "version": 0, - "asset": asset_entity, - } - _assetversion_data = data.get("assetversion_data", {}) - assetversion_cust_attrs = _assetversion_data.pop( - "custom_attributes", {} - ) - asset_version_comment = _assetversion_data.pop( - "comment", None - ) - assetversion_data.update(_assetversion_data) - - assetversion_entity = session.query( - self.query("AssetVersion", assetversion_data) - ).first() - - # Extracting metadata, and adding after entity creation. This is - # due to a ftrack_api bug where you can't add metadata on creation. - assetversion_metadata = assetversion_data.pop("metadata", {}) - - if task: - assetversion_data['task'] = task - - # Create a new entity if none exits. - if not assetversion_entity: - assetversion_entity = session.create( - "AssetVersion", assetversion_data - ) - self.log.debug( - info_msg.format( - entity_type="AssetVersion", - data=assetversion_data, - metadata=assetversion_metadata - ) - ) - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - # Adding metadata - existing_assetversion_metadata = assetversion_entity["metadata"] - existing_assetversion_metadata.update(assetversion_metadata) - assetversion_entity["metadata"] = existing_assetversion_metadata - - # Add comment - if asset_version_comment: - assetversion_entity["comment"] = asset_version_comment - try: - session.commit() - except Exception: - session.rollback() - session._configure_locations() - self.log.warning(( - "Comment was not possible to set for AssetVersion" - "\"{0}\". Can't set it's value to: \"{1}\"" - ).format( - assetversion_entity["id"], str(asset_version_comment) - )) - - # Adding Custom Attributes - for attr, val in assetversion_cust_attrs.items(): - if attr in assetversion_entity["custom_attributes"]: - try: - assetversion_entity["custom_attributes"][attr] = val - session.commit() - continue - except Exception: - session.rollback() - session._configure_locations() - - self.log.warning(( - "Custom Attrubute \"{0}\"" - " is not available for AssetVersion <{1}>." - " Can't set it's value to: \"{2}\"" - ).format(attr, assetversion_entity["id"], str(val))) - - # Have to commit the version and asset, because location can't - # determine the final location without. - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - # Component - # Get existing entity. - component_data = { - "name": "main", - "version": assetversion_entity - } - component_data.update(data.get("component_data", {})) - - component_entity = session.query( - self.query("Component", component_data) - ).first() - - component_overwrite = data.get("component_overwrite", False) - location = data.get("component_location", session.pick_location()) - - # Overwrite existing component data if requested. - if component_entity and component_overwrite: - - origin_location = session.query( - "Location where name is \"ftrack.origin\"" - ).one() - - # Removing existing members from location - components = list(component_entity.get("members", [])) - components += [component_entity] - for component in components: - for loc in component["component_locations"]: - if location["id"] == loc["location_id"]: - location.remove_component( - component, recursive=False - ) - - # Deleting existing members on component entity - for member in component_entity.get("members", []): - session.delete(member) - del(member) - - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - # Reset members in memory - if "members" in component_entity.keys(): - component_entity["members"] = [] - - # Add components to origin location - try: - collection = clique.parse(data["component_path"]) - except ValueError: - # Assume its a single file - # Changing file type - name, ext = os.path.splitext(data["component_path"]) - component_entity["file_type"] = ext - - origin_location.add_component( - component_entity, data["component_path"] - ) - else: - # Changing file type - component_entity["file_type"] = collection.format("{tail}") - - # Create member components for sequence. - for member_path in collection: - - size = 0 - try: - size = os.path.getsize(member_path) - except OSError: - pass - - name = collection.match(member_path).group("index") - - member_data = { - "name": name, - "container": component_entity, - "size": size, - "file_type": os.path.splitext(member_path)[-1] - } - - component = session.create( - "FileComponent", member_data - ) - origin_location.add_component( - component, member_path, recursive=False - ) - component_entity["members"].append(component) - - # Add components to location. - location.add_component( - component_entity, origin_location, recursive=True - ) - - data["component"] = component_entity - msg = "Overwriting Component with path: {0}, data: {1}, " - msg += "location: {2}" - self.log.info( - msg.format( - data["component_path"], - component_data, - location - ) - ) - - # Extracting metadata, and adding after entity creation. This is - # due to a ftrack_api bug where you can't add metadata on creation. - component_metadata = component_data.pop("metadata", {}) - - # Create new component if none exists. - new_component = False - if not component_entity: - component_entity = assetversion_entity.create_component( - data["component_path"], - data=component_data, - location=location - ) - data["component"] = component_entity - msg = "Created new Component with path: {0}, data: {1}" - msg += ", metadata: {2}, location: {3}" - self.log.info( - msg.format( - data["component_path"], - component_data, - component_metadata, - location - ) - ) - new_component = True - - # Adding metadata - existing_component_metadata = component_entity["metadata"] - existing_component_metadata.update(component_metadata) - component_entity["metadata"] = existing_component_metadata - - # if component_data['name'] = 'ftrackreview-mp4-mp4': - # assetversion_entity["thumbnail_id"] - - # Setting assetversion thumbnail - if data.get("thumbnail", False): - assetversion_entity["thumbnail_id"] = component_entity["id"] - - # Inform user about no changes to the database. - if (component_entity and not component_overwrite and - not new_component): - data["component"] = component_entity - self.log.info( - "Found existing component, and no request to overwrite. " - "Nothing has been changed." - ) - else: - # Commit changes. - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - if assetversion_entity not in used_asset_versions: - used_asset_versions.append(assetversion_entity) - - asset_versions_key = "ftrackIntegratedAssetVersions" - if asset_versions_key not in instance.data: - instance.data[asset_versions_key] = [] - - for asset_version in used_asset_versions: - if asset_version not in instance.data[asset_versions_key]: - instance.data[asset_versions_key].append(asset_version) - def _ensure_asset_types_exists(self, session, component_list): """Make sure that all AssetType entities exists for integration. From 89d29a1d87ffe21e5d6d6d187ebb6e268bf289a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 12:16:03 +0200 Subject: [PATCH 162/337] store more data about asset versions in integrate ftrack api --- .../plugins/publish/integrate_ftrack_api.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 7bba93c7cd..7ebf807f55 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -115,6 +115,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session, component_list ) + asset_versions_data_by_id = {} used_asset_versions = [] # Iterate over components and publish for data in component_list: @@ -144,11 +145,27 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): # 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: + asset_versions_data_by_id[version_id] = { + "asset_version": asset_version_entity, + "component_items": [] + } + + asset_versions_data_by_id[version_id]["component_items"].append( + data + ) # Backwards compatibility if asset_version_entity not in used_asset_versions: used_asset_versions.append(asset_version_entity) + instance.data["ftrackIntegratedAssetVersionsData"] = ( + asset_versions_data_by_id + ) + + # Backwards compatibility asset_versions_key = "ftrackIntegratedAssetVersions" if asset_versions_key not in instance.data: instance.data[asset_versions_key] = [] From 312d0309ab92de834629c58587f1a758d1d1e90c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 12:36:09 +0200 Subject: [PATCH 163/337] Fix - reworked alternative sites Implements recursive relationship between alternative sites --- openpype/plugins/publish/integrate_new.py | 80 +++++++++++++++++------ 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 959fd3bbee..ed1c02b825 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -1116,18 +1116,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 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 always_accesible: + 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 - # add alternative sites - rec = self._add_alternative_sites(system_sync_server_presets, - already_attached_sites, - rec) - log.debug("final sites:: {}".format(rec["sites"])) return rec @@ -1158,22 +1157,65 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ 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 = {} 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 - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = real_created + if not alt_site_pairs.get(site_name): + alt_site_pairs[site_name] = [] - return rec + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + if not alt_site_pairs.get(alt_site): + alt_site_pairs[alt_site] = [] + alt_site_pairs[alt_site].extend([site_name]) + + # transitive relationship, eg site is alternative to another which is + # alternative to nex site + loop = True + while loop: + loop = False + for site_name, alt_sites in alt_site_pairs.items(): + for alt_site in alt_sites: + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name: + continue + + for alt_alt_site in alt_site_pairs.get(alt_site, []): + if ( alt_alt_site != site_name + and alt_alt_site not in alt_sites): + alt_site_pairs[site_name].append(alt_alt_site) + loop = True + + return alt_site_pairs def handle_destination_files(self, integrated_file_sizes, mode): """ Clean destination files From 9f9f47145b0a1e88c5e28c1b2ade7c842191e14c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 12:45:49 +0200 Subject: [PATCH 164/337] Fix - added active site from settings if same as local id Without this Tray configuring background process will not show proper site in LS dropdown --- openpype/modules/sync_server/sync_server_module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index caf58503f1..ddcf16a410 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -848,6 +848,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if self.enabled and sync_settings.get('enabled'): sites.append(self.LOCAL_SITE) + active_site = sync_settings["config"]["active_site"] + # for Tray running background process + if active_site == get_local_site_id() and active_site not in sites: + sites.append(active_site) + return sites def tray_init(self): From 3d0238a6caba987a7d959de98354713518899ac3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:29 +0200 Subject: [PATCH 165/337] removed unused imports --- .../ftrack/event_handlers_user/action_create_folders.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py index d15a865124..0ed12bd03e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py @@ -1,11 +1,6 @@ import os from openpype_modules.ftrack.lib import BaseAction, statics_icon -from avalon import lib as avalonlib -from openpype.api import ( - Anatomy, - get_project_settings -) -from openpype.lib import ApplicationManager +from openpype.api import Anatomy class CreateFolders(BaseAction): From 840daefb9fe188b91deefb40b3669caf5f4fb6bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 13:31:20 +0200 Subject: [PATCH 166/337] added example_addons into ignored filenames --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 5cdeb86087..2d5545d135 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -37,6 +37,7 @@ IGNORED_DEFAULT_FILENAMES = ( "__init__.py", "base.py", "interfaces.py", + "example_addons", ) From 02afd4b915c078b0bf67b02a61a0244e819facb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 13:31:46 +0200 Subject: [PATCH 167/337] subfolder of module must have init file --- openpype/modules/base.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 2d5545d135..94aa8fca1b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -304,7 +304,13 @@ def _load_modules(): fullpath = os.path.join(current_dir, filename) basename, ext = os.path.splitext(filename) - if not os.path.isdir(fullpath) and ext not in (".py", ): + if os.path.isdir(fullpath): + # Check existence of init fil + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + continue + + elif ext not in (".py", ): continue try: @@ -342,7 +348,13 @@ def _load_modules(): fullpath = os.path.join(dirpath, filename) basename, ext = os.path.splitext(filename) - if not os.path.isdir(fullpath) and ext not in (".py", ): + if os.path.isdir(fullpath): + # Check existence of init fil + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + continue + + elif ext not in (".py", ): continue # TODO add more logic how to define if folder is module or not From 062b584ecbc47c46f5f3933d346ffdf67cb6d48b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 13:41:31 +0200 Subject: [PATCH 168/337] added default_modules subfolder into ignored filenames --- openpype/modules/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 94aa8fca1b..23c908299f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -38,6 +38,7 @@ IGNORED_DEFAULT_FILENAMES = ( "base.py", "interfaces.py", "example_addons", + "default_modules", ) @@ -308,6 +309,9 @@ def _load_modules(): # Check existence of init fil init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): + log.debug(( + "Module directory does not contan __init__.py file {}" + ).format(fullpath)) continue elif ext not in (".py", ): @@ -352,6 +356,9 @@ def _load_modules(): # Check existence of init fil init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): + log.debug(( + "Module directory does not contan __init__.py file {}" + ).format(fullpath)) continue elif ext not in (".py", ): From 4da7f7c1cce65434d385bf8cad5dc678226c62e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 13:48:21 +0200 Subject: [PATCH 169/337] changed integrate ftrack note to b able add published paths into asset version comment --- .../plugins/publish/integrate_ftrack_note.py | 196 +++++++++--------- .../defaults/project_settings/ftrack.json | 2 +- .../defaults/project_settings/global.json | 2 +- .../schema_project_ftrack.json | 8 +- 4 files changed, 111 insertions(+), 97 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index acd295854d..c165e99918 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -15,10 +15,112 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): # Can be set in presets: # - Allows only `intent` and `comment` keys + note_template = None + # Backwards compatibility note_with_intent_template = "{intent}: {comment}" # - note label must exist in Ftrack note_labels = [] + def process(self, instance): + # Check if there are any integrated AssetVersion entities + asset_versions_key = "ftrackIntegratedAssetVersionsData" + asset_versions_data_by_id = instance.data.get(asset_versions_key) + if not asset_versions_data_by_id: + self.log.info("There are any integrated AssetVersions") + return + + comment = (instance.context.data.get("comment") or "").strip() + if not comment: + self.log.info("Comment is not set.") + else: + self.log.debug("Comment is set to `{}`".format(comment)) + + session = instance.context.data["ftrackSession"] + + intent = instance.context.data.get("intent") + if intent and isinstance(intent, dict): + intent_val = intent.get("value") + intent_label = intent.get("label") + else: + intent_val = intent_label = intent + + final_intent_label = None + if intent_val: + final_intent_label = self.get_intent_label(session, intent_val) + if final_intent_label is None: + final_intent_label = intent_label + + # if intent label is set then format comment + # - it is possible that intent_label is equal to "" (empty string) + if final_intent_label: + self.log.debug( + "Intent label is set to `{}`.".format(final_intent_label) + ) + + elif intent_val: + self.log.debug(( + "Intent is set to `{}` and was not added" + " to comment because label is set to `{}`." + ).format(intent_val, final_intent_label)) + + else: + self.log.debug("Intent is not set.") + + user = session.query( + "User where username is \"{}\"".format(session.api_user) + ).first() + if not user: + self.log.warning( + "Was not able to query current User {}".format( + session.api_user + ) + ) + + labels = [] + if self.note_labels: + all_labels = session.query("select id, name from NoteLabel").all() + labels_by_low_name = {lab["name"].lower(): lab for lab in all_labels} + for _label in self.note_labels: + label = labels_by_low_name.get(_label.lower()) + if not label: + self.log.warning( + "Note Label `{}` was not found.".format(_label) + ) + continue + + labels.append(label) + + 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"] + + published_paths = set() + for component_item in component_items: + published_paths.add(component_item["component_path"]) + + # Backwards compatibility for older settings using + # attribute 'note_with_intent_template' + template = self.note_template + if template is None: + template = self.note_with_intent_template + comment = template.format(**{ + "intent": final_intent_label, + "comment": comment, + "published_paths": "\n".join(sorted(published_paths)) + }) + asset_version.create_note(comment, author=user, labels=labels) + + try: + session.commit() + self.log.debug("Note added to AssetVersion \"{}\"".format( + str(asset_version) + )) + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) + def get_intent_label(self, session, intent_value): if not intent_value: return @@ -45,12 +147,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): if not items: return - if sys.version_info[0] < 3: - string_type = basestring - else: - string_type = str - - if isinstance(items, string_type): + if isinstance(items, six.string_types): items = json.loads(items) intent_label = None @@ -60,90 +157,3 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): break return intent_label - - def process(self, instance): - comment = (instance.context.data.get("comment") or "").strip() - if not comment: - self.log.info("Comment is not set.") - return - - self.log.debug("Comment is set to `{}`".format(comment)) - - session = instance.context.data["ftrackSession"] - - intent = instance.context.data.get("intent") - if intent and isinstance(intent, dict): - intent_val = intent.get("value") - intent_label = intent.get("label") - else: - intent_val = intent_label = intent - - final_label = None - if intent_val: - final_label = self.get_intent_label(session, intent_val) - if final_label is None: - final_label = intent_label - - # if intent label is set then format comment - # - it is possible that intent_label is equal to "" (empty string) - if final_label: - msg = "Intent label is set to `{}`.".format(final_label) - comment = self.note_with_intent_template.format(**{ - "intent": final_label, - "comment": comment - }) - - elif intent_val: - msg = ( - "Intent is set to `{}` and was not added" - " to comment because label is set to `{}`." - ).format(intent_val, final_label) - - else: - msg = "Intent is not set." - - self.log.debug(msg) - - asset_versions_key = "ftrackIntegratedAssetVersions" - asset_versions = instance.data.get(asset_versions_key) - if not asset_versions: - self.log.info("There are any integrated AssetVersions") - return - - user = session.query( - "User where username is \"{}\"".format(session.api_user) - ).first() - if not user: - self.log.warning( - "Was not able to query current User {}".format( - session.api_user - ) - ) - - labels = [] - if self.note_labels: - all_labels = session.query("NoteLabel").all() - labels_by_low_name = {lab["name"].lower(): lab for lab in all_labels} - for _label in self.note_labels: - label = labels_by_low_name.get(_label.lower()) - if not label: - self.log.warning( - "Note Label `{}` was not found.".format(_label) - ) - continue - - labels.append(label) - - for asset_version in asset_versions: - asset_version.create_note(comment, author=user, labels=labels) - - try: - session.commit() - self.log.debug("Note added to AssetVersion \"{}\"".format( - str(asset_version) - )) - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index ca1cfe1e12..9b350ec88d 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -354,7 +354,7 @@ }, "IntegrateFtrackNote": { "enabled": true, - "note_with_intent_template": "{intent}: {comment}", + "note_template": "{intent}: {comment}", "note_labels": [] }, "ValidateFtrackAttributes": { diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index ed28d357f2..4c94eee254 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -190,7 +190,7 @@ "tasks": [], "template_name": "simpleUnrealTexture" }, - { + { "families": [ "staticMesh", "skeletalMesh" 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 fb384882c6..2b62d67c98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -738,10 +738,14 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "label", + "label": "Template may contain formatting keys {intent}, {comment} and {published_paths}." + }, { "type": "text", - "key": "note_with_intent_template", - "label": "Note with intent template" + "key": "note_template", + "label": "Note template" }, { "type": "list", From 905eccdc4197cdf0db4c2dfdbcca86619736ef02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 13:58:53 +0200 Subject: [PATCH 170/337] fixed registered host callbacks --- openpype/hosts/flame/startup/openpype_in_flame.py | 9 +++++---- .../hosts/houdini/plugins/publish/collect_inputs.py | 5 +++-- .../houdini/plugins/publish/increment_current_file.py | 4 ++-- openpype/hosts/houdini/plugins/publish/save_scene.py | 5 +++-- openpype/hosts/maya/api/lib.py | 5 +++-- openpype/hosts/photoshop/api/pipeline.py | 4 ++-- openpype/hosts/tvpaint/plugins/load/load_workfile.py | 5 +++-- openpype/lib/avalon_context.py | 3 ++- openpype/pipeline/create/context.py | 2 +- openpype/pipeline/create/legacy_create.py | 3 ++- .../plugins/publish/collect_scene_loaded_versions.py | 5 +++-- openpype/scripts/fusion_switch_shot.py | 3 ++- openpype/tools/mayalookassigner/commands.py | 11 +++++++---- openpype/tools/mayalookassigner/vray_proxies.py | 5 +++-- openpype/tools/publisher/control.py | 8 +++++--- openpype/tools/sceneinventory/model.py | 9 ++++++--- openpype/tools/subsetmanager/model.py | 4 ++-- openpype/tools/subsetmanager/window.py | 9 ++++----- openpype/tools/workfiles/app.py | 3 ++- 19 files changed, 60 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/flame/startup/openpype_in_flame.py b/openpype/hosts/flame/startup/openpype_in_flame.py index 7015abc7f4..779143cfb3 100644 --- a/openpype/hosts/flame/startup/openpype_in_flame.py +++ b/openpype/hosts/flame/startup/openpype_in_flame.py @@ -4,17 +4,18 @@ from Qt import QtWidgets from pprint import pformat import atexit -import avalon import openpype.hosts.flame.api as opfapi -from openpype.pipeline import install_host +from openpype.pipeline import ( + install_host, + registered_host, +) def openpype_install(): """Registering OpenPype in context """ install_host(opfapi) - print("Avalon registered hosts: {}".format( - avalon.api.registered_host())) + print("Avalon registered hosts: {}".format(registered_host())) # Exception handler diff --git a/openpype/hosts/houdini/plugins/publish/collect_inputs.py b/openpype/hosts/houdini/plugins/publish/collect_inputs.py index 39e2737e8c..8c7098c710 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_inputs.py +++ b/openpype/hosts/houdini/plugins/publish/collect_inputs.py @@ -1,6 +1,7 @@ -import avalon.api as api import pyblish.api +from openpype.pipeline import registered_host + def collect_input_containers(nodes): """Collect containers that contain any of the node in `nodes`. @@ -18,7 +19,7 @@ def collect_input_containers(nodes): lookup = frozenset(nodes) containers = [] - host = api.registered_host() + host = registered_host() for container in host.ls(): node = container["node"] diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 31c2954ee7..c5cacd1880 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -1,8 +1,8 @@ import pyblish.api -import avalon.api from openpype.api import version_up from openpype.action import get_errored_plugins_from_data +from openpype.pipeline import registered_host class IncrementCurrentFile(pyblish.api.InstancePlugin): @@ -41,7 +41,7 @@ class IncrementCurrentFile(pyblish.api.InstancePlugin): ) # Filename must not have changed since collecting - host = avalon.api.registered_host() + host = registered_host() current_file = host.current_file() assert ( context.data["currentFile"] == current_file diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index fe5962fbd3..6128c7af77 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -1,5 +1,6 @@ import pyblish.api -import avalon.api + +from openpype.pipeline import registered_host class SaveCurrentScene(pyblish.api.ContextPlugin): @@ -12,7 +13,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): def process(self, context): # Filename must not have changed since collecting - host = avalon.api.registered_host() + host = registered_host() current_file = host.current_file() assert context.data['currentFile'] == current_file, ( "Collected filename from current scene name." diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 92fc5133a9..3364e75769 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -26,6 +26,7 @@ from openpype.pipeline import ( loaders_from_representation, get_representation_path, load_container, + registered_host, ) from .commands import reset_frame_range @@ -1574,7 +1575,7 @@ def assign_look_by_version(nodes, version_id): "name": "json"}) # See if representation is already loaded, if so reuse it. - host = api.registered_host() + host = registered_host() representation_id = str(look_representation['_id']) for container in host.ls(): if (container['loader'] == "LookLoader" and @@ -2612,7 +2613,7 @@ def get_attr_in_layer(attr, layer): def fix_incompatible_containers(): """Backwards compatibility: old containers to use new ReferenceLoader""" - host = api.registered_host() + host = registered_host() for container in host.ls(): loader = container['loader'] diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 7fdaa61b40..1f069c2636 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -3,7 +3,6 @@ from Qt import QtWidgets from bson.objectid import ObjectId import pyblish.api -import avalon.api from avalon import io from openpype.api import Logger @@ -14,6 +13,7 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, + registered_host, ) import openpype.hosts.photoshop @@ -33,7 +33,7 @@ def check_inventory(): if not lib.any_outdated(): return - host = avalon.api.registered_host() + host = registered_host() outdated_containers = [] for container in host.ls(): representation = container['representation'] diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index d224cfc390..1ce5449065 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -1,12 +1,13 @@ import os -from avalon import api, io +from avalon import io from openpype.lib import ( StringTemplate, get_workfile_template_key_from_context, get_workdir_data, get_last_workfile_with_version, ) +from openpype.pipeline import registered_host from openpype.api import Anatomy from openpype.hosts.tvpaint.api import lib, pipeline, plugin @@ -22,7 +23,7 @@ class LoadWorkfile(plugin.Loader): def load(self, context, name, namespace, options): # Load context of current workfile as first thing # - which context and extension has - host = api.registered_host() + host = registered_host() current_file = host.current_file() context = pipeline.get_current_workfile_context() diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 0348d88be2..e82dcc558f 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -161,9 +161,10 @@ def is_latest(representation): @with_avalon def any_outdated(): """Return whether the current scene has any outdated content""" + from openpype.pipeline import registered_host checked = set() - host = avalon.api.registered_host() + host = registered_host() for container in host.ls(): representation = container['representation'] if representation in checked: diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3efdb0e5c3..0cc2819172 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -356,7 +356,7 @@ class CreatedInstance: already existing instance. creator(BaseCreator): Creator responsible for instance. host(ModuleType): Host implementation loaded with - `avalon.api.registered_host`. + `openpype.pipeline.registered_host`. new(bool): Is instance new. """ # Keys that can't be changed or removed from data after loading using diff --git a/openpype/pipeline/create/legacy_create.py b/openpype/pipeline/create/legacy_create.py index cf6629047e..46e0e3d663 100644 --- a/openpype/pipeline/create/legacy_create.py +++ b/openpype/pipeline/create/legacy_create.py @@ -142,7 +142,8 @@ def legacy_create(Creator, name, asset, options=None, data=None): Name of instance """ - from avalon.api import registered_host + from openpype.pipeline import registered_host + host = registered_host() plugin = Creator(name, asset, options, data) diff --git a/openpype/plugins/publish/collect_scene_loaded_versions.py b/openpype/plugins/publish/collect_scene_loaded_versions.py index 6746757e5f..e54592abb8 100644 --- a/openpype/plugins/publish/collect_scene_loaded_versions.py +++ b/openpype/plugins/publish/collect_scene_loaded_versions.py @@ -1,7 +1,8 @@ from bson.objectid import ObjectId import pyblish.api -from avalon import api, io +from avalon import io +from openpype.pipeline import registered_host class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): @@ -24,7 +25,7 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): ] def process(self, context): - host = api.registered_host() + host = registered_host() if host is None: self.log.warn("No registered host.") return diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index 6db8ff36a8..85a5821c6e 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -9,6 +9,7 @@ import avalon.fusion # Config imports import openpype.lib as pype +from openpype.pipeline import registered_host import openpype.hosts.fusion.lib as fusion_lib from openpype.lib.avalon_context import get_workdir_from_session @@ -176,7 +177,7 @@ def switch(asset_name, filepath=None, new=True): current_comp = fusion.LoadComp(filepath, quiet=True) assert current_comp is not None, "Fusion could not load '%s'" % filepath - host = api.registered_host() + host = registered_host() containers = list(host.ls()) assert containers, "Nothing to update" diff --git a/openpype/tools/mayalookassigner/commands.py b/openpype/tools/mayalookassigner/commands.py index 78fd51c7a3..8fd592d347 100644 --- a/openpype/tools/mayalookassigner/commands.py +++ b/openpype/tools/mayalookassigner/commands.py @@ -5,9 +5,12 @@ import os from bson.objectid import ObjectId import maya.cmds as cmds -from avalon import io, api +from avalon import io -from openpype.pipeline import remove_container +from openpype.pipeline import ( + remove_container, + registered_host, +) from openpype.hosts.maya.api import lib from .vray_proxies import get_alembic_ids_cache @@ -79,7 +82,7 @@ def get_all_asset_nodes(): list: list of dictionaries """ - host = api.registered_host() + host = registered_host() nodes = [] for container in host.ls(): @@ -192,7 +195,7 @@ def remove_unused_looks(): """ - host = api.registered_host() + host = registered_host() unused = [] for container in host.ls(): diff --git a/openpype/tools/mayalookassigner/vray_proxies.py b/openpype/tools/mayalookassigner/vray_proxies.py index 25621fc652..c97664f3cb 100644 --- a/openpype/tools/mayalookassigner/vray_proxies.py +++ b/openpype/tools/mayalookassigner/vray_proxies.py @@ -11,13 +11,14 @@ from bson.objectid import ObjectId import alembic.Abc from maya import cmds -from avalon import io, api +from avalon import io from openpype.pipeline import ( load_container, loaders_from_representation, discover_loader_plugins, get_representation_path, + registered_host, ) from openpype.hosts.maya.api import lib @@ -188,7 +189,7 @@ def load_look(version_id): "name": "ma"}) # See if representation is already loaded, if so reuse it. - host = api.registered_host() + host = registered_host() representation_id = str(look_representation['_id']) for container in host.ls(): if (container['loader'] == "LookLoader" and diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 6707feac9c..2973d6a5bb 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -11,10 +11,12 @@ try: except Exception: from openpype.lib.python_2_comp import WeakMethod -import avalon.api import pyblish.api -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + PublishValidationError, + registered_host, +) from openpype.pipeline.create import CreateContext from Qt import QtCore @@ -353,7 +355,7 @@ class PublisherController: """ def __init__(self, dbcon=None, headless=False): self.log = logging.getLogger("PublisherController") - self.host = avalon.api.registered_host() + self.host = registered_host() self.headless = headless self.create_context = CreateContext( diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 091d6ca925..f8fd8a911a 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -7,8 +7,11 @@ from Qt import QtCore, QtGui import qtawesome from bson.objectid import ObjectId -from avalon import api, io, schema -from openpype.pipeline import HeroVersionType +from avalon import io, schema +from openpype.pipeline import ( + HeroVersionType, + registered_host, +) from openpype.style import get_default_entity_icon_color from openpype.tools.utils.models import TreeModel, Item from openpype.modules import ModulesManager @@ -181,7 +184,7 @@ class InventoryModel(TreeModel): def refresh(self, selected=None, items=None): """Refresh the model""" - host = api.registered_host() + host = registered_host() if not items: # for debugging or testing, injecting items from outside items = host.ls() diff --git a/openpype/tools/subsetmanager/model.py b/openpype/tools/subsetmanager/model.py index b76c3c2343..760a167b42 100644 --- a/openpype/tools/subsetmanager/model.py +++ b/openpype/tools/subsetmanager/model.py @@ -2,7 +2,7 @@ import uuid from Qt import QtCore, QtGui -from avalon import api +from openpype.pipeline import registered_host ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 @@ -21,7 +21,7 @@ class InstanceModel(QtGui.QStandardItemModel): self._instances_by_item_id = {} instances = None - host = api.registered_host() + host = registered_host() list_instances = getattr(host, "list_instances", None) if list_instances: instances = list_instances() diff --git a/openpype/tools/subsetmanager/window.py b/openpype/tools/subsetmanager/window.py index a53af52174..6314e67015 100644 --- a/openpype/tools/subsetmanager/window.py +++ b/openpype/tools/subsetmanager/window.py @@ -4,9 +4,8 @@ import sys from Qt import QtWidgets, QtCore import qtawesome -from avalon import api - from openpype import style +from openpype.pipeline import registered_host from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.lib import ( iter_model_rows, @@ -106,7 +105,7 @@ class SubsetManagerWindow(QtWidgets.QDialog): self._details_widget.set_details(container, item_id) def _on_save(self): - host = api.registered_host() + host = registered_host() if not hasattr(host, "save_instances"): print("BUG: Host does not have \"save_instances\" method") return @@ -141,7 +140,7 @@ class SubsetManagerWindow(QtWidgets.QDialog): # Prepare menu menu = QtWidgets.QMenu(self) actions = [] - host = api.registered_host() + host = registered_host() if hasattr(host, "remove_instance"): action = QtWidgets.QAction("Remove instance", menu) action.setData(host.remove_instance) @@ -176,7 +175,7 @@ class SubsetManagerWindow(QtWidgets.QDialog): self._details_widget.set_details(None, None) self._model.refresh() - host = api.registered_host() + host = registered_host() dev_mode = os.environ.get("AVALON_DEVELOP_MODE") or "" editable = False if dev_mode.lower() in ("1", "yes", "true", "on"): diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index f0e7900cf5..38e1911060 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -3,6 +3,7 @@ import logging from avalon import api +from openpype.pipeline import registered_host from openpype.tools.utils import qt_app_context from .window import Window @@ -47,7 +48,7 @@ def show(root=None, debug=False, parent=None, use_context=True, save=True): except (AttributeError, RuntimeError): pass - host = api.registered_host() + host = registered_host() validate_host_requirements(host) if debug: From 2cd10be1557ff7600b44417179d4cfed2f2a8ea3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 14:00:36 +0200 Subject: [PATCH 171/337] fixed registered host in workfiles tool --- openpype/tools/workfiles/files_widget.py | 3 ++- openpype/tools/workfiles/save_as_dialog.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 56af7752da..bb2ded3b94 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -18,6 +18,7 @@ from openpype.lib.avalon_context import ( update_current_task, compute_session_changes ) +from openpype.pipeline import registered_host from .model import ( WorkAreaFilesModel, PublishFilesModel, @@ -93,7 +94,7 @@ class FilesWidget(QtWidgets.QWidget): # This is not root but workfile directory self._workfiles_root = None self._workdir_path = None - self.host = api.registered_host() + self.host = registered_host() # Whether to automatically select the latest modified # file on a refresh of the files model. diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index f5ae393d0f..0a7c7821ba 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -11,6 +11,7 @@ from openpype.lib import ( get_last_workfile_with_version, get_workdir_data, ) +from openpype.pipeline import registered_host from openpype.tools.utils import PlaceholderLineEdit log = logging.getLogger(__name__) @@ -65,7 +66,7 @@ class CommentMatcher(object): return # Create a regex group for extensions - extensions = api.registered_host().file_extensions() + extensions = registered_host().file_extensions() any_extension = "(?:{})".format( "|".join(re.escape(ext[1:]) for ext in extensions) ) @@ -200,7 +201,7 @@ class SaveAsDialog(QtWidgets.QDialog): self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.result = None - self.host = api.registered_host() + self.host = registered_host() self.root = root self.work_file = None self._extensions = extensions From d41e99cb345bb3407e5e1af5d05de981f1a49795 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 14:14:48 +0200 Subject: [PATCH 172/337] Fix - added recursive configuration for alternative sites --- .../modules/sync_server/sync_server_module.py | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 7126c17e17..d2f341786c 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -229,6 +229,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): attached_sites[remote_site] = create_metadata(remote_site, created=False) + attached_sites = self._add_alternative_sites(attached_sites) # add skeleton for sites where it should be always synced to # usually it would be a backup site which is handled by separate # background process @@ -236,8 +237,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if site not in attached_sites: attached_sites[site] = create_metadata(site, created=False) - attached_sites = self._add_alternative_sites(attached_sites) - return list(attached_sites.values()) def _get_always_accessible_sites(self, project_name): @@ -264,9 +263,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ additional_sites = self.sync_system_settings.get("sites", {}) + alt_site_pairs = self._get_alt_site_pairs(additional_sites) + for site_name, site_info in additional_sites.items(): # Get alternate sites (stripped names) for this site name - alt_sites = site_info.get("alternative_sites", []) + alt_sites = alt_site_pairs.get(site_name) alt_sites = [site.strip() for site in alt_sites] alt_sites = set(alt_sites) @@ -289,6 +290,44 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return 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 = {} + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + if not alt_site_pairs.get(site_name): + alt_site_pairs[site_name] = [] + + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + if not alt_site_pairs.get(alt_site): + alt_site_pairs[alt_site] = [] + alt_site_pairs[alt_site].extend([site_name]) + + # transitive relationship, eg site is alternative to another which is + # alternative to nex site + loop = True + while loop: + loop = False + for site, alt_sites in alt_site_pairs.items(): + for alt_site in alt_sites: + for alt_alt_site in alt_site_pairs.get(alt_site, []): + if ( alt_alt_site != site + and alt_alt_site not in alt_sites): + alt_site_pairs[site].append(alt_alt_site) + loop = True + + return alt_site_pairs + def clear_project(self, collection, site_name): """ Clear 'collection' of 'site_name' and its local files From b3767433733c07d7a89ef53f2b62edf9c09fc37e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 14:15:38 +0200 Subject: [PATCH 173/337] Fix - moved conftest to be applicable for all kind of tests --- tests/{integration => }/conftest.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{integration => }/conftest.py (100%) diff --git a/tests/integration/conftest.py b/tests/conftest.py similarity index 100% rename from tests/integration/conftest.py rename to tests/conftest.py From 586a429beabec5b7c71ee44ba6896f383ae4a2e6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 14:15:56 +0200 Subject: [PATCH 174/337] Added basic test for alternate site method --- .../modules/sync_server/test_module_api.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/unit/openpype/modules/sync_server/test_module_api.py diff --git a/tests/unit/openpype/modules/sync_server/test_module_api.py b/tests/unit/openpype/modules/sync_server/test_module_api.py new file mode 100644 index 0000000000..377045e229 --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/test_module_api.py @@ -0,0 +1,59 @@ +"""Test file for Sync Server, tests API methods, currently for integrate_new + + File: + creates temporary directory and downloads .zip file from GDrive + unzips .zip file + uses content of .zip file (MongoDB's dumps) to import to new databases + with use of 'monkeypatch_session' modifies required env vars + temporarily + runs battery of tests checking that site operation for Sync Server + module are working + removes temporary folder + removes temporary databases (?) +""" +import pytest + +import sys, os + +os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" +os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" +os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" +os.environ["AVALON_TIMEOUT"] = '3000' +os.environ["OPENPYPE_DEBUG"] = "3" + +os.environ["AVALON_PROJECT"] = "petr_test" +os.environ["AVALON_DB"] = "avalon" +os.environ["QT_PREFERRED_BINDING"] = "PySide2" +os.environ["QT_VERBOSE"] = "true" + +from tests.lib.testing_classes import ModuleUnitTest + + +class TestModuleApi(ModuleUnitTest): + + REPRESENTATION_ID = "60e578d0c987036c6a7b741d" + + TEST_FILES = [("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", + "test_site_operations.zip", '')] + + @pytest.fixture(scope="module") + def setup_sync_server_module(self, dbcon): + """Get sync_server_module from ModulesManager""" + from openpype.modules import ModulesManager + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + yield sync_server + + def test_get_alt_site_pairs(self, setup_sync_server_module): + conf_sites = {'SFTP': {"alternative_sites": ["studio"]}, + "studio2": {"alternative_sites": ["studio"]}} + + ret = setup_sync_server_module._get_alt_site_pairs(conf_sites) + expected = {"SFTP": ["studio", "studio2"], + "studio": ["SFTP", "studio2"], + "studio2": ["studio", "SFTP"]} + assert ret == expected, "Not matching result" + + +test_case = TestModuleApi() From 87f40d53e098ce3cef8bc31fc733e73df36f9cc3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 14:44:03 +0200 Subject: [PATCH 175/337] flame: adding media info getter obj --- openpype/hosts/flame/api/lib.py | 116 +++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 7316fa1c5b..4e989abb9e 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1,9 +1,14 @@ import sys import os import re +import six import json import pickle +import itertools import contextlib +import xml.etree.cElementTree as cET +from copy import deepcopy +from xml.etree import ElementTree as ET from pprint import pformat from .constants import ( MARKER_COLOR, @@ -12,9 +17,10 @@ from .constants import ( COLOR_MAP, MARKER_PUBLISH_DEFAULT ) -from openpype.api import Logger -log = Logger.get_logger(__name__) +import openpype.api as openpype + +log = openpype.Logger.get_logger(__name__) FRAME_PATTERN = re.compile(r"[\._](\d+)[\.]") @@ -711,3 +717,109 @@ def get_batch_group_from_desktop(name): for bgroup in project_desktop.batch_groups: if bgroup.name.get_value() in name: return bgroup + + +class MediaInfoFile: + media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" + tmp_name = "_tmp.clip" + tmp_file = None + + out_feed_nb_ticks = None + out_feed_fps = None + out_feed_drop_mode = None + + log = log + + def __init__(self, path): + # test if media script paht exists + self._validate_media_script_path() + + # derivate other feed variables + self.feed_basename = os.path.basename(path) + self.feed_dir = os.path.dirname(path) + self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() + + self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) + + # remove previously generated temp files + # it will be regenerated + self._clear_tmp_file() + + self.log.info("Temp File: {}".format(self.tmp_file)) + + def _validate_media_script_path(self): + if not os.path.isfile(self.media_script_path): + raise IOError("Media Scirpt does not exist: `{}`".format( + self.media_script_path)) + + def _generate_media_info_file(self): + # Create cmd arguments for gettig xml file info file + cmd_args = [ + self.media_script_path, + "-e", self.feed_ext, + "-o", self.tmp_file, + self.feed_dir + ] + + # execute creation of clip xml template data + try: + openpype.run_subprocess(cmd_args) + self._make_single_clip_media_info() + except TypeError: + self.log.error("Error creating self.tmp_file") + six.reraise(*sys.exc_info()) + + def _make_single_clip_media_info(self): + with open(self.tmp_file) as f: + lines = f.readlines() + _added_root = itertools.chain( + "", deepcopy(lines)[1:], "") + new_root = ET.fromstringlist(_added_root) + + # find the clip which is matching to my input name + xml_clips = new_root.findall("clip") + matching_clip = None + for xml_clip in xml_clips: + if xml_clip.find("name").text in self.feed_basename: + matching_clip = xml_clip + + if matching_clip is None: + # return warning there is missing clip + raise ET.ParseError( + "Missing clip in `{}`. Available clips {}".format( + self.feed_basename, [ + xml_clip.find("name").text + for xml_clip in xml_clips + ] + )) + + self._write_result_xml_to_file(self.tmp_file, matching_clip) + + def _clear_tmp_file(self): + if os.path.isfile(self.tmp_file): + os.remove(self.tmp_file) + + def _get_time_info_from_origin(self, xml_data): + try: + for out_track in xml_data.iter('track'): + for out_feed in out_track.iter('feeds'): + out_feed_nb_ticks_obj = out_feed.find( + 'startTimecode/nbTicks') + self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text + out_feed_fps_obj = out_feed.find( + 'startTimecode/rate') + self.out_feed_fps = out_feed_fps_obj.text + out_feed_drop_mode_obj = out_feed.find( + 'startTimecode/dropMode') + self.out_feed_drop_mode = out_feed_drop_mode_obj.text + break + else: + continue + except Exception as msg: + self.log.warning(msg) + + def _write_result_xml_to_file(self, file, xml_data): + # save it as new file + tree = cET.ElementTree(xml_data) + tree.write(file, xml_declaration=True, + method='xml', encoding='UTF-8') \ No newline at end of file From 7dc2c618a0b6ffd80529620d576f929afaa01cf0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 14:53:13 +0200 Subject: [PATCH 176/337] flame: update media info file generator --- openpype/hosts/flame/api/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 4e989abb9e..633854168a 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -724,6 +724,7 @@ class MediaInfoFile: tmp_name = "_tmp.clip" tmp_file = None + clip_data = None out_feed_nb_ticks = None out_feed_fps = None out_feed_drop_mode = None @@ -752,7 +753,7 @@ class MediaInfoFile: raise IOError("Media Scirpt does not exist: `{}`".format( self.media_script_path)) - def _generate_media_info_file(self): + def generate_media_info_file(self): # Create cmd arguments for gettig xml file info file cmd_args = [ self.media_script_path, @@ -793,6 +794,8 @@ class MediaInfoFile: ] )) + self._get_time_info_from_origin(matching_clip) + self.clip_data = matching_clip self._write_result_xml_to_file(self.tmp_file, matching_clip) def _clear_tmp_file(self): From cadd5d1e5db0f5cc18ac231cb0287a11ee7b4fe5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:02:42 +0200 Subject: [PATCH 177/337] fix registered host in AE host --- openpype/hosts/aftereffects/api/pipeline.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 94bc369856..4953b19ced 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, + registered_host, ) import openpype.hosts.aftereffects from openpype.lib import register_event_callback @@ -37,7 +38,7 @@ def check_inventory(): if not lib.any_outdated(): return - host = pyblish.api.registered_host() + host = registered_host() outdated_containers = [] for container in host.ls(): representation = container['representation'] @@ -54,12 +55,12 @@ def check_inventory(): # Warn about outdated containers. print("Starting new QApplication..") app = QtWidgets.QApplication(sys.argv) - - message_box = QtWidgets.QMessageBox() - message_box.setIcon(QtWidgets.QMessageBox.Warning) - msg = "There are outdated containers in the scene." - message_box.setText(msg) - message_box.exec_() + if outdated_containers: + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() def application_launch(): From 91c461a1f761fa206906fa3531b74f9afd2762be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:03:27 +0200 Subject: [PATCH 178/337] change registering logs --- openpype/hosts/flame/startup/openpype_in_flame.py | 2 +- openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/startup/openpype_in_flame.py b/openpype/hosts/flame/startup/openpype_in_flame.py index 779143cfb3..f2ac23b19e 100644 --- a/openpype/hosts/flame/startup/openpype_in_flame.py +++ b/openpype/hosts/flame/startup/openpype_in_flame.py @@ -15,7 +15,7 @@ def openpype_install(): """Registering OpenPype in context """ install_host(opfapi) - print("Avalon registered hosts: {}".format(registered_host())) + print("Registered host: {}".format(registered_host())) # Exception handler diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py index aa98563785..de8fc4b3b4 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py @@ -17,7 +17,7 @@ def main(env): # activate resolve from pype install_host(api) - log.info(f"Avalon registered hosts: {registered_host()}") + log.info(f"Registered host: {registered_host()}") menu.launch_openpype_menu() From e3cfc6cc25fc0698a8719d2df38cddb40952e008 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 15:09:06 +0200 Subject: [PATCH 179/337] Added creating subset name for workfile from template --- .../aftereffects/plugins/publish/collect_workfile.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index c1c2be4855..cb5a2bad4f 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -1,6 +1,7 @@ import os from avalon import api import pyblish.api +from openpype.lib import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): @@ -38,7 +39,14 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # workfile instance family = "workfile" - subset = family + task.capitalize() + subset = get_subset_name_with_asset_doc( + family, + "", + context.data["anatomyData"]["task"]["name"], + context.data["assetEntity"], + context.data["anatomyData"]["project"]["name"], + host_name=context.data["hostName"] + ) # Create instance instance = context.create_instance(subset) From 57404e5bcd0bc382993af9554c7be71a430dd174 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Apr 2022 15:22:56 +0200 Subject: [PATCH 180/337] Added creating subset name for workfile from template for Harmony --- .../harmony/plugins/publish/collect_workfile.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/plugins/publish/collect_workfile.py b/openpype/hosts/harmony/plugins/publish/collect_workfile.py index 63bfd5929b..c0493315a4 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_workfile.py +++ b/openpype/hosts/harmony/plugins/publish/collect_workfile.py @@ -3,6 +3,8 @@ import pyblish.api import os +from openpype.lib import get_subset_name_with_asset_doc + class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" @@ -14,10 +16,15 @@ class CollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): """Plugin entry point.""" family = "workfile" - task = os.getenv("AVALON_TASK", None) - sanitized_task_name = task[0].upper() + task[1:] basename = os.path.basename(context.data["currentFile"]) - subset = "{}{}".format(family, sanitized_task_name) + subset = get_subset_name_with_asset_doc( + family, + "", + context.data["anatomyData"]["task"]["name"], + context.data["assetEntity"], + context.data["anatomyData"]["project"]["name"], + host_name=context.data["hostName"] + ) # Create instance instance = context.create_instance(subset) From 8167a65f597b6526e69a66a5591f86c2b5c8aa54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:26:53 +0200 Subject: [PATCH 181/337] added more checks --- .../plugins/publish/integrate_ftrack_note.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index c165e99918..a77b6d6674 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -47,8 +47,9 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): final_intent_label = None if intent_val: final_intent_label = self.get_intent_label(session, intent_val) - if final_intent_label is None: - final_intent_label = intent_label + + if final_intent_label is None: + final_intent_label = intent_label # if intent label is set then format comment # - it is possible that intent_label is equal to "" (empty string) @@ -103,11 +104,18 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): template = self.note_template if template is None: template = self.note_with_intent_template - comment = template.format(**{ + format_data = { "intent": final_intent_label, "comment": comment, "published_paths": "\n".join(sorted(published_paths)) - }) + } + comment = template.format(**format_data) + if not comment: + 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) try: From fe39d0a300fd1ad76390f152cd8502a6f798c244 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:39:50 +0200 Subject: [PATCH 182/337] add host name to possible keys in template --- .../modules/ftrack/plugins/publish/integrate_ftrack_note.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index a77b6d6674..8609e8bca6 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -29,6 +29,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): self.log.info("There are any integrated AssetVersions") return + host_name = instance.context.data["hostName"] comment = (instance.context.data.get("comment") or "").strip() if not comment: self.log.info("Comment is not set.") @@ -107,7 +108,8 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): format_data = { "intent": final_intent_label, "comment": comment, - "published_paths": "\n".join(sorted(published_paths)) + "host_name": host_name, + "published_paths": "\n".join(sorted(published_paths)), } comment = template.format(**format_data) if not comment: From a2a2d5e193798783f1be34d38820c16ece48d404 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:41:30 +0200 Subject: [PATCH 183/337] removed repeated logic of any_outdata from after effects --- openpype/hosts/aftereffects/api/pipeline.py | 25 +++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 4953b19ced..3ed2de0e9d 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -38,29 +38,14 @@ def check_inventory(): if not lib.any_outdated(): return - host = registered_host() - outdated_containers = [] - for container in host.ls(): - representation = container['representation'] - representation_doc = io.find_one( - { - "_id": ObjectId(representation), - "type": "representation" - }, - projection={"parent": True} - ) - if representation_doc and not lib.is_latest(representation_doc): - outdated_containers.append(container) - # Warn about outdated containers. print("Starting new QApplication..") app = QtWidgets.QApplication(sys.argv) - if outdated_containers: - message_box = QtWidgets.QMessageBox() - message_box.setIcon(QtWidgets.QMessageBox.Warning) - msg = "There are outdated containers in the scene." - message_box.setText(msg) - message_box.exec_() + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() def application_launch(): From d79773e801b428ee629521a65374406cda8b8bfc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 15:42:58 +0200 Subject: [PATCH 184/337] flame: fixing feeds to feed tag --- openpype/hosts/flame/api/lib.py | 2 +- openpype/hosts/flame/api/plugin.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 633854168a..508dc0155f 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -805,7 +805,7 @@ class MediaInfoFile: def _get_time_info_from_origin(self, xml_data): try: for out_track in xml_data.iter('track'): - for out_feed in out_track.iter('feeds'): + for out_feed in out_track.iter('feed'): out_feed_nb_ticks_obj = out_feed.find( 'startTimecode/nbTicks') self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 464f5ce89b..bd0f9f1a81 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -679,6 +679,7 @@ class ClipLoader(LoaderPlugin): ] +# TODO: inheritance from flame.api.lib.MediaInfoFile class OpenClipSolver: media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" tmp_name = "_tmp.clip" From 04b8eaf2998eb3269800df0d8fa4985a4a6a8df0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 15:44:30 +0200 Subject: [PATCH 185/337] added new key into settings 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 2b62d67c98..b3c094e398 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -740,7 +740,7 @@ }, { "type": "label", - "label": "Template may contain formatting keys {intent}, {comment} and {published_paths}." + "label": "Template may contain formatting keys {intent}, {comment}, {host_name} and {published_paths}." }, { "type": "text", From 67c759ae2d72558c2caa5cfcc30266a1d16cddb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 15:45:42 +0200 Subject: [PATCH 186/337] flame: add media info file 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 28511458c2..1308b04a7d 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -28,7 +28,8 @@ from .lib import ( get_padding_from_filename, maintained_object_duplication, get_clip_segment, - get_batch_group_from_desktop + get_batch_group_from_desktop, + MediaInfoFile ) from .utils import ( setup, @@ -103,6 +104,7 @@ __all__ = [ "maintained_object_duplication", "get_clip_segment", "get_batch_group_from_desktop", + "MediaInfoFile", # pipeline "install", From 6c9f9c18119cc286078cd109d3253073a3d273aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 16:08:31 +0200 Subject: [PATCH 187/337] flame: add generator into init of class --- openpype/hosts/flame/api/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 508dc0155f..998d7dfa7e 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -748,12 +748,14 @@ class MediaInfoFile: self.log.info("Temp File: {}".format(self.tmp_file)) + self._generate_media_info_file() + def _validate_media_script_path(self): if not os.path.isfile(self.media_script_path): raise IOError("Media Scirpt does not exist: `{}`".format( self.media_script_path)) - def generate_media_info_file(self): + def _generate_media_info_file(self): # Create cmd arguments for gettig xml file info file cmd_args = [ self.media_script_path, From f43e6f0fcdfdb82876c06f6631654a3318edf053 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Apr 2022 16:08:48 +0200 Subject: [PATCH 188/337] flame: get real source_in --- openpype/hosts/flame/otio/flame_export.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 1b5980b40a..3e76968963 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -7,6 +7,7 @@ import json import logging import opentimelineio as otio from . import utils +from openpype.hosts.flame.api import MediaInfoFile import flame from pprint import pformat @@ -345,7 +346,13 @@ def create_otio_clip(clip_data): media_reference = create_otio_reference(clip_data) # calculate source in - first_frame = utils.get_frame_from_filename(clip_data["fpath"]) or 0 + media_info = MediaInfoFile(clip_data["fpath"]) + xml_timecode_ticks = media_info.out_feed_nb_ticks + if xml_timecode_ticks: + first_frame = int(xml_timecode_ticks) + else: + first_frame = utils.get_frame_from_filename(clip_data["fpath"]) or 0 + source_in = int(clip_data["source_in"]) - int(first_frame) # creatae source range From 00406a737610dc4623ff78edda1e7369ddbfc3f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 16:13:50 +0200 Subject: [PATCH 189/337] added more information abou app - name and label --- .../plugins/publish/collect_app_name.py | 13 +++++++++ .../plugins/publish/collect_app_name.py | 13 +++++++++ openpype/plugins/publish/collect_host_name.py | 28 +++++++++++++------ 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_app_name.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_app_name.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_app_name.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_app_name.py new file mode 100644 index 0000000000..857f3dca20 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_app_name.py @@ -0,0 +1,13 @@ +import pyblish.api + + +class CollectSAAppName(pyblish.api.ContextPlugin): + """Collect app name and label.""" + + label = "Collect App Name/Label" + order = pyblish.api.CollectorOrder - 0.5 + hosts = ["standalonepublisher"] + + def process(self, context): + context.data["appName"] = "standalone publisher" + context.data["appLabel"] = "Standalone publisher" diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_app_name.py b/openpype/hosts/traypublisher/plugins/publish/collect_app_name.py new file mode 100644 index 0000000000..e38d10e70f --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_app_name.py @@ -0,0 +1,13 @@ +import pyblish.api + + +class CollectTrayPublisherAppName(pyblish.api.ContextPlugin): + """Collect app name and label.""" + + label = "Collect App Name/Label" + order = pyblish.api.CollectorOrder - 0.5 + hosts = ["traypublisher"] + + def process(self, context): + context.data["appName"] = "tray publisher" + context.data["appLabel"] = "Tray publisher" diff --git a/openpype/plugins/publish/collect_host_name.py b/openpype/plugins/publish/collect_host_name.py index b731e3ed26..d64af4d049 100644 --- a/openpype/plugins/publish/collect_host_name.py +++ b/openpype/plugins/publish/collect_host_name.py @@ -18,20 +18,30 @@ class CollectHostName(pyblish.api.ContextPlugin): def process(self, context): host_name = context.data.get("hostName") + app_name = context.data.get("appName") + app_label = context.data.get("appLabel") # Don't override value if is already set - if host_name: + if host_name and app_name and app_label: return - # Use AVALON_APP as first if available it is the same as host name - # - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and - # set it back to AVALON_APP env variable - host_name = os.environ.get("AVALON_APP") + # Use AVALON_APP to get host name if available if not host_name: + host_name = os.environ.get("AVALON_APP") + + # Use AVALON_APP_NAME to get full app name + if not app_name: app_name = os.environ.get("AVALON_APP_NAME") - if app_name: - app_manager = ApplicationManager() - app = app_manager.applications.get(app_name) - if app: + + # Fill missing values based on app full name + if (not host_name or not app_label) and app_name: + app_manager = ApplicationManager() + app = app_manager.applications.get(app_name) + if app: + if not host_name: host_name = app.host_name + if not app_label: + app_label = app.full_label context.data["hostName"] = host_name + context.data["appName"] = app_name + context.data["appLabel"] = app_label From e68a7cbf7bb9c4362bd5c64ad617ec38db9c633b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 16:15:40 +0200 Subject: [PATCH 190/337] added app name and lable keys into note template --- .../modules/ftrack/plugins/publish/integrate_ftrack_note.py | 4 ++++ .../schemas/projects_schema/schema_project_ftrack.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 8609e8bca6..8220b8e6ca 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -30,6 +30,8 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): return host_name = instance.context.data["hostName"] + app_name = instance.context.data["appName"] + app_label = instance.context.data["appLabel"] comment = (instance.context.data.get("comment") or "").strip() if not comment: self.log.info("Comment is not set.") @@ -109,6 +111,8 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): "intent": final_intent_label, "comment": comment, "host_name": host_name, + "app_name": app_name, + "app_label": app_label, "published_paths": "\n".join(sorted(published_paths)), } comment = template.format(**format_data) 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 b3c094e398..0d7faac2ba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -740,7 +740,7 @@ }, { "type": "label", - "label": "Template may contain formatting keys {intent}, {comment}, {host_name} and {published_paths}." + "label": "Template may contain formatting keys intent, comment, host_name, app_name, app_label and published_paths." }, { "type": "text", From 2a3460e0fa57408356a2af0ebfcba33ad83f4132 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 16:15:55 +0200 Subject: [PATCH 191/337] changed note template input into multiline input --- .../schemas/projects_schema/schema_project_ftrack.json | 3 ++- 1 file changed, 2 insertions(+), 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 0d7faac2ba..0ed2fb3536 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -745,7 +745,8 @@ { "type": "text", "key": "note_template", - "label": "Note template" + "label": "Note template", + "multiline": true }, { "type": "list", From 2e9b7325e47c30a75850c048669c2e04d8bbf3de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 16:25:27 +0200 Subject: [PATCH 192/337] change new line char with br/ html tag --- .../modules/ftrack/plugins/publish/integrate_ftrack_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 8220b8e6ca..3a9f904d00 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -113,7 +113,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): "host_name": host_name, "app_name": app_name, "app_label": app_label, - "published_paths": "\n".join(sorted(published_paths)), + "published_paths": "
".join(sorted(published_paths)), } comment = template.format(**format_data) if not comment: From dff1a51f96a1237071e2fe2ed3cac400c7a3bf6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 16:33:05 +0200 Subject: [PATCH 193/337] removed outdated log --- .../modules/ftrack/plugins/publish/integrate_ftrack_note.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 3a9f904d00..56a7a89e16 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -61,12 +61,6 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): "Intent label is set to `{}`.".format(final_intent_label) ) - elif intent_val: - self.log.debug(( - "Intent is set to `{}` and was not added" - " to comment because label is set to `{}`." - ).format(intent_val, final_intent_label)) - else: self.log.debug("Intent is not set.") From 86647e02310d0913afd7919d364f6c07bf2274e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 17:26:53 +0200 Subject: [PATCH 194/337] added collector for intent label --- .../plugins/publish/collect_intent_label.py | 78 +++++++++++++++++ .../plugins/publish/integrate_ftrack_note.py | 83 +++++-------------- 2 files changed, 101 insertions(+), 60 deletions(-) create mode 100644 openpype/modules/ftrack/plugins/publish/collect_intent_label.py diff --git a/openpype/modules/ftrack/plugins/publish/collect_intent_label.py b/openpype/modules/ftrack/plugins/publish/collect_intent_label.py new file mode 100644 index 0000000000..c23722933c --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/collect_intent_label.py @@ -0,0 +1,78 @@ +""" +Requires: + context -> ftrackSession - connected ftrack.Session + +Provides: + context -> ftrackIntentLabel +""" +import json + +import six +import pyblish.api + + +class CollectFtrackApi(pyblish.api.ContextPlugin): + """ Collects an ftrack session and the current task id. """ + + order = pyblish.api.CollectorOrder + 0.49991 + label = "Collect Ftrack Intent Label" + + def process(self, context): + intent = context.data.get("intent") + if intent and isinstance(intent, dict): + intent_val = intent.get("value") + intent_label = intent.get("label") + else: + intent_val = intent_label = intent + + session = context.data.get("ftrackSession") + if session is None: + context.data["ftrackIntentLabel"] = intent_label + self.log.info("Ftrack session is not available. Skipping.") + return + + final_intent_label = None + if intent_val: + final_intent_label = self.get_intent_label(session, intent_val) + + if final_intent_label is None: + final_intent_label = intent_label + + context.data["ftrackIntentLabel"] = final_intent_label + + def get_intent_label(self, session, intent_value): + if not intent_value: + return + + intent_configurations = session.query( + "CustomAttributeConfiguration where key is intent" + ).all() + if not intent_configurations: + return + + intent_configuration = intent_configurations[0] + if len(intent_configuration) > 1: + self.log.warning(( + "Found more than one `intent` custom attribute." + " Using first found." + )) + + config = intent_configuration.get("config") + if not config: + return + + configuration = json.loads(config) + items = configuration.get("data") + if not items: + return + + if isinstance(items, six.string_types): + items = json.loads(items) + + intent_label = None + for item in items: + if item["value"] == intent_value: + intent_label = item["menu"] + break + + return intent_label diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 56a7a89e16..2fe97dc7ac 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -1,7 +1,18 @@ +""" +Requires: + context > hostName + context > appName + context > appLabel + context > comment + context > ftrackSession + context > ftrackIntentLabel + instance > ftrackIntegratedAssetVersionsData +""" + import sys -import json -import pyblish.api + import six +import pyblish.api class IntegrateFtrackNote(pyblish.api.InstancePlugin): @@ -29,36 +40,25 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): self.log.info("There are any integrated AssetVersions") return - host_name = instance.context.data["hostName"] - app_name = instance.context.data["appName"] - app_label = instance.context.data["appLabel"] - comment = (instance.context.data.get("comment") or "").strip() + context = instance.context + host_name = context.data["hostName"] + app_name = context.data["appName"] + app_label = context.data["appLabel"] + comment = (context.data.get("comment") or "").strip() if not comment: self.log.info("Comment is not set.") else: self.log.debug("Comment is set to `{}`".format(comment)) - session = instance.context.data["ftrackSession"] + session = context.data["ftrackSession"] - intent = instance.context.data.get("intent") - if intent and isinstance(intent, dict): - intent_val = intent.get("value") - intent_label = intent.get("label") - else: - intent_val = intent_label = intent - - final_intent_label = None - if intent_val: - final_intent_label = self.get_intent_label(session, intent_val) - - if final_intent_label is None: - final_intent_label = intent_label + intent_label = context.data["ftrackIntentLabel"] # if intent label is set then format comment # - it is possible that intent_label is equal to "" (empty string) - if final_intent_label: + if intent_label: self.log.debug( - "Intent label is set to `{}`.".format(final_intent_label) + "Intent label is set to `{}`.".format(intent_label) ) else: @@ -102,7 +102,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): if template is None: template = self.note_with_intent_template format_data = { - "intent": final_intent_label, + "intent": intent_label, "comment": comment, "host_name": host_name, "app_name": app_name, @@ -128,40 +128,3 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): session.rollback() session._configure_locations() six.reraise(tp, value, tb) - - def get_intent_label(self, session, intent_value): - if not intent_value: - return - - intent_configurations = session.query( - "CustomAttributeConfiguration where key is intent" - ).all() - if not intent_configurations: - return - - intent_configuration = intent_configurations[0] - if len(intent_configuration) > 1: - self.log.warning(( - "Found more than one `intent` custom attribute." - " Using first found." - )) - - config = intent_configuration.get("config") - if not config: - return - - configuration = json.loads(config) - items = configuration.get("data") - if not items: - return - - if isinstance(items, six.string_types): - items = json.loads(items) - - intent_label = None - for item in items: - if item["value"] == intent_value: - intent_label = item["menu"] - break - - return intent_label From e277cb8ed87be7e7591ae76e048f183c5bf6ce27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 17:28:39 +0200 Subject: [PATCH 195/337] added ftrack integrator adding comment to description --- .../publish/integrate_ftrack_description.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py new file mode 100644 index 0000000000..7e8371cd9d --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py @@ -0,0 +1,76 @@ +""" +Requires: + context > comment + context > ftrackSession + context > ftrackIntentLabel + instance > ftrackIntegratedAssetVersionsData +""" + +import sys + +import six +import pyblish.api + + +class IntegrateFtrackDescription(pyblish.api.InstancePlugin): + """Add description to AssetVersions in Ftrack.""" + + # Must be after integrate asset new + order = pyblish.api.IntegratorOrder + 0.4999 + label = "Integrate Ftrack description" + families = ["ftrack"] + optional = True + + # Can be set in settings: + # - Allows `intent` and `comment` keys + description_template = "{comment}" + + def process(self, instance): + # Check if there are any integrated AssetVersion entities + asset_versions_key = "ftrackIntegratedAssetVersionsData" + asset_versions_data_by_id = instance.data.get(asset_versions_key) + if not asset_versions_data_by_id: + self.log.info("There are any integrated AssetVersions") + return + + comment = (instance.context.data.get("comment") or "").strip() + if not comment: + self.log.info("Comment is not set.") + else: + self.log.debug("Comment is set to `{}`".format(comment)) + + session = instance.context.data["ftrackSession"] + + intent_label = instance.context.data["ftrackIntentLabel"] + + # 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) + ) + + else: + self.log.debug("Intent is not set.") + + 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 + + try: + session.commit() + self.log.debug("Comment added to AssetVersion \"{}\"".format( + str(asset_version) + )) + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) From 49808788f03f07037cb4c21270a1510c235d9ca1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 17:41:54 +0200 Subject: [PATCH 196/337] added settings for integrate ftrack description --- .../defaults/project_settings/ftrack.json | 6 +++ .../schema_project_ftrack.json | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9b350ec88d..31d6a70ac7 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -357,6 +357,12 @@ "note_template": "{intent}: {comment}", "note_labels": [] }, + "IntegrateFtrackDescription": { + "enabled": false, + "optional": true, + "active": true, + "description_template": "{comment}" + }, "ValidateFtrackAttributes": { "enabled": false, "ftrack_custom_attributes": {} 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 0ed2fb3536..5ce9b24b4b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -756,6 +756,44 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "IntegrateFtrackDescription", + "label": "Integrate Ftrack Description", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Add description to integrated AssetVersion." + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "label", + "label": "Template may contain formatting keys intent and comment." + }, + { + "type": "text", + "key": "description_template", + "label": "Description template" + } + ] + }, { "type": "dict", "collapsible": true, From 68957cc0d9e545be7328dc484aaa22b4039b10b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 18:14:34 +0200 Subject: [PATCH 197/337] changed name of publish plugin --- openpype/modules/ftrack/plugins/publish/collect_intent_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_intent_label.py b/openpype/modules/ftrack/plugins/publish/collect_intent_label.py index c23722933c..8375fba15e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_intent_label.py +++ b/openpype/modules/ftrack/plugins/publish/collect_intent_label.py @@ -11,7 +11,7 @@ import six import pyblish.api -class CollectFtrackApi(pyblish.api.ContextPlugin): +class CollectFtrackIntentLabel(pyblish.api.ContextPlugin): """ Collects an ftrack session and the current task id. """ order = pyblish.api.CollectorOrder + 0.49991 From acba6e8bb08913906209946970f86359a37ca1e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Apr 2022 18:52:44 +0200 Subject: [PATCH 198/337] fix import of get_representation_context --- openpype/hosts/tvpaint/plugins/load/load_reference_image.py | 2 +- openpype/pipeline/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 5e4e3965d2..af1a4a9b6b 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,6 +1,6 @@ import collections import qargparse -from avalon.pipeline import get_representation_context +from openpype.pipeline import get_representation_context from openpype.hosts.tvpaint.api import lib, pipeline, plugin diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 8460d20ef1..883713b078 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -41,6 +41,7 @@ from .load import ( loaders_from_representation, get_representation_path, + get_representation_context, get_repres_contexts, ) @@ -113,6 +114,7 @@ __all__ = ( "loaders_from_representation", "get_representation_path", + "get_representation_context", "get_repres_contexts", # --- Publish --- From f9df89dc0d57fed7f1fccc3a8f43fa194ef8b2d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Apr 2022 04:49:26 +0200 Subject: [PATCH 199/337] Fix Validate Asset Docs filename and class name --- .../publish/{validate_aseset_docs.py => validate_asset_docs.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename openpype/plugins/publish/{validate_aseset_docs.py => validate_asset_docs.py} (94%) diff --git a/openpype/plugins/publish/validate_aseset_docs.py b/openpype/plugins/publish/validate_asset_docs.py similarity index 94% rename from openpype/plugins/publish/validate_aseset_docs.py rename to openpype/plugins/publish/validate_asset_docs.py index eed75cdf8a..ddd579621c 100644 --- a/openpype/plugins/publish/validate_aseset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -2,7 +2,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError -class ValidateContainers(pyblish.api.InstancePlugin): +class ValidateAssetDocs(pyblish.api.InstancePlugin): """Validate existence of asset asset documents on instances. Without asset document it is not possible to publish the instance. From ae57c8619d863589ae1fc365b86adbab0af890bf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Apr 2022 04:50:32 +0200 Subject: [PATCH 200/337] Fix grammar + typos --- openpype/plugins/publish/validate_asset_docs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index ddd579621c..bc1f9b9e6c 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -3,7 +3,7 @@ from openpype.pipeline import PublishValidationError class ValidateAssetDocs(pyblish.api.InstancePlugin): - """Validate existence of asset asset documents on instances. + """Validate existence of asset documents on instances. Without asset document it is not possible to publish the instance. @@ -22,10 +22,10 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): return if instance.data.get("assetEntity"): - self.log.info("Instance have set asset document in it's data.") + self.log.info("Instance has set asset document in its data.") else: raise PublishValidationError(( - "Instance \"{}\" don't have set asset" - " document which is needed for publishing." + "Instance \"{}\" doesn't have asset document " + "set which is needed for publishing." ).format(instance.data["name"])) From 31683fb432b885add58e90883f23a4b82a989428 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Apr 2022 05:17:59 +0200 Subject: [PATCH 201/337] Allow to select invalid camera contents if no cameras found + improve error logging --- .../maya/plugins/publish/validate_camera_contents.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py index d9e88edaac..20af8d2315 100644 --- a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py @@ -40,7 +40,14 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): # list when there are no actual cameras results in # still an empty 'invalid' list if len(cameras) < 1: - raise RuntimeError("No cameras in instance.") + if members: + # If there are members in the instance return all of + # them as 'invalid' so the user can still select invalid + cls.log.error("No cameras found in instance " + "members: {}".format(members)) + return members + + raise RuntimeError("No cameras found in empty instance.") # non-camera shapes valid_shapes = cmds.ls(shapes, type=('camera', 'locator'), long=True) From 8fde20646bc440e40d949d36bce941e80392f885 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 7 Apr 2022 10:25:40 +0200 Subject: [PATCH 202/337] Hound --- openpype/modules/sync_server/sync_server_module.py | 4 ++-- .../openpype/modules/sync_server/test_module_api.py | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index d2f341786c..0a70830255 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -265,7 +265,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): alt_site_pairs = self._get_alt_site_pairs(additional_sites) - for site_name, site_info in additional_sites.items(): + for site_name in additional_sites.keys(): # Get alternate sites (stripped names) for this site name alt_sites = alt_site_pairs.get(site_name) alt_sites = [site.strip() for site in alt_sites] @@ -321,7 +321,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): for site, alt_sites in alt_site_pairs.items(): for alt_site in alt_sites: for alt_alt_site in alt_site_pairs.get(alt_site, []): - if ( alt_alt_site != site + if (alt_alt_site != site and alt_alt_site not in alt_sites): alt_site_pairs[site].append(alt_alt_site) loop = True diff --git a/tests/unit/openpype/modules/sync_server/test_module_api.py b/tests/unit/openpype/modules/sync_server/test_module_api.py index 377045e229..b6ba2a01b6 100644 --- a/tests/unit/openpype/modules/sync_server/test_module_api.py +++ b/tests/unit/openpype/modules/sync_server/test_module_api.py @@ -13,19 +13,6 @@ """ import pytest -import sys, os - -os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" -os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" -os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["AVALON_TIMEOUT"] = '3000' -os.environ["OPENPYPE_DEBUG"] = "3" - -os.environ["AVALON_PROJECT"] = "petr_test" -os.environ["AVALON_DB"] = "avalon" -os.environ["QT_PREFERRED_BINDING"] = "PySide2" -os.environ["QT_VERBOSE"] = "true" - from tests.lib.testing_classes import ModuleUnitTest From c298e06ba621162e204299ff849786614d0c02be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 11:11:46 +0200 Subject: [PATCH 203/337] use 'filepath_from_context' instead of 'get_representation_path_from_context' --- openpype/hosts/maya/plugins/inventory/import_modelrender.py | 5 ++--- .../hosts/photoshop/plugins/load/load_image_from_sequence.py | 3 +-- openpype/pipeline/load/plugins.py | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index d9bb256fac..c2e43f196f 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -4,7 +4,6 @@ from bson.objectid import ObjectId from openpype.pipeline import ( InventoryAction, get_representation_context, - get_representation_path_from_context, ) from openpype.hosts.maya.api.lib import ( maintained_selection, @@ -80,10 +79,10 @@ class ImportModelRender(InventoryAction): }) context = get_representation_context(look_repr["_id"]) - maya_file = get_representation_path_from_context(context) + maya_file = self.filepath_from_context(context) context = get_representation_context(json_repr["_id"]) - json_file = get_representation_path_from_context(context) + json_file = self.filepath_from_context(context) # Import the look file with maintained_selection(): diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 5f39121ae1..c25c5a8f2c 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -2,7 +2,6 @@ import os import qargparse -from openpype.pipeline import get_representation_path_from_context from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.api import get_unique_layer_name @@ -63,7 +62,7 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): """ files = [] for context in repre_contexts: - fname = get_representation_path_from_context(context) + fname = cls.filepath_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index d60aed0083..a30a2188a4 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -41,7 +41,8 @@ class LoaderPlugin(list): def get_representations(cls): return cls.representations - def filepath_from_context(self, context): + @classmethod + def filepath_from_context(cls, context): return get_representation_path_from_context(context) def load(self, context, name=None, namespace=None, options=None): From cb3722552c4e2875365f237c36aee702af3bf39f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 11:57:33 +0200 Subject: [PATCH 204/337] removed ftrackIntentLabel --- .../plugins/publish/collect_intent_label.py | 78 ------------------- .../publish/integrate_ftrack_description.py | 12 ++- .../plugins/publish/integrate_ftrack_note.py | 12 ++- 3 files changed, 20 insertions(+), 82 deletions(-) delete mode 100644 openpype/modules/ftrack/plugins/publish/collect_intent_label.py diff --git a/openpype/modules/ftrack/plugins/publish/collect_intent_label.py b/openpype/modules/ftrack/plugins/publish/collect_intent_label.py deleted file mode 100644 index 8375fba15e..0000000000 --- a/openpype/modules/ftrack/plugins/publish/collect_intent_label.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Requires: - context -> ftrackSession - connected ftrack.Session - -Provides: - context -> ftrackIntentLabel -""" -import json - -import six -import pyblish.api - - -class CollectFtrackIntentLabel(pyblish.api.ContextPlugin): - """ Collects an ftrack session and the current task id. """ - - order = pyblish.api.CollectorOrder + 0.49991 - label = "Collect Ftrack Intent Label" - - def process(self, context): - intent = context.data.get("intent") - if intent and isinstance(intent, dict): - intent_val = intent.get("value") - intent_label = intent.get("label") - else: - intent_val = intent_label = intent - - session = context.data.get("ftrackSession") - if session is None: - context.data["ftrackIntentLabel"] = intent_label - self.log.info("Ftrack session is not available. Skipping.") - return - - final_intent_label = None - if intent_val: - final_intent_label = self.get_intent_label(session, intent_val) - - if final_intent_label is None: - final_intent_label = intent_label - - context.data["ftrackIntentLabel"] = final_intent_label - - def get_intent_label(self, session, intent_value): - if not intent_value: - return - - intent_configurations = session.query( - "CustomAttributeConfiguration where key is intent" - ).all() - if not intent_configurations: - return - - intent_configuration = intent_configurations[0] - if len(intent_configuration) > 1: - self.log.warning(( - "Found more than one `intent` custom attribute." - " Using first found." - )) - - config = intent_configuration.get("config") - if not config: - return - - configuration = json.loads(config) - items = configuration.get("data") - if not items: - return - - if isinstance(items, six.string_types): - items = json.loads(items) - - intent_label = None - for item in items: - if item["value"] == intent_value: - intent_label = item["menu"] - break - - return intent_label diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py index 7e8371cd9d..c6a3d47f66 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py @@ -2,7 +2,6 @@ Requires: context > comment context > ftrackSession - context > ftrackIntentLabel instance > ftrackIntegratedAssetVersionsData """ @@ -41,7 +40,16 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin): session = instance.context.data["ftrackSession"] - intent_label = instance.context.data["ftrackIntentLabel"] + 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 label is set then format comment # - it is possible that intent_label is equal to "" (empty string) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 2fe97dc7ac..952b21546d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -5,7 +5,6 @@ Requires: context > appLabel context > comment context > ftrackSession - context > ftrackIntentLabel instance > ftrackIntegratedAssetVersionsData """ @@ -52,7 +51,16 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): session = context.data["ftrackSession"] - intent_label = context.data["ftrackIntentLabel"] + 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 label is set then format comment # - it is possible that intent_label is equal to "" (empty string) From c2788070a370a39536e134acc13a5f5243b2530f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 12:22:34 +0200 Subject: [PATCH 205/337] flame: refactoring batch group creation --- openpype/hosts/flame/api/__init__.py | 6 +- openpype/hosts/flame/api/batch_utils.py | 146 +++++++++++++----- .../plugins/publish/integrate_batch_group.py | 98 ++++++++---- 3 files changed, 182 insertions(+), 68 deletions(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 1308b04a7d..a0c40904ed 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -72,7 +72,8 @@ from .render_utils import ( modify_preset_file ) from .batch_utils import ( - create_batch + create_batch_group, + create_batch_group_conent ) __all__ = [ @@ -148,5 +149,6 @@ __all__ = [ "modify_preset_file", # batch utils - "create_batch" + "create_batch_group", + "create_batch_group_conent" ] diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 43742c6e4f..26f324090b 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -1,66 +1,134 @@ import flame -def create_batch(name, frame_start, frame_duration, **kwargs): +def create_batch_group( + name, + frame_start, + frame_duration, + update_batch_group=None, + **kwargs +): """Create Batch Group in active project's Desktop Args: name (str): name of batch group to be created frame_start (int): start frame of batch frame_end (int): end frame of batch + update_batch_group (PyBatch)[optional]: batch group to update + + Return: + PyBatch: active flame batch group """ + # make sure some batch obj is present + batch_group = update_batch_group or flame.batch + schematic_reels = kwargs.get("shematic_reels") or ['LoadedReel1'] shelf_reels = kwargs.get("shelf_reels") or ['ShelfReel1'] - write_pref = kwargs["write_pref"] handle_start = kwargs.get("handleStart") or 0 handle_end = kwargs.get("handleEnd") or 0 frame_start -= handle_start frame_duration += handle_start + handle_end - # Create batch group with name, start_frame value, duration value, - # set of schematic reel names, set of shelf reel names - bgroup = flame.batch.create_batch_group( - name, - start_frame=frame_start, - duration=frame_duration, - reels=schematic_reels, - shelf_reels=shelf_reels - ) + if not update_batch_group: + # Create batch group with name, start_frame value, duration value, + # set of schematic reel names, set of shelf reel names + batch_group = batch_group.create_batch_group( + name, + start_frame=frame_start, + duration=frame_duration, + reels=schematic_reels, + shelf_reels=shelf_reels + ) + else: + batch_group.name = name + batch_group.start_frame = frame_start + batch_group.duration = frame_duration + + # add reels to batch group + _add_reels_to_batch_group( + batch_group, schematic_reels, shelf_reels) + + # TODO: also update write node if there is any + # TODO: also update loaders to start from correct frameStart if kwargs.get("switch_batch_tab"): # use this command to switch to the batch tab - flame.batch.go_to() + batch_group.go_to() - comp_node = flame.batch.create_node("Comp") + return batch_group - # TODO: convert this to iterational processing, - # so it could be driven from `imageio` settigns - # create write node - write_node = flame.batch.create_node('Write File') - # assign attrs - write_node.name = write_pref["name"] - write_node.media_path = write_pref["media_path"] - write_node.media_path_pattern = write_pref["media_path_pattern"] - write_node.create_clip = write_pref["create_clip"] - write_node.include_setup = write_pref["include_setup"] - write_node.create_clip_path = write_pref["create_clip_path"] - write_node.include_setup_path = write_pref["include_setup_path"] - write_node.file_type = write_pref["file_type"] - write_node.format_extension = write_pref["format_extension"] - write_node.bit_depth = write_pref["bit_depth"] - write_node.compress = write_pref["compress"] - write_node.compress_mode = write_pref["compress_mode"] - write_node.frame_index_mode = write_pref["frame_index_mode"] - write_node.frame_padding = write_pref["frame_padding"] - write_node.version_mode = write_pref["version_mode"] - write_node.version_name = write_pref["version_name"] - write_node.version_padding = write_pref["version_padding"] - flame.batch.connect_nodes(comp_node, "Result", write_node, "Front") +def _add_reels_to_batch_group(batch_group, reels, shelf_reels): + # update or create defined reels + # helper variables + reel_names = [ + r.name.get_value() + for r in batch_group.reels + ] + shelf_reel_names = [ + r.name.get_value() + for r in batch_group.shelf_reels + ] + # add schematic reels + for _r in reels: + if _r in reel_names: + continue + batch_group.create_reel(_r) + + # add shelf reels + for _sr in shelf_reels: + if _sr in shelf_reel_names: + continue + batch_group.create_shelf_reel(_sr) + + +def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): + """Creating batch group with links + + Args: + batch_nodes (list of dict): each dict is node definition + batch_links (list of dict): each dict is link definition + batch_group (PyBatch, optional): batch group. Defaults to None. + """ + # make sure some batch obj is present + batch_group = batch_group or flame.batch + + created_nodes = {} + for node in batch_nodes: + # NOTE: node_props needs to be ideally OrederDict type + node_id, node_type, node_props = ( + node["id"], node["type"], node["properties"]) + + # create batch node + batch_node = batch_group.create_node(node_type) + + # set attributes found in node props + for key, value in node_props.items(): + if not hasattr(batch_node, key): + continue + setattr(batch_node, key, value) + + # add created node for possible linking + created_nodes[node_id] = batch_node + + # link nodes to each other + for link in batch_links: + _from_n, _to_n = link["from_node"], link["to_node"] + + # check if all linking nodes are available + if not all([ + created_nodes.get(_from_n["id"]), + created_nodes.get(_to_n["id"]) + ]): + continue + + # link nodes in defined link + batch_group.connect_nodes( + created_nodes[_from_n["id"]], _from_n["connector"], + created_nodes[_to_n["id"]], _to_n["connector"] + ) # sort batch nodes - flame.batch.organize() - - return bgroup + batch_group.organize() diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 979134bbfe..524d9b1ac2 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -1,5 +1,6 @@ import os import copy +from collections import OrderedDict from pprint import pformat import pyblish from openpype.lib import get_workdir @@ -30,12 +31,48 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # create or get already created batch group bgroup = self._get_batch_group(instance, task_data) + # add batch group content + self._add_nodes_to_batch_with_links(instance, task_data, bgroup) + # load plate to batch group self.log.info("Loading subset `{}` into batch `{}`".format( instance.data["subset"], bgroup.name.get_value() )) self._load_clip_to_context(instance, bgroup) + def _add_nodes_to_batch_with_links(self, instance, task_data, batch_group): + # get write file node properties > OrederDict because order does mater + write_pref_data = self._get_write_prefs(instance, task_data) + + batch_nodes = [ + { + "type": "comp", + "properties": {}, + "id": "comp_node01" + }, + { + "type": "Write File", + "properties": write_pref_data, + "id": "write_file_node01" + } + ] + batch_links = [ + { + "from_node": { + "id": "comp_node01", + "connector": "Result" + }, + "to_node": { + "id": "write_file_node01", + "connector": "Front" + } + } + ] + + # add nodes into batch group + opfapi.create_batch_group_conent( + batch_nodes, batch_links, batch_group) + def _load_clip_to_context(self, instance, bgroup): # get all loaders for host loaders_by_name = { @@ -123,13 +160,11 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): task_name = task_data["name"] batchgroup_name = "{}_{}".format(asset_name, task_name) - write_pref_data = self._get_write_prefs(instance, task_data) batch_data = { "shematic_reels": [ "OP_LoadedReel" ], - "write_pref": write_pref_data, "handleStart": handle_start, "handleEnd": handle_end } @@ -143,21 +178,24 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): self.log.info( "Creating new batch group: {}".format(batchgroup_name)) # create batch with utils - bgroup = opfapi.create_batch( + bgroup = opfapi.create_batch_group( batchgroup_name, frame_start, frame_duration, **batch_data ) + else: self.log.info( "Updating batch group: {}".format(batchgroup_name)) # update already created batch group - bgroup.name = batchgroup_name - bgroup.start_frame = frame_start - bgroup.duration = frame_duration - # TODO: also update write node if there is any - # TODO: also update loaders to start from correct frameStart + bgroup = opfapi.create_batch_group( + batchgroup_name, + frame_start, + frame_duration, + update_batch_group=bgroup, + **batch_data + ) return bgroup @@ -249,25 +287,31 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): version_name = "v" version_padding = 3 - return { - "name": name, - "media_path": media_path, - "media_path_pattern": media_path_pattern, - "create_clip": create_clip, - "include_setup": include_setup, - "create_clip_path": create_clip_path, - "include_setup_path": include_setup_path, - "file_type": file_type, - "format_extension": format_extension, - "bit_depth": bit_depth, - "compress": compress, - "compress_mode": compress_mode, - "frame_index_mode": frame_index_mode, - "frame_padding": frame_padding, - "version_mode": version_mode, - "version_name": version_name, - "version_padding": version_padding - } + # return it as ordered dict + reutrn_dict = OrderedDict() + # need to make sure the order of keys is correct + for item in ( + ("name", name), + ("media_path", media_path), + ("media_path_pattern", media_path_pattern), + ("create_clip", create_clip), + ("include_setup", include_setup), + ("create_clip_path", create_clip_path), + ("include_setup_path", include_setup_path), + ("file_type", file_type), + ("format_extension", format_extension), + ("bit_depth", bit_depth), + ("compress", compress), + ("compress_mode", compress_mode), + ("frame_index_mode", frame_index_mode), + ("frame_padding", frame_padding), + ("version_mode", version_mode), + ("version_name", version_name), + ("version_padding", version_padding) + ): + reutrn_dict.update({item[0]: item[1]}) + + return reutrn_dict def _get_shot_task_dir_path(self, instance, task_data): project_doc = instance.data["projectEntity"] From 1c6164005c89e8cf26a15e642c991a40a1261e9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 13:24:43 +0200 Subject: [PATCH 206/337] wrapper around settings content is splitter so projects view can be resized --- openpype/tools/settings/settings/categories.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index a5b5cd40f0..c8ade5fcdb 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -216,7 +216,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def create_ui(self): self.modify_defaults_checkbox = None - conf_wrapper_widget = QtWidgets.QWidget(self) + conf_wrapper_widget = QtWidgets.QSplitter(self) configurations_widget = QtWidgets.QWidget(conf_wrapper_widget) # Breadcrumbs/Path widget @@ -294,10 +294,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): configurations_layout.addWidget(scroll_widget, 1) - conf_wrapper_layout = QtWidgets.QHBoxLayout(conf_wrapper_widget) - conf_wrapper_layout.setContentsMargins(0, 0, 0, 0) - conf_wrapper_layout.setSpacing(0) - conf_wrapper_layout.addWidget(configurations_widget, 1) + conf_wrapper_widget.addWidget(configurations_widget) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -327,7 +324,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.breadcrumbs_model = None self.refresh_btn = refresh_btn - self.conf_wrapper_layout = conf_wrapper_layout + self.conf_wrapper_widget = conf_wrapper_widget self.main_layout = main_layout self.ui_tweaks() @@ -818,7 +815,9 @@ class ProjectWidget(SettingsCategoryWidget): project_list_widget = ProjectListWidget(self) - self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0) + self.conf_wrapper_widget.insertWidget(0, project_list_widget) + self.conf_wrapper_widget.setStretchFactor(0, 0) + self.conf_wrapper_widget.setStretchFactor(1, 1) project_list_widget.project_changed.connect(self._on_project_change) project_list_widget.version_change_requested.connect( From c966b96e059e4344027542f5db2d42cc80a39a3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 13:29:12 +0200 Subject: [PATCH 207/337] fixed default value of use sequence for review --- .../modules/deadline/plugins/publish/submit_publish_job.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 921b172f2b..3c4e0d2913 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -531,10 +531,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # expected files contains more explicitly and from what # should be review made. # - "review" tag is never added when is set to 'False' - use_sequence_for_review = instance.get( - "useSequenceForReview", True - ) - if use_sequence_for_review: + if instance["useSequenceForReview"]: # if filtered aov name is found in filename, toggle it for # preview video rendering for app in self.aov_filter.keys(): @@ -737,7 +734,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionHeight": data.get("resolutionHeight", 1080), "multipartExr": data.get("multipartExr", False), "jobBatchName": data.get("jobBatchName", ""), - "useSequenceForReview": data.get("useSequenceForReview") + "useSequenceForReview": data.get("useSequenceForReview", True) } if "prerender" in instance.data["families"]: From c2a41760676afceb065351520db9cfeec62b9d33 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 13:30:43 +0200 Subject: [PATCH 208/337] flame: fix integrating batch group to loader with bgroup --- openpype/hosts/flame/plugins/load/load_clip_batch.py | 2 +- .../hosts/flame/plugins/publish/integrate_batch_group.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 3c13d88d3a..5de3226035 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -26,7 +26,7 @@ class LoadClipBatch(opfapi.ClipLoader): def load(self, context, name, namespace, options): # get flame objects - self.batch = flame.batch + self.batch = options.get("batch") or flame.batch # load clip to timeline and get main variables namespace = namespace diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 524d9b1ac2..c70c2baa4a 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -130,7 +130,10 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): try: op_pipeline.load.load_with_repre_context( loader_plugin, repre_context, **{ - "data": {"workdir": self.task_workdir} + "data": { + "workdir": self.task_workdir, + "batch": bgroup + } }) except op_pipeline.load.IncompatibleLoaderError as msg: self.log.error( From 53fba93840cc8af47dad7d274880de83146b558f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 7 Apr 2022 12:14:49 +0000 Subject: [PATCH 209/337] [Automated] Bump version --- CHANGELOG.md | 51 +++++++++++++++++---------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a53311d70..e17ab74293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.9.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...HEAD) @@ -10,16 +10,28 @@ **🆕 New features** +- Ftrack: Add description integrator [\#3027](https://github.com/pypeclub/OpenPype/pull/3027) - Publishing textures for Unreal [\#2988](https://github.com/pypeclub/OpenPype/pull/2988) -- Maya to Unreal \> Static and Skeletal Meshes [\#2978](https://github.com/pypeclub/OpenPype/pull/2978) +- Maya to Unreal: Static and Skeletal Meshes [\#2978](https://github.com/pypeclub/OpenPype/pull/2978) **🚀 Enhancements** +- Ftrack: Add more options for note text of integrate ftrack note [\#3025](https://github.com/pypeclub/OpenPype/pull/3025) +- Console Interpreter: Changed how console splitter size are reused on show [\#3016](https://github.com/pypeclub/OpenPype/pull/3016) +- Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) - Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) +- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) **🐛 Bug fixes** +- Deadline: Fixed default value of use sequence for review [\#3033](https://github.com/pypeclub/OpenPype/pull/3033) +- Settings UI: Version column can be extended so version are visible [\#3032](https://github.com/pypeclub/OpenPype/pull/3032) +- General: Fix import after movements [\#3028](https://github.com/pypeclub/OpenPype/pull/3028) +- Harmony: Added creating subset name for workfile from template [\#3024](https://github.com/pypeclub/OpenPype/pull/3024) +- AfterEffects: Added creating subset name for workfile from template [\#3023](https://github.com/pypeclub/OpenPype/pull/3023) +- General: Add example addons to ignored [\#3022](https://github.com/pypeclub/OpenPype/pull/3022) +- Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) - Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) - Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) - Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) @@ -27,6 +39,7 @@ **Merged pull requests:** +- Maya: Allow to select invalid camera contents if no cameras found [\#3030](https://github.com/pypeclub/OpenPype/pull/3030) - General: adding limitations for pyright [\#2994](https://github.com/pypeclub/OpenPype/pull/2994) ## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04) @@ -58,7 +71,8 @@ - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) +- Ftrack: Fill workfile in custom attribute [\#2906](https://github.com/pypeclub/OpenPype/pull/2906) +- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) **🐛 Bug fixes** @@ -92,7 +106,6 @@ - General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) - General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) - General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918) -- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914) - General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912) **Merged pull requests:** @@ -108,11 +121,9 @@ **🚀 Enhancements** -- General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) -- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) +- Settings UI: Add simple tooltips for settings entities [\#2901](https://github.com/pypeclub/OpenPype/pull/2901) - nuke: imageio adding ocio config version 1.2 [\#2897](https://github.com/pypeclub/OpenPype/pull/2897) -- Nuke: ExtractReviewSlate can handle more codes and profiles [\#2879](https://github.com/pypeclub/OpenPype/pull/2879) -- Flame: sequence used for reference video [\#2869](https://github.com/pypeclub/OpenPype/pull/2869) +- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) **🐛 Bug fixes** @@ -121,39 +132,15 @@ - Pyblish Pype - ensure current state is correct when entering new group order [\#2899](https://github.com/pypeclub/OpenPype/pull/2899) - SceneInventory: Fix import of load function [\#2894](https://github.com/pypeclub/OpenPype/pull/2894) - Harmony - fixed creator issue [\#2891](https://github.com/pypeclub/OpenPype/pull/2891) -- General: Remove forgotten use of avalon Creator [\#2885](https://github.com/pypeclub/OpenPype/pull/2885) -- General: Avoid circular import [\#2884](https://github.com/pypeclub/OpenPype/pull/2884) -- Fixes for attaching loaded containers \(\#2837\) [\#2874](https://github.com/pypeclub/OpenPype/pull/2874) **🔀 Refactored code** - General: Reduce style usage to OpenPype repository [\#2889](https://github.com/pypeclub/OpenPype/pull/2889) -- General: Move loader logic from avalon to openpype [\#2886](https://github.com/pypeclub/OpenPype/pull/2886) ## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.0-nightly.9...3.9.0) -### 📖 Documentation - -- Documentation: Change Photoshop & AfterEffects plugin path [\#2878](https://github.com/pypeclub/OpenPype/pull/2878) - -**🚀 Enhancements** - -- General: Subset name filtering in ExtractReview outpus [\#2872](https://github.com/pypeclub/OpenPype/pull/2872) -- NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867) - -**🐛 Bug fixes** - -- General: Missing time function [\#2877](https://github.com/pypeclub/OpenPype/pull/2877) -- Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) -- Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) -- General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) - -**🔀 Refactored code** - -- Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876) - ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.2-nightly.3...3.8.2) diff --git a/openpype/version.py b/openpype/version.py index c314151e9b..1dbbab64de 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.3-nightly.1" +__version__ = "3.9.3-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index dd1a666dea..aa00f4022f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.3-nightly.1" # OpenPype +version = "3.9.3-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 616d0cf6208ce1be4134b63a9a6722b30bca8252 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 7 Apr 2022 12:23:14 +0000 Subject: [PATCH 210/337] [Automated] Release --- CHANGELOG.md | 9 ++++----- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e17ab74293..f1e7d5d9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.9.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...3.9.3) ### 📖 Documentation @@ -20,6 +20,7 @@ - Console Interpreter: Changed how console splitter size are reused on show [\#3016](https://github.com/pypeclub/OpenPype/pull/3016) - Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) - Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) +- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) @@ -59,7 +60,6 @@ **🚀 Enhancements** -- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) - Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) - General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) @@ -71,7 +71,6 @@ - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- Ftrack: Fill workfile in custom attribute [\#2906](https://github.com/pypeclub/OpenPype/pull/2906) - Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) **🐛 Bug fixes** @@ -121,7 +120,7 @@ **🚀 Enhancements** -- Settings UI: Add simple tooltips for settings entities [\#2901](https://github.com/pypeclub/OpenPype/pull/2901) +- General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) - nuke: imageio adding ocio config version 1.2 [\#2897](https://github.com/pypeclub/OpenPype/pull/2897) - Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) diff --git a/openpype/version.py b/openpype/version.py index 1dbbab64de..97aa585ca7 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.3-nightly.2" +__version__ = "3.9.3" diff --git a/pyproject.toml b/pyproject.toml index aa00f4022f..006f6eb4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.3-nightly.2" # OpenPype +version = "3.9.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 614c49b57f4acd32d9d6db9cf97f6e0c628ce33d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 14:27:00 +0200 Subject: [PATCH 211/337] flame: fix wiretap with umask --- openpype/hosts/flame/api/scripts/wiretap_com.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index 14fbcec954..d904e7850a 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -185,7 +185,9 @@ class WireTapCom(object): exit_code = subprocess.call( project_create_cmd, - cwd=os.path.expanduser('~')) + cwd=os.path.expanduser('~'), + preexec_fn=_subprocess_preexec_fn + ) if exit_code != 0: RuntimeError("Cannot create project in flame db") @@ -448,7 +450,9 @@ class WireTapCom(object): exit_code = subprocess.call( project_colorspace_cmd, - cwd=os.path.expanduser('~')) + cwd=os.path.expanduser('~'), + preexec_fn=_subprocess_preexec_fn + ) if exit_code != 0: RuntimeError("Cannot set colorspace {} on project {}".format( @@ -456,6 +460,11 @@ class WireTapCom(object): )) +def _subprocess_preexec_fn(): + os.setpgrp() + os.umask(0o022) + + if __name__ == "__main__": # get json exchange data json_path = sys.argv[-1] From ce4aa40f217857ae527800667d243e048ff40159 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 14:52:13 +0200 Subject: [PATCH 212/337] flame: fixing umask to 0o000 to reflect permissions to 0777 --- openpype/hosts/flame/api/scripts/wiretap_com.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index d904e7850a..f78102c0a1 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -462,7 +462,7 @@ class WireTapCom(object): def _subprocess_preexec_fn(): os.setpgrp() - os.umask(0o022) + os.umask(0o000) if __name__ == "__main__": From cd59b3af66e6bd39e20cce1ec2d1195a26225e9e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 14:53:29 +0200 Subject: [PATCH 213/337] flame: make sure nodes are not duplicated and update --- openpype/hosts/flame/api/batch_utils.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 26f324090b..23d16f8d1a 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -94,15 +94,25 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): """ # make sure some batch obj is present batch_group = batch_group or flame.batch - + all_batch_nodes = { + b.name.get_value(): b + for b in batch_group.nodes + } created_nodes = {} for node in batch_nodes: # NOTE: node_props needs to be ideally OrederDict type node_id, node_type, node_props = ( node["id"], node["type"], node["properties"]) - # create batch node - batch_node = batch_group.create_node(node_type) + # get node name for checking if exists + node_name = node_props.get("name") or node_id + + if all_batch_nodes.get(node_name): + # update existing batch node + batch_node = all_batch_nodes[node_name] + else: + # create new batch node + batch_node = batch_group.create_node(node_type) # set attributes found in node props for key, value in node_props.items(): From 4ed6c0257ece24cc254afb4edc9a4cc698c4dd53 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:05:46 +0200 Subject: [PATCH 214/337] flame: returning all batch nodes --- openpype/hosts/flame/api/batch_utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 23d16f8d1a..20118c249c 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -91,6 +91,9 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): batch_nodes (list of dict): each dict is node definition batch_links (list of dict): each dict is link definition batch_group (PyBatch, optional): batch group. Defaults to None. + + Return: + dict: all batch nodes {name or id: PyNode} """ # make sure some batch obj is present batch_group = batch_group or flame.batch @@ -98,7 +101,6 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): b.name.get_value(): b for b in batch_group.nodes } - created_nodes = {} for node in batch_nodes: # NOTE: node_props needs to be ideally OrederDict type node_id, node_type, node_props = ( @@ -121,7 +123,7 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): setattr(batch_node, key, value) # add created node for possible linking - created_nodes[node_id] = batch_node + all_batch_nodes[node_id] = batch_node # link nodes to each other for link in batch_links: @@ -129,16 +131,18 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): # check if all linking nodes are available if not all([ - created_nodes.get(_from_n["id"]), - created_nodes.get(_to_n["id"]) + all_batch_nodes.get(_from_n["id"]), + all_batch_nodes.get(_to_n["id"]) ]): continue # link nodes in defined link batch_group.connect_nodes( - created_nodes[_from_n["id"]], _from_n["connector"], - created_nodes[_to_n["id"]], _to_n["connector"] + all_batch_nodes[_from_n["id"]], _from_n["connector"], + all_batch_nodes[_to_n["id"]], _to_n["connector"] ) # sort batch nodes batch_group.organize() + + return all_batch_nodes From 4da106cf3def8ee5d2dfb91076f0fecb7f05bc0e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:06:14 +0200 Subject: [PATCH 215/337] flame: debug log nodes attrs --- .../flame/plugins/publish/integrate_batch_group.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index c70c2baa4a..350acdfa90 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -32,7 +32,16 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): bgroup = self._get_batch_group(instance, task_data) # add batch group content - self._add_nodes_to_batch_with_links(instance, task_data, bgroup) + all_batch_nodes = self._add_nodes_to_batch_with_links( + instance, task_data, bgroup) + + for name, node in all_batch_nodes: + self.log.debug("name: {}, dir: {}".format( + name, dir(node) + )) + self.log.debug("__ node.attributes: {}".format( + node.attributes + )) # load plate to batch group self.log.info("Loading subset `{}` into batch `{}`".format( @@ -70,7 +79,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): ] # add nodes into batch group - opfapi.create_batch_group_conent( + return opfapi.create_batch_group_conent( batch_nodes, batch_links, batch_group) def _load_clip_to_context(self, instance, bgroup): From d0a2a781aea6009a2b928c7752ef64ae87c47fab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:08:45 +0200 Subject: [PATCH 216/337] flame: fixing head and tail --- .../hosts/flame/plugins/publish/collect_timeline_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index fe9bce5232..0af769a380 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -72,9 +72,9 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # solve handles length marker_data["handleStart"] = min( - marker_data["handleStart"], head) + marker_data["handleStart"], abs(head)) marker_data["handleEnd"] = min( - marker_data["handleEnd"], tail) + marker_data["handleEnd"], abs(tail)) with_audio = bool(marker_data.pop("audio")) From 1535b4712409a57bbbbddb73a65261106423ec7b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:10:23 +0200 Subject: [PATCH 217/337] flame: add hack comment --- .../hosts/flame/plugins/publish/collect_timeline_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 0af769a380..95c2002bd9 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -247,6 +247,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): head = clip_data.get("segment_head") tail = clip_data.get("segment_tail") + # HACK: it is here to serve for versions bellow 2021.1 if not head: head = int(clip_data["source_in"]) - int(first_frame) if not tail: From 304584573b7c67aac46f6b255f1ceb09fe1b4d7f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:14:32 +0200 Subject: [PATCH 218/337] flame: missing dict items function call --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 350acdfa90..cac99a25ac 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -35,7 +35,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): all_batch_nodes = self._add_nodes_to_batch_with_links( instance, task_data, bgroup) - for name, node in all_batch_nodes: + for name, node in all_batch_nodes.items(): self.log.debug("name: {}, dir: {}".format( name, dir(node) )) From 16bd11083da52c5836e183e8895524279378f08e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 15:22:09 +0200 Subject: [PATCH 219/337] flame: set node name if it doesn't exists in node props then set it from node_id --- openpype/hosts/flame/api/batch_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index 20118c249c..d4c8294466 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -107,7 +107,7 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): node["id"], node["type"], node["properties"]) # get node name for checking if exists - node_name = node_props.get("name") or node_id + node_name = node_props.pop("name", None) or node_id if all_batch_nodes.get(node_name): # update existing batch node @@ -116,6 +116,9 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): # create new batch node batch_node = batch_group.create_node(node_type) + # set name + setattr(batch_node, "name", node_name) + # set attributes found in node props for key, value in node_props.items(): if not hasattr(batch_node, key): From aa0176aeb71af90fd8808d30ffed0098ac55a8b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 15:58:01 +0200 Subject: [PATCH 220/337] modified extract slate frame to add values based on templates --- .../plugins/publish/extract_slate_frame.py | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index e917a28046..9737d4d5f8 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -1,6 +1,8 @@ import os import nuke +import six import pyblish.api + import openpype from openpype.hosts.nuke.api.lib import maintained_selection @@ -18,6 +20,10 @@ class ExtractSlateFrame(openpype.api.Extractor): families = ["slate"] hosts = ["nuke"] + key_value_mapping = { + "f_submission_note": [True, "{comment}"], + "f_submitting_for": [True, "{intent[value]}"] + } def process(self, instance): if hasattr(self, "viewer_lut_raw"): @@ -129,9 +135,7 @@ class ExtractSlateFrame(openpype.api.Extractor): for node in temporary_nodes: nuke.delete(node) - def get_view_process_node(self): - # Select only the target node if nuke.selectedNodes(): [n.setSelected(False) for n in nuke.selectedNodes()] @@ -162,13 +166,45 @@ class ExtractSlateFrame(openpype.api.Extractor): return comment = instance.context.data.get("comment") - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") + intent = instance.context.data.get("intent") + if not isinstance(intent, dict): + intent = { + "label": intent, + "value": intent + } - try: - node["f_submission_note"].setValue(comment) - node["f_submitting_for"].setValue(intent_value or "") - except NameError: - return - instance.data.pop("slateNode") + fill_data = { + "comment": comment, + "intent": intent + } + + for key, value in self.key_value_mapping.items(): + enabled, template = value + if not enabled: + continue + + try: + value = template.format(**fill_data) + + except ValueError: + self.log.warning( + "Couldn't fill template \"{}\" with data: {}".format( + template, fill_data + ), + exc_info=True + ) + continue + + except KeyError: + self.log.warning( + "Template contains unknown key", + exc_info=True + ) + continue + + try: + node[key].setValue(value) + except NameError: + self.log.warning( + "Failed to set value \"{}\" on node attribute \"{}\"" + ).format(value)) From 18c53a527a22c1a52d12c181d4d6302715ddf148 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 7 Apr 2022 16:06:20 +0200 Subject: [PATCH 221/337] Nuke Tut doc fix remove artist_host_nuke.md, fix in-page link to artist_hosts_nuke.md --- website/docs/artist_hosts_nuke.md | 145 -------------------------- website/docs/artist_hosts_nuke_tut.md | 2 +- 2 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 website/docs/artist_hosts_nuke.md diff --git a/website/docs/artist_hosts_nuke.md b/website/docs/artist_hosts_nuke.md deleted file mode 100644 index 1e02599570..0000000000 --- a/website/docs/artist_hosts_nuke.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -id: artist_hosts_nuke -title: Nuke -sidebar_label: Nuke ---- - -:::important -After Nuke starts it will automatically **Apply All Settings** for you. If you are sure the settings are wrong just contact your supervisor and he will set them correctly for you in project database. -::: - -:::note -The workflows are identical for both. We are supporting versions **`11.0`** and above. -::: - -## OpenPype global tools - -- [Set Context](artist_tools.md#set-context) -- [Work Files](artist_tools.md#workfiles) -- [Create](artist_tools.md#creator) -- [Load](artist_tools.md#loader) -- [Manage (Inventory)](artist_tools.md#inventory) -- [Publish](artist_tools.md#publisher) -- [Library Loader](artist_tools.md#library-loader) - -## Nuke specific tools - -
-
- -### Set Frame Ranges - -Use this feature in case you are not sure the frame range is correct. - -##### Result - -- setting Frame Range in script settings -- setting Frame Range in viewers (timeline) - -
-
- -![Set Frame Ranges](assets/nuke_setFrameRanges.png) - -
-
- - -
- -![Set Frame Ranges Timeline](assets/nuke_setFrameRanges_timeline.png) - -
- -1. limiting to Frame Range without handles -2. **Input** handle on start -3. **Output** handle on end - -
-
- -### Set Resolution - -
-
- - -This menu item will set correct resolution format for you defined by your production. - -##### Result - -- creates new item in formats with project name -- sets the new format as used - -
-
- -![Set Resolution](assets/nuke_setResolution.png) - -
-
- - -### Set Colorspace - -
-
- -This menu item will set correct Colorspace definitions for you. All has to be configured by your production (Project coordinator). - -##### Result - -- set Colorspace in your script settings -- set preview LUT to your viewers -- set correct colorspace to all discovered Read nodes (following expression set in settings) - -
-
- -![Set Colorspace](assets/nuke_setColorspace.png) - -
-
- - -### Apply All Settings - -
-
- -It is usually enough if you once per while use this option just to make yourself sure the workfile is having set correct properties. - -##### Result - -- set Frame Ranges -- set Colorspace -- set Resolution - -
-
- -![Apply All Settings](assets/nuke_applyAllSettings.png) - -
-
- -### Build Workfile - -
-
- -This tool will append all available subsets into an actual node graph. It will look into database and get all last [versions](artist_concepts.md#version) of available [subsets](artist_concepts.md#subset). - - -##### Result - -- adds all last versions of subsets (rendered image sequences) as read nodes -- adds publishable write node as `renderMain` subset - -
-
- -![Build First Work File](assets/nuke_buildFirstWorkfile.png) - -
-
\ No newline at end of file diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index 4b0ef7a78a..eefb213dd2 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -161,7 +161,7 @@ Nuke OpenPype menu shows the current context Launching Nuke with context stops your timer, and starts the clock on the shot and task you picked. -Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. +Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke_tut.md#apply-all-settings) from the OpenPype menu. - Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings, Creates Viewer node, sets it’s range and indicates handles by In and Out points. From 5bc3516baf676d115a992a6f63828a5c110c2556 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 16:43:46 +0200 Subject: [PATCH 222/337] added settings for slate templates --- .../plugins/publish/extract_slate_frame.py | 15 ++++-- .../defaults/project_settings/nuke.json | 14 +++++- .../schemas/schema_nuke_publish.json | 47 +++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 9737d4d5f8..f71d3ffff5 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -1,6 +1,7 @@ import os import nuke -import six +import copy + import pyblish.api import openpype @@ -20,9 +21,12 @@ class ExtractSlateFrame(openpype.api.Extractor): families = ["slate"] hosts = ["nuke"] + # Settings values + # - can be extended by other attributes from node in the future key_value_mapping = { "f_submission_note": [True, "{comment}"], - "f_submitting_for": [True, "{intent[value]}"] + "f_submitting_for": [True, "{intent[value]}"], + "f_vfx_scope_of_work": [False, ""] } def process(self, instance): @@ -173,10 +177,11 @@ class ExtractSlateFrame(openpype.api.Extractor): "value": intent } - fill_data = { + fill_data = copy.deepcopy(instance.data["anatomyData"]) + fill_data.update({ "comment": comment, "intent": intent - } + }) for key, value in self.key_value_mapping.items(): enabled, template = value @@ -205,6 +210,6 @@ class ExtractSlateFrame(openpype.api.Extractor): try: node[key].setValue(value) except NameError: - self.log.warning( + self.log.warning(( "Failed to set value \"{}\" on node attribute \"{}\"" ).format(value)) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 44d7f2d9d0..bdccb9b38e 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -160,7 +160,19 @@ } }, "ExtractSlateFrame": { - "viewer_lut_raw": false + "viewer_lut_raw": false, + "f_submission_note": [ + true, + "{comment}" + ], + "f_submitting_for": [ + true, + "{intent[value]}" + ], + "f_vfx_scope_of_work": [ + false, + "" + ] }, "IncrementScriptVersion": { "enabled": 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 27e8957786..8dbf224ce5 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 @@ -389,6 +389,53 @@ "type": "boolean", "key": "viewer_lut_raw", "label": "Viewer LUT raw" + }, + { + "type": "separator" + }, + { + "type": "label", + "label": "Fill specific slate node values with templates. Uncheck the checkbox to not change the value.", + "word_wrap": true + }, + { + "type": "list-strict", + "key": "f_submission_note", + "label": "Submission Note", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] + }, + { + "type": "list-strict", + "key": "f_submitting_for", + "label": "Submission For", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] + }, + { + "type": "list-strict", + "key": "f_vfx_scope_of_work", + "label": "VFX Scope Of Work", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] } ] }, From 208a6b0ecae86bca96dad9fc0984df16f3b86d7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 7 Apr 2022 17:08:16 +0200 Subject: [PATCH 223/337] Fix Python requirements to 3.7.9 According to some cases on Discord, 3.7.8 is not enough with PySide2 combination --- website/docs/dev_requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/dev_requirements.md b/website/docs/dev_requirements.md index 6c87054ba0..a10aea7865 100644 --- a/website/docs/dev_requirements.md +++ b/website/docs/dev_requirements.md @@ -14,7 +14,7 @@ The main things you will need to run and build pype are: - **Terminal** in your OS - PowerShell 5.0+ (Windows) - Bash (Linux) -- [**Python 3.7.8**](#python) or higher +- [**Python 3.7.9**](#python) or higher - [**MongoDB**](#database) From afbacb1944b690f03e31fb129d39476880c3934a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 17:14:33 +0200 Subject: [PATCH 224/337] flame: fixing setter for node name --- openpype/hosts/flame/api/batch_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/batch_utils.py b/openpype/hosts/flame/api/batch_utils.py index d4c8294466..9d419a4a90 100644 --- a/openpype/hosts/flame/api/batch_utils.py +++ b/openpype/hosts/flame/api/batch_utils.py @@ -117,7 +117,7 @@ def create_batch_group_conent(batch_nodes, batch_links, batch_group=None): batch_node = batch_group.create_node(node_type) # set name - setattr(batch_node, "name", node_name) + batch_node.name.set_value(node_name) # set attributes found in node props for key, value in node_props.items(): From fc6d01d0043df4e019ab89cf44d8c592e71be8e3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Apr 2022 17:15:44 +0200 Subject: [PATCH 225/337] hound catch --- openpype/hosts/flame/api/lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 998d7dfa7e..a4d8a7f9f0 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -826,5 +826,7 @@ class MediaInfoFile: def _write_result_xml_to_file(self, file, xml_data): # save it as new file tree = cET.ElementTree(xml_data) - tree.write(file, xml_declaration=True, - method='xml', encoding='UTF-8') \ No newline at end of file + tree.write( + file, xml_declaration=True, + method='xml', encoding='UTF-8' + ) From 81b2be514f970e19ccc292bee0afba80f893cb83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 18:47:37 +0200 Subject: [PATCH 226/337] changed order of ftrack collectors --- openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py | 2 +- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 2 +- 2 files changed, 2 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 07af217fb6..436a61cc18 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -6,7 +6,7 @@ import avalon.api class CollectFtrackApi(pyblish.api.ContextPlugin): """ Collects an ftrack session and the current task id. """ - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.4991 label = "Collect Ftrack Api" def process(self, context): diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 70030acad9..95987fe42e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -25,7 +25,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): based on 'families' (editorial drives it by presence of 'review') """ label = "Collect Ftrack Family" - order = pyblish.api.CollectorOrder + 0.4998 + order = pyblish.api.CollectorOrder + 0.4990 profiles = None From a34f279685d087dda82c99a6b0c32f9ceb9cc907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 18:47:53 +0200 Subject: [PATCH 227/337] modified labels to contain colons --- .../projects_schema/schemas/schema_nuke_publish.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8dbf224ce5..3bf0eb3214 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 @@ -401,7 +401,7 @@ { "type": "list-strict", "key": "f_submission_note", - "label": "Submission Note", + "label": "Submission Note:", "object_types": [ { "type": "boolean" @@ -414,7 +414,7 @@ { "type": "list-strict", "key": "f_submitting_for", - "label": "Submission For", + "label": "Submission For:", "object_types": [ { "type": "boolean" @@ -427,7 +427,7 @@ { "type": "list-strict", "key": "f_vfx_scope_of_work", - "label": "VFX Scope Of Work", + "label": "VFX Scope Of Work:", "object_types": [ { "type": "boolean" From 2ce4704a981f621d4bdfbdc7af27840edf0cb409 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 18:55:43 +0200 Subject: [PATCH 228/337] added collector looking for custom attribute values and it's settings --- .../modules/ftrack/lib/custom_attributes.py | 2 +- .../publish/collect_custom_attributes_data.py | 138 ++++++++++++++++++ .../defaults/project_settings/ftrack.json | 4 + .../schema_project_ftrack.json | 25 ++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index 29c6b5e7f8..2f53815368 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -135,7 +135,7 @@ def query_custom_attributes( output.extend( session.query( ( - "select value, entity_id from {}" + "select value, entity_id, configuration_id from {}" " where entity_id in ({}) and configuration_id in ({})" ).format( table_name, diff --git a/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py new file mode 100644 index 0000000000..f04c7c7954 --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py @@ -0,0 +1,138 @@ +""" +Requires: + context > ftrackSession + context > ftrackEntity + instance > ftrackEntity + +Provides: + instance > customData > ftrack +""" +import copy + +import pyblish.api + + +class CollectFtrackCustomAttributeData(pyblish.api.ContextPlugin): + """Collect custom attribute values and store them to customData. + + Data are stored into each instance in context under + instance.data["customData"]["ftrack"]. + """ + + order = pyblish.api.CollectorOrder + 0.4992 + label = "Collect Ftrack Custom Attribute Data" + + # Name of custom attributes for which will be look for + custom_attribute_keys = [] + + def process(self, context): + if not self.custom_attribute_keys: + self.log.info("Custom attribute keys are not set. Skipping") + return + + ftrack_entities_by_id = {} + default_entity_id = None + + context_entity = context.data.get("ftrackEntity") + if context_entity: + entity_id = context_entity["id"] + default_entity_id = entity_id + ftrack_entities_by_id[entity_id] = context_entity + + instances_by_entity_id = { + default_entity_id: [] + } + for instance in context: + entity = instance.data.get("ftrackEntity") + if not entity: + instances_by_entity_id[default_entity_id].append(instance) + continue + + entity_id = entity["id"] + ftrack_entities_by_id[entity_id] = entity + if entity_id not in instances_by_entity_id: + instances_by_entity_id[entity_id] = [] + instances_by_entity_id[entity_id].append(instance) + + if not ftrack_entities_by_id: + self.log.info("Ftrack entities are not set. Skipping") + return + + session = context.data["ftrackSession"] + custom_attr_key_by_id = self.query_attr_confs(session) + if not custom_attr_key_by_id: + self.log.info(( + "Didn't find any of defined custom attributes {}" + ).format(", ".join(self.custom_attribute_keys))) + return + + entity_ids = list(instances_by_entity_id.keys()) + values_by_entity_id = self.query_attr_values( + session, entity_ids, custom_attr_key_by_id + ) + + for entity_id, instances in instances_by_entity_id.items(): + if entity_id not in values_by_entity_id: + # Use defaut empty values + entity_id = None + + value = values_by_entity_id[entity_id] + if "customData" not in instance.data: + instance.data["customData"] = {} + instance.data["customData"]["ftrack"] = copy.deepcopy(value) + + def query_attr_values(self, session, entity_ids, custom_attr_key_by_id): + # Prepare values for query + entity_ids_joined = ",".join([ + '"{}"'.format(entity_id) + for entity_id in entity_ids + ]) + conf_ids_joined = ",".join([ + '"{}"'.format(conf_id) + for conf_id in custom_attr_key_by_id.keys() + ]) + # Query custom attribute values + value_items = session.query( + ( + "select value, entity_id, configuration_id" + " from CustomAttributeValue" + " where entity_id in ({}) and configuration_id in ({})" + ).format( + entity_ids_joined, + conf_ids_joined + ) + ).all() + + # Prepare default value output per entity id + values_by_key = { + key: None for key in self.custom_attribute_keys + } + # Prepare all entity ids that were queried + values_by_entity_id = { + entity_id: copy.deepcopy(values_by_key) + for entity_id in entity_ids + } + # Add none entity id which is used as default value + values_by_entity_id[None] = copy.deepcopy(values_by_key) + # Go through queried data and store them + for item in value_items: + conf_id = item["configuration_id"] + conf_key = custom_attr_key_by_id[conf_id] + entity_id = item["entity_id"] + values_by_entity_id[entity_id][conf_key] = item["value"] + return values_by_entity_id + + def query_attr_confs(self, session): + custom_attributes = set(self.custom_attribute_keys) + cust_attrs_query = ( + "select id, key from CustomAttributeConfiguration" + " where key in ({})" + ).format(", ".join( + ["\"{}\"".format(attr_name) for attr_name in custom_attributes] + )) + + custom_attr_confs = session.query(cust_attrs_query).all() + return { + conf["id"]: conf["key"] + for conf in custom_attr_confs + } diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 31d6a70ac7..deade08c0b 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -352,6 +352,10 @@ } ] }, + "CollectFtrackCustomAttributeData": { + "enabled": false, + "custom_attribute_keys": [] + }, "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 5ce9b24b4b..47effb3dbd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -725,6 +725,31 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFtrackCustomAttributeData", + "label": "Collect Custom Attribute Data", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Collect custom attributes from ftrack for ftrack entities that can be used in some templates during publishing." + }, + { + "type": "list", + "key": "custom_attribute_keys", + "label": "Custom attribute keys", + "object_type": "text" + } + ] + }, { "type": "dict", "collapsible": true, From ab252ec7c9b7af96835ff248bce0cc75d6df6bd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 18:56:00 +0200 Subject: [PATCH 229/337] added ability to define word wrap of labels --- openpype/settings/entities/schemas/README.md | 1 + openpype/tools/settings/settings/base.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index fbfd699937..b4bfef2972 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -745,6 +745,7 @@ How output of the schema could look like on save: ### 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`) ``` { diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index bd48b3a966..44ec09b2ca 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -567,7 +567,9 @@ class GUIWidget(BaseWidget): def _create_label_ui(self): label = self.entity["label"] + word_wrap = self.entity.schema_data.get("word_wrap", False) label_widget = QtWidgets.QLabel(label, self) + label_widget.setWordWrap(word_wrap) label_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) label_widget.setObjectName("SettingsLabel") label_widget.linkActivated.connect(self._on_link_activate) From 486317cb96826185cd1257c880ee04681dd6a264 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 19:06:25 +0200 Subject: [PATCH 230/337] added custom data to slate text formatting data --- openpype/hosts/nuke/plugins/publish/extract_slate_frame.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index f71d3ffff5..6935afe144 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -179,6 +179,9 @@ class ExtractSlateFrame(openpype.api.Extractor): fill_data = copy.deepcopy(instance.data["anatomyData"]) fill_data.update({ + "custom": copy.deepcopy( + instance.data.get("customData") or {} + ), "comment": comment, "intent": intent }) From bedccd6f69540117379f7db680260ebd3bfdcea2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 19:06:35 +0200 Subject: [PATCH 231/337] added custom data to burnin custm data --- openpype/plugins/publish/extract_burnin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index b2ca8850b6..a543083a87 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -221,11 +221,17 @@ class ExtractBurnin(openpype.api.Extractor): filled_anatomy = anatomy.format_all(burnin_data) burnin_data["anatomy"] = filled_anatomy.get_solved() - # Add context data burnin_data. - burnin_data["custom"] = ( + custom_data = copy.deepcopy( + instance.data.get("customData") or {} + ) + # Backwards compatibility + custom_data.update( instance.data.get("custom_burnin_data") or {} ) + # Add context data burnin_data. + burnin_data["custom"] = custom_data + # Add source camera name to burnin data camera_name = repre.get("camera_name") if camera_name: From f9c2f87f903d4f411b8b950997e258756a6aa0f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 19:09:29 +0200 Subject: [PATCH 232/337] fixed adding data to instance --- .../publish/collect_custom_attributes_data.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py index f04c7c7954..ef0e4a9ccb 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py +++ b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py @@ -76,10 +76,17 @@ class CollectFtrackCustomAttributeData(pyblish.api.ContextPlugin): # Use defaut empty values entity_id = None - value = values_by_entity_id[entity_id] - if "customData" not in instance.data: - instance.data["customData"] = {} - instance.data["customData"]["ftrack"] = copy.deepcopy(value) + for instance in instances: + value = copy.deepcopy(values_by_entity_id[entity_id]) + if "customData" not in instance.data: + instance.data["customData"] = {} + instance.data["customData"]["ftrack"] = value + instance_label = ( + instance.data.get("label") or instance.data["name"] + ) + self.log.debug(( + "Added ftrack custom data to instance \"{}\": {}" + ).format(instance_label, value)) def query_attr_values(self, session, entity_ids, custom_attr_key_by_id): # Prepare values for query From a2eb78a3a7d3c275fb650c70cd3d67a275d80b9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Apr 2022 19:17:05 +0200 Subject: [PATCH 233/337] added few more comments --- .../ftrack/plugins/publish/collect_custom_attributes_data.py | 3 +++ openpype/plugins/publish/extract_burnin.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py index ef0e4a9ccb..43fa3bc3f8 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py +++ b/openpype/modules/ftrack/plugins/publish/collect_custom_attributes_data.py @@ -17,6 +17,9 @@ class CollectFtrackCustomAttributeData(pyblish.api.ContextPlugin): Data are stored into each instance in context under instance.data["customData"]["ftrack"]. + + Hierarchical attributes are not looked up properly for that functionality + custom attribute values lookup must be extended. """ order = pyblish.api.CollectorOrder + 0.4992 diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a543083a87..41c84103a6 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -224,7 +224,7 @@ class ExtractBurnin(openpype.api.Extractor): custom_data = copy.deepcopy( instance.data.get("customData") or {} ) - # Backwards compatibility + # Backwards compatibility (since 2022/04/07) custom_data.update( instance.data.get("custom_burnin_data") or {} ) From 5f1940a9982ffee2266776d0f70812853241c941 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 11:17:22 +0200 Subject: [PATCH 234/337] flame: adding maintainable temp file path --- openpype/hosts/flame/api/__init__.py | 2 ++ openpype/hosts/flame/api/lib.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index a0c40904ed..6744a7ff11 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -27,6 +27,7 @@ from .lib import ( get_frame_from_filename, get_padding_from_filename, maintained_object_duplication, + maintained_temp_file_path, get_clip_segment, get_batch_group_from_desktop, MediaInfoFile @@ -103,6 +104,7 @@ __all__ = [ "get_frame_from_filename", "get_padding_from_filename", "maintained_object_duplication", + "maintained_temp_file_path", "get_clip_segment", "get_batch_group_from_desktop", "MediaInfoFile", diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index a4d8a7f9f0..51d48becf1 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -4,6 +4,7 @@ import re import six import json import pickle +import tempfile import itertools import contextlib import xml.etree.cElementTree as cET @@ -695,6 +696,25 @@ def maintained_object_duplication(item): flame.delete(duplicate) +@contextlib.contextmanager +def maintained_temp_file_path(suffix=None): + _suffix = suffix or "" + + try: + # Store dumped json to temporary file + temporary_file = tempfile.mktemp( + suffix=_suffix, prefix="flame_maintained_") + yield temporary_file.name.replace("\\", "/") + + except IOError as _error: + raise IOError( + "Not able to create temp json file: {}".format(_error)) from _error + + finally: + # Remove the temporary json + os.remove(temporary_file) + + def get_clip_segment(flame_clip): name = flame_clip.name.get_value() version = flame_clip.versions[0] From 03bf240816d98bd47e0944eac0a57706c7cc0869 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 11:18:10 +0200 Subject: [PATCH 235/337] flame: refactoring MediaInfoFile class so it is parentable --- openpype/hosts/flame/api/lib.py | 161 +++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 45 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 51d48becf1..de3467aa76 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 six import json import pickle import tempfile @@ -740,19 +739,34 @@ def get_batch_group_from_desktop(name): class MediaInfoFile: - media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" - tmp_name = "_tmp.clip" - tmp_file = None + """Class to get media info file clip data - clip_data = None - out_feed_nb_ticks = None - out_feed_fps = None - out_feed_drop_mode = None + Raises: + IOError: MEDIA_SCRIPT_PATH path doesn't exists + TypeError: Not able to generate clip xml data file + ET.ParseError: Missing clip in xml clip data + IOError: Not able to save xml clip data to file + + Attributes: + str: `MEDIA_SCRIPT_PATH` path to flame binary + logging.Logger: `log` logger + """ + MEDIA_SCRIPT_PATH = "/opt/Autodesk/mio/current/dl_get_media_info" log = log - def __init__(self, path): - # test if media script paht exists + _clip_data = None + _start_frame = None + _fps = None + _drop_mode = None + + def __init__(self, path, **kwargs): + + # replace log if any + if kwargs.get("log"): + self.log = kwargs["log"] + + # test if `dl_get_media_info` paht exists self._validate_media_script_path() # derivate other feed variables @@ -760,40 +774,93 @@ class MediaInfoFile: self.feed_dir = os.path.dirname(path) self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) + with maintained_temp_file_path(".clip") as tmp_path: + self.log.info("Temp File: {}".format(tmp_path)) + self._generate_media_info_file(tmp_path) - # remove previously generated temp files - # it will be regenerated - self._clear_tmp_file() + # get clip data and make them single if there is multiple + # clips data + xml_data = self._make_single_clip_media_info(tmp_path) - self.log.info("Temp File: {}".format(self.tmp_file)) + # get all time related data and assign them + self._get_time_info_from_origin(xml_data) + self.set_clip_data(xml_data) - self._generate_media_info_file() + @property + def clip_data(self): + """Clip's xml clip data + + Returns: + xml.etree.ElementTree: xml data + """ + return self._clip_data + + @clip_data.setter + def clip_data(self, data): + self._clip_data = data + + @property + def start_frame(self): + """ Clip's starting frame found in timecode + + Returns: + int: number of frames + """ + return self._start_frame + + @start_frame.setter + def start_frame(self, number): + self._start_frame = int(number) + + @property + def fps(self): + """ Clip's frame rate + + Returns: + float: frame rate + """ + return self._fps + + @fps.setter + def fps(self, fl_number): + self._fps = float(fl_number) + + @property + def drop_mode(self): + """ Clip's drop frame mode + + Returns: + str: drop frame flag + """ + return self._drop_mode + + @drop_mode.setter + def drop_mode(self, text): + self._drop_mode = str(text) def _validate_media_script_path(self): - if not os.path.isfile(self.media_script_path): + if not os.path.isfile(self.MEDIA_SCRIPT_PATH): raise IOError("Media Scirpt does not exist: `{}`".format( - self.media_script_path)) + self.MEDIA_SCRIPT_PATH)) - def _generate_media_info_file(self): + def _generate_media_info_file(self, fpath): # Create cmd arguments for gettig xml file info file cmd_args = [ - self.media_script_path, + self.MEDIA_SCRIPT_PATH, "-e", self.feed_ext, - "-o", self.tmp_file, + "-o", fpath, self.feed_dir ] - # execute creation of clip xml template data try: + # execute creation of clip xml template data openpype.run_subprocess(cmd_args) - self._make_single_clip_media_info() - except TypeError: - self.log.error("Error creating self.tmp_file") - six.reraise(*sys.exc_info()) + except TypeError as error: + raise TypeError( + "Error creating `{}` due: {}".format(fpath, error)) from error - def _make_single_clip_media_info(self): - with open(self.tmp_file) as f: + def _make_single_clip_media_info(self, fpath): + with open(fpath) as f: lines = f.readlines() _added_root = itertools.chain( "", deepcopy(lines)[1:], "") @@ -816,37 +883,41 @@ class MediaInfoFile: ] )) - self._get_time_info_from_origin(matching_clip) - self.clip_data = matching_clip - self._write_result_xml_to_file(self.tmp_file, matching_clip) - - def _clear_tmp_file(self): - if os.path.isfile(self.tmp_file): - os.remove(self.tmp_file) + return matching_clip def _get_time_info_from_origin(self, xml_data): try: 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') - self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text + self.start_frame(out_feed_nb_ticks_obj.text) + + # fps out_feed_fps_obj = out_feed.find( 'startTimecode/rate') - self.out_feed_fps = out_feed_fps_obj.text + self.fps(out_feed_fps_obj.text) + + # drop frame mode out_feed_drop_mode_obj = out_feed.find( 'startTimecode/dropMode') - self.out_feed_drop_mode = out_feed_drop_mode_obj.text + self.drop_mode(out_feed_drop_mode_obj.text) break else: continue except Exception as msg: self.log.warning(msg) - def _write_result_xml_to_file(self, file, xml_data): - # save it as new file - tree = cET.ElementTree(xml_data) - tree.write( - file, xml_declaration=True, - method='xml', encoding='UTF-8' - ) + @staticmethod + def write_clip_data_to_file(fpath, xml_data): + try: + # save it as new file + tree = cET.ElementTree(xml_data) + tree.write( + fpath, xml_declaration=True, + method='xml', encoding='UTF-8' + ) + except IOError as error: + raise IOError( + "Not able to write data to file: {}".format(error)) from error From 1848513fb90943266c882a1fd55840e2c069f24d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 11:49:35 +0200 Subject: [PATCH 236/337] OP-3072 - check enablement as a first step for GDrive --- openpype/modules/sync_server/providers/gdrive.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index b783f7958b..aa7329b104 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -73,6 +73,11 @@ class GDriveHandler(AbstractProvider): format(site_name)) return + if not self.presets["enabled"]: + log.debug("Sync Server: Site {} not enabled for {}.". + format(site_name, project_name)) + return + current_platform = platform.system().lower() cred_path = self.presets.get("credentials_url", {}). \ get(current_platform) or '' @@ -97,11 +102,10 @@ class GDriveHandler(AbstractProvider): return self.service = None - if self.presets["enabled"]: - self.service = self._get_gd_service(cred_path) + self.service = self._get_gd_service(cred_path) - self._tree = tree - self.active = True + self._tree = tree + self.active = True def is_active(self): """ From 753ee312a1f36985603e57b3488bd62cff15ad41 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 11:52:10 +0200 Subject: [PATCH 237/337] fixed imports in unreal creators --- openpype/hosts/unreal/plugins/create/create_camera.py | 8 +++----- openpype/hosts/unreal/plugins/create/create_layout.py | 8 +++----- openpype/hosts/unreal/plugins/create/create_look.py | 5 ++--- .../hosts/unreal/plugins/create/create_staticmeshfbx.py | 4 ++-- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index c2905fb6dd..2842900834 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -2,13 +2,11 @@ import unreal from unreal import EditorAssetLibrary as eal from unreal import EditorLevelLibrary as ell -from openpype.hosts.unreal.api.plugin import Creator -from avalon.unreal import ( - instantiate, -) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api.pipeline import instantiate -class CreateCamera(Creator): +class CreateCamera(plugin.Creator): """Layout output for character rigs""" name = "layoutMain" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index 00e83cf433..751bece167 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- from unreal import EditorLevelLibrary as ell -from openpype.hosts.unreal.api.plugin import Creator -from avalon.unreal import ( - instantiate, -) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api.pipeline import instantiate -class CreateLayout(Creator): +class CreateLayout(plugin.Creator): """Layout output for character rigs.""" name = "layoutMain" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 59c40d3e74..12f6b70ae6 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- """Create look in Unreal.""" import unreal # noqa -from openpype.hosts.unreal.api.plugin import Creator -from openpype.hosts.unreal.api import pipeline +from openpype.hosts.unreal.api import pipeline, plugin -class CreateLook(Creator): +class CreateLook(plugin.Creator): """Shader connections defining shape look.""" name = "unrealLook" diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 700eac7366..601c2fae06 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """Create Static Meshes as FBX geometry.""" import unreal # noqa -from openpype.hosts.unreal.api.plugin import Creator +from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( instantiate, ) -class CreateStaticMeshFBX(Creator): +class CreateStaticMeshFBX(plugin.Creator): """Static FBX geometry.""" name = "unrealStaticMeshMain" From 08e7d47cf80bddee85623f36e425114b23a02e38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 11:52:23 +0200 Subject: [PATCH 238/337] removed unused creator in pipeline.py --- openpype/hosts/unreal/api/pipeline.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 6d7a6ad1e2..f2c264e5a4 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -4,7 +4,6 @@ import logging from typing import List import pyblish.api -from avalon import api from openpype.pipeline import ( register_loader_plugin_path, @@ -76,30 +75,6 @@ def _register_events(): pass -class Creator(LegacyCreator): - hosts = ["unreal"] - asset_types = [] - - def process(self): - nodes = list() - - with unreal.ScopedEditorTransaction("OpenPype Creating Instance"): - if (self.options or {}).get("useSelection"): - self.log.info("setting ...") - print("settings ...") - nodes = unreal.EditorUtilityLibrary.get_selected_assets() - - asset_paths = [a.get_path_name() for a in nodes] - self.name = move_assets_to_path( - "/Game", self.name, asset_paths - ) - - instance = create_publish_instance("/Game", self.name) - imprint(instance, self.data) - - return instance - - def ls(): """List all containers. From fc49311937375908301845196e8677967476a7c1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 11:50:32 +0200 Subject: [PATCH 239/337] OP-3072 - check enablement as a first step for Dropbox --- .../modules/sync_server/providers/dropbox.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/openpype/modules/sync_server/providers/dropbox.py b/openpype/modules/sync_server/providers/dropbox.py index f5910299e5..dfc42fed75 100644 --- a/openpype/modules/sync_server/providers/dropbox.py +++ b/openpype/modules/sync_server/providers/dropbox.py @@ -17,6 +17,7 @@ class DropboxHandler(AbstractProvider): self.active = False self.site_name = site_name self.presets = presets + self.dbx = None if not self.presets: log.info( @@ -24,6 +25,11 @@ class DropboxHandler(AbstractProvider): ) return + if not self.presets["enabled"]: + 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" @@ -44,16 +50,13 @@ class DropboxHandler(AbstractProvider): log.info(msg) return - self.dbx = None - - if self.presets["enabled"]: - try: - self.dbx = self._get_service( - token, acting_as_member, team_folder_name - ) - except Exception as e: - log.info("Could not establish dropbox object: {}".format(e)) - return + try: + self.dbx = self._get_service( + token, acting_as_member, team_folder_name + ) + except Exception as e: + log.info("Could not establish dropbox object: {}".format(e)) + return super(AbstractProvider, self).__init__() From adbd3593424fe9ea981705505038bcf89b9d81b9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 11:54:36 +0200 Subject: [PATCH 240/337] flame: otio removing reel clip dependency --- openpype/hosts/flame/otio/flame_export.py | 70 ++++------------------- 1 file changed, 11 insertions(+), 59 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 3e76968963..25be310d01 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -261,24 +261,15 @@ def create_otio_markers(otio_item, item): otio_item.markers.append(otio_marker) -def create_otio_reference(clip_data): +def create_otio_reference(clip_data, fps=None): metadata = _get_metadata(clip_data) # get file info for path and start frame frame_start = 0 - fps = CTX.get_fps() + fps = fps or CTX.get_fps() path = clip_data["fpath"] - reel_clip = None - match_reel_clip = [ - clip for clip in CTX.clips - if clip["fpath"] == path - ] - if match_reel_clip: - reel_clip = match_reel_clip.pop() - fps = reel_clip["fps"] - file_name = os.path.basename(path) file_head, extension = os.path.splitext(file_name) @@ -342,16 +333,17 @@ def create_otio_reference(clip_data): def create_otio_clip(clip_data): segment = clip_data["PySegment"] - # create media reference - media_reference = create_otio_reference(clip_data) - # calculate source in media_info = MediaInfoFile(clip_data["fpath"]) - xml_timecode_ticks = media_info.out_feed_nb_ticks - if xml_timecode_ticks: - first_frame = int(xml_timecode_ticks) - else: - first_frame = utils.get_frame_from_filename(clip_data["fpath"]) or 0 + media_timecode_start = media_info.start_frame + media_fps = media_info.fps + + # create media reference + media_reference = create_otio_reference(clip_data, media_fps) + + # define first frame + first_frame = media_timecode_start or utils.get_frame_from_filename( + clip_data["fpath"]) or 0 source_in = int(clip_data["source_in"]) - int(first_frame) @@ -385,41 +377,6 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def get_clips_in_reels(project): - output_clips = [] - project_desktop = project.current_workspace.desktop - - for reel_group in project_desktop.reel_groups: - for reel in reel_group.reels: - for clip in reel.clips: - clip_data = { - "PyClip": clip, - "fps": float(str(clip.frame_rate)[:-4]) - } - - attrs = [ - "name", "width", "height", - "ratio", "sample_rate", "bit_depth" - ] - - for attr in attrs: - val = getattr(clip, attr) - clip_data[attr] = val - - version = clip.versions[-1] - track = version.tracks[-1] - # each reel clip is also having one segment - for segment in track.segments: - segment_data = _get_segment_attributes( - segment, from_clip=True) - if segment_data: - clip_data.update(segment_data) - - output_clips.append(clip_data) - - return output_clips - - def _get_colourspace_policy(): output = {} @@ -579,11 +536,6 @@ def create_otio_timeline(sequence): log.info(sequence.attributes) CTX.project = get_current_flame_project() - CTX.clips = get_clips_in_reels(CTX.project) - - log.debug(pformat( - CTX.clips - )) # get current timeline CTX.set_fps( From 4797f584981bd5d07370f417ebe073c6cedbd76f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 11:59:34 +0200 Subject: [PATCH 241/337] flame: add todos for metadata feature --- openpype/hosts/flame/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index de3467aa76..e9e0130401 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -271,6 +271,7 @@ def rescan_hooks(): def get_metadata(project_name, _log=None): + # TODO: can be replaced by MediaInfoFile class method from adsk.libwiretapPythonClientAPI import ( WireTapClient, WireTapServerHandle, @@ -750,6 +751,8 @@ class MediaInfoFile: Attributes: str: `MEDIA_SCRIPT_PATH` path to flame binary logging.Logger: `log` logger + + TODO: add method for getting metadata to dict """ MEDIA_SCRIPT_PATH = "/opt/Autodesk/mio/current/dl_get_media_info" From 7f0b4710f2f1fba3f185e1ee47ec85fa03187662 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 12:13:47 +0200 Subject: [PATCH 242/337] Refactor - faster resolution of query --- openpype/modules/sync_server/sync_server_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index ddcf16a410..2c27571f9f 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -850,7 +850,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): active_site = sync_settings["config"]["active_site"] # for Tray running background process - if active_site == get_local_site_id() and active_site not in sites: + if active_site not in sites and active_site == get_local_site_id(): sites.append(active_site) return sites From 43a68681d6fa71254158006fb642074da51205c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 12:21:00 +0200 Subject: [PATCH 243/337] Refactor - changed logic to loop through alt sites --- .../modules/sync_server/sync_server_module.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 0a70830255..ebdcffdab7 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -313,18 +313,23 @@ class SyncServerModule(OpenPypeModule, ITrayModule): alt_site_pairs[alt_site] = [] alt_site_pairs[alt_site].extend([site_name]) - # transitive relationship, eg site is alternative to another which is - # alternative to nex site - loop = True - while loop: - loop = False - for site, alt_sites in alt_site_pairs.items(): - for alt_site in alt_sites: - for alt_alt_site in alt_site_pairs.get(alt_site, []): - if (alt_alt_site != site - and alt_alt_site not in alt_sites): - alt_site_pairs[site].append(alt_alt_site) - loop = True + 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 @@ -992,6 +997,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if self.enabled and sync_settings.get('enabled'): sites.append(self.LOCAL_SITE) + active_site = sync_settings["config"]["active_site"] + # for Tray running background process + if active_site not in sites and active_site == get_local_site_id(): + sites.append(active_site) + return sites def tray_init(self): From bfae95a8475981e014561c1eec8b9b54de5a8424 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 12:23:16 +0200 Subject: [PATCH 244/337] flame: OpenClipSolver inheriting from MediaInfoFile class --- openpype/hosts/flame/api/lib.py | 4 +- openpype/hosts/flame/api/plugin.py | 162 ++++++----------------------- 2 files changed, 36 insertions(+), 130 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index e9e0130401..91f5c26562 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -766,8 +766,8 @@ class MediaInfoFile: def __init__(self, path, **kwargs): # replace log if any - if kwargs.get("log"): - self.log = kwargs["log"] + if kwargs.get("logger"): + self.log = kwargs["logger"] # test if `dl_get_media_info` paht exists self._validate_media_script_path() diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index bd0f9f1a81..a23be946ba 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -679,53 +679,39 @@ class ClipLoader(LoaderPlugin): ] -# TODO: inheritance from flame.api.lib.MediaInfoFile -class OpenClipSolver: - media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" - tmp_name = "_tmp.clip" - tmp_file = None +class OpenClipSolver(flib.MediaInfoFile): create_new_clip = False - out_feed_nb_ticks = None - out_feed_fps = None - out_feed_drop_mode = None - log = log def __init__(self, openclip_file_path, feed_data): - # test if media script paht exists - self._validate_media_script_path() + self.out_file = openclip_file_path # new feed variables: - feed_path = feed_data["path"] - self.feed_version_name = feed_data["version"] - self.feed_colorspace = feed_data.get("colorspace") + feed_path = feed_data.pop("path") + # initialize parent class + super(OpenClipSolver).__init__( + feed_path, + **feed_data + ) + + # get logger if any if feed_data.get("logger"): self.log = feed_data["logger"] + # get other metadata + self.feed_version_name = feed_data["version"] + self.feed_colorspace = feed_data.get("colorspace") + # derivate other feed variables self.feed_basename = os.path.basename(feed_path) self.feed_dir = os.path.dirname(feed_path) self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - if not self._is_valid_tmp_file(openclip_file_path): - # openclip does not exist yet and will be created - self.tmp_file = self.out_file = openclip_file_path + if not self._is_valid_tmp_file(self.out_file): self.create_new_clip = True - else: - # update already created clip - # output a temp file - self.out_file = openclip_file_path - self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) - - # remove previously generated temp files - # it will be regenerated - self._clear_tmp_file() - - self.log.info("Temp File: {}".format(self.tmp_file)) - def _is_valid_tmp_file(self, file): # check if file exists if os.path.isfile(file): @@ -740,7 +726,6 @@ class OpenClipSolver: return False def make(self): - self._generate_media_info_file() if self.create_new_clip: # New openClip @@ -748,58 +733,6 @@ class OpenClipSolver: else: self._update_open_clip() - def _validate_media_script_path(self): - if not os.path.isfile(self.media_script_path): - raise IOError("Media Scirpt does not exist: `{}`".format( - self.media_script_path)) - - def _generate_media_info_file(self): - # Create cmd arguments for gettig xml file info file - cmd_args = [ - self.media_script_path, - "-e", self.feed_ext, - "-o", self.tmp_file, - self.feed_dir - ] - - # execute creation of clip xml template data - try: - openpype.run_subprocess(cmd_args) - self._make_single_clip_media_info() - except TypeError: - self.log.error("Error creating self.tmp_file") - six.reraise(*sys.exc_info()) - - def _make_single_clip_media_info(self): - with open(self.tmp_file) as f: - lines = f.readlines() - _added_root = itertools.chain( - "", deepcopy(lines)[1:], "") - new_root = ET.fromstringlist(_added_root) - - # find the clip which is matching to my input name - xml_clips = new_root.findall("clip") - matching_clip = None - for xml_clip in xml_clips: - if xml_clip.find("name").text in self.feed_basename: - matching_clip = xml_clip - - if matching_clip is None: - # return warning there is missing clip - raise ET.ParseError( - "Missing clip in `{}`. Available clips {}".format( - self.feed_basename, [ - xml_clip.find("name").text - for xml_clip in xml_clips - ] - )) - - self._write_result_xml_to_file(self.tmp_file, matching_clip) - - def _clear_tmp_file(self): - if os.path.isfile(self.tmp_file): - os.remove(self.tmp_file) - def _clear_handler(self, xml_object): for handler in xml_object.findall("./handler"): self.log.debug("Handler found") @@ -808,9 +741,8 @@ class OpenClipSolver: def _create_new_open_clip(self): self.log.info("Building new openClip") - tmp_xml = ET.parse(self.tmp_file) - - tmp_xml_feeds = tmp_xml.find('tracks/track/feeds') + # clip data comming from MediaInfoFile + tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') tmp_xml_feeds.set('currentVersion', self.feed_version_name) for tmp_feed in tmp_xml_feeds: tmp_feed.set('vuid', self.feed_version_name) @@ -821,46 +753,47 @@ class OpenClipSolver: self._clear_handler(tmp_feed) - tmp_xml_versions_obj = tmp_xml.find('versions') + tmp_xml_versions_obj = self.clip_data.find('versions') tmp_xml_versions_obj.set('currentVersion', self.feed_version_name) for xml_new_version in tmp_xml_versions_obj: xml_new_version.set('uid', self.feed_version_name) xml_new_version.set('type', 'version') - xml_data = self._fix_xml_data(tmp_xml) + xml_data = self._fix_xml_data(self.clip_data) self.log.info("Adding feed version: {}".format(self.feed_basename)) - self._write_result_xml_to_file(self.out_file, xml_data) - - self.log.info("openClip Updated: {}".format(self.tmp_file)) + self.write_clip_data_to_file(self.out_file, xml_data) def _update_open_clip(self): self.log.info("Updating openClip ..") out_xml = ET.parse(self.out_file) - tmp_xml = ET.parse(self.tmp_file) self.log.debug(">> out_xml: {}".format(out_xml)) - self.log.debug(">> tmp_xml: {}".format(tmp_xml)) + self.log.debug(">> self.clip_data: {}".format(self.clip_data)) # Get new feed from tmp file - tmp_xml_feed = tmp_xml.find('tracks/track/feeds/feed') + tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed') self._clear_handler(tmp_xml_feed) - self._get_time_info_from_origin(out_xml) - if self.out_feed_fps: + # update fps from MediaInfoFile class + if self.fps: tmp_feed_fps_obj = tmp_xml_feed.find( "startTimecode/rate") - tmp_feed_fps_obj.text = self.out_feed_fps - if self.out_feed_nb_ticks: + tmp_feed_fps_obj.text = self.fps + + # update start_frame from MediaInfoFile class + if self.start_frame: tmp_feed_nb_ticks_obj = tmp_xml_feed.find( "startTimecode/nbTicks") - tmp_feed_nb_ticks_obj.text = self.out_feed_nb_ticks - if self.out_feed_drop_mode: + tmp_feed_nb_ticks_obj.text = self.start_frame + + # update drop_mode from MediaInfoFile class + if self.drop_mode: tmp_feed_drop_mode_obj = tmp_xml_feed.find( "startTimecode/dropMode") - tmp_feed_drop_mode_obj.text = self.out_feed_drop_mode + tmp_feed_drop_mode_obj.text = self.drop_mode new_path_obj = tmp_xml_feed.find( "spans/span/path") @@ -901,31 +834,10 @@ class OpenClipSolver: self.log.info("Adding feed version: {}".format( self.feed_version_name)) - self._write_result_xml_to_file(self.out_file, xml_data) + self.write_clip_data_to_file(self.out_file, xml_data) self.log.info("openClip Updated: {}".format(self.out_file)) - self._clear_tmp_file() - - def _get_time_info_from_origin(self, xml_data): - try: - for out_track in xml_data.iter('track'): - for out_feed in out_track.iter('feed'): - out_feed_nb_ticks_obj = out_feed.find( - 'startTimecode/nbTicks') - self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text - out_feed_fps_obj = out_feed.find( - 'startTimecode/rate') - self.out_feed_fps = out_feed_fps_obj.text - out_feed_drop_mode_obj = out_feed.find( - 'startTimecode/dropMode') - self.out_feed_drop_mode = out_feed_drop_mode_obj.text - break - else: - continue - except Exception as msg: - self.log.warning(msg) - def _feed_exists(self, xml_data, path): # loop all available feed paths and check if # the path is not already in file @@ -940,12 +852,6 @@ class OpenClipSolver: self._clear_handler(xml_root) return xml_root - def _write_result_xml_to_file(self, file, xml_data): - # save it as new file - tree = cET.ElementTree(xml_data) - tree.write(file, xml_declaration=True, - method='xml', encoding='UTF-8') - def _create_openclip_backup_file(self, file): bck_file = "{}.bak".format(file) # if backup does not exist From 507f3615ab8f42f5664afcac01d339e0517afdf5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 12:24:04 +0200 Subject: [PATCH 245/337] Refactor - changed logic to loop through alt sites --- openpype/plugins/publish/integrate_new.py | 31 +++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index ed1c02b825..3eca460ba3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -8,6 +8,7 @@ import errno import six import re import shutil +from collections import deque from bson.objectid import ObjectId from pymongo import DeleteOne, InsertOne @@ -1199,21 +1200,23 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # transitive relationship, eg site is alternative to another which is # alternative to nex site - loop = True - while loop: - loop = False - for site_name, alt_sites in alt_site_pairs.items(): - for alt_site in alt_sites: - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name: - continue + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() - for alt_alt_site in alt_site_pairs.get(alt_site, []): - if ( alt_alt_site != site_name - and alt_alt_site not in alt_sites): - alt_site_pairs[site_name].append(alt_alt_site) - loop = True + # 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 72ecb6192a0ac609b4ad951dbd35712144b012a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 12:52:27 +0200 Subject: [PATCH 246/337] flame: fixing flame compatibility and python2 --- openpype/hosts/flame/api/__init__.py | 1 - openpype/hosts/flame/api/lib.py | 6 +++--- openpype/hosts/flame/api/plugin.py | 5 +---- openpype/hosts/flame/otio/flame_export.py | 5 ++--- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 6744a7ff11..2c461e5f16 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -57,7 +57,6 @@ from .plugin import ( PublishableClip, ClipLoader, OpenClipSolver - ) from .workio import ( open_file, diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 91f5c26562..6d93018fef 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -708,7 +708,7 @@ def maintained_temp_file_path(suffix=None): except IOError as _error: raise IOError( - "Not able to create temp json file: {}".format(_error)) from _error + "Not able to create temp json file: {}".format(_error)) finally: # Remove the temporary json @@ -860,7 +860,7 @@ class MediaInfoFile: openpype.run_subprocess(cmd_args) except TypeError as error: raise TypeError( - "Error creating `{}` due: {}".format(fpath, error)) from error + "Error creating `{}` due: {}".format(fpath, error)) def _make_single_clip_media_info(self, fpath): with open(fpath) as f: @@ -923,4 +923,4 @@ class MediaInfoFile: ) except IOError as error: raise IOError( - "Not able to write data to file: {}".format(error)) from error + "Not able to write data to file: {}".format(error)) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index a23be946ba..ab74bb4605 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,15 +1,11 @@ -import itertools import os import re import shutil -import sys -import xml.etree.cElementTree as cET from copy import deepcopy from xml.etree import ElementTree as ET import openpype.api as openpype import qargparse -import six from openpype import style from openpype.pipeline import LegacyCreator, LoaderPlugin from Qt import QtCore, QtWidgets @@ -740,6 +736,7 @@ class OpenClipSolver(flib.MediaInfoFile): def _create_new_open_clip(self): self.log.info("Building new openClip") + self.log.debug(">> self.clip_data: {}".format(self.clip_data)) # clip data comming from MediaInfoFile tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 25be310d01..0b9c9ce817 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -7,13 +7,10 @@ import json import logging import opentimelineio as otio from . import utils -from openpype.hosts.flame.api import MediaInfoFile import flame from pprint import pformat -reload(utils) # noqa - log = logging.getLogger(__name__) @@ -331,6 +328,8 @@ def create_otio_reference(clip_data, fps=None): def create_otio_clip(clip_data): + from openpype.hosts.flame.api import MediaInfoFile + segment = clip_data["PySegment"] # calculate source in From 9256e022909bb363d8e4cf4be532248363579d40 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 13:09:29 +0200 Subject: [PATCH 247/337] flame: setter getter error --- openpype/hosts/flame/api/lib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 6d93018fef..d68dd2a886 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -704,7 +704,7 @@ def maintained_temp_file_path(suffix=None): # Store dumped json to temporary file temporary_file = tempfile.mktemp( suffix=_suffix, prefix="flame_maintained_") - yield temporary_file.name.replace("\\", "/") + yield temporary_file.replace("\\", "/") except IOError as _error: raise IOError( @@ -787,7 +787,7 @@ class MediaInfoFile: # get all time related data and assign them self._get_time_info_from_origin(xml_data) - self.set_clip_data(xml_data) + self.clip_data = xml_data @property def clip_data(self): @@ -895,17 +895,17 @@ class MediaInfoFile: # start frame out_feed_nb_ticks_obj = out_feed.find( 'startTimecode/nbTicks') - self.start_frame(out_feed_nb_ticks_obj.text) + self.start_frame = out_feed_nb_ticks_obj.text # fps out_feed_fps_obj = out_feed.find( 'startTimecode/rate') - self.fps(out_feed_fps_obj.text) + self.fps = out_feed_fps_obj.text # drop frame mode out_feed_drop_mode_obj = out_feed.find( 'startTimecode/dropMode') - self.drop_mode(out_feed_drop_mode_obj.text) + self.drop_mode = out_feed_drop_mode_obj.text break else: continue From 3f9b06139d44448416deae9d251dabffdcfb9506 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 14:13:43 +0200 Subject: [PATCH 248/337] flame: fix getroot --- openpype/hosts/flame/api/lib.py | 2 +- openpype/hosts/flame/api/plugin.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index d68dd2a886..6ee0eb6a82 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -739,7 +739,7 @@ def get_batch_group_from_desktop(name): return bgroup -class MediaInfoFile: +class MediaInfoFile(object): """Class to get media info file clip data Raises: diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ab74bb4605..6136c4922c 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -687,7 +687,7 @@ class OpenClipSolver(flib.MediaInfoFile): feed_path = feed_data.pop("path") # initialize parent class - super(OpenClipSolver).__init__( + super(OpenClipSolver, self).__init__( feed_path, **feed_data ) @@ -756,7 +756,7 @@ class OpenClipSolver(flib.MediaInfoFile): xml_new_version.set('uid', self.feed_version_name) xml_new_version.set('type', 'version') - xml_data = self._fix_xml_data(self.clip_data) + xml_data = self._clear_handler(self.clip_data) self.log.info("Adding feed version: {}".format(self.feed_basename)) self.write_clip_data_to_file(self.out_file, xml_data) @@ -823,7 +823,7 @@ class OpenClipSolver(flib.MediaInfoFile): "version", {"type": "version", "uid": self.feed_version_name}) out_xml_versions_obj.insert(0, new_version_obj) - xml_data = self._fix_xml_data(out_xml) + xml_data = self._clear_handler(out_xml) # fist create backup self._create_openclip_backup_file(self.out_file) @@ -844,11 +844,6 @@ class OpenClipSolver(flib.MediaInfoFile): "Not appending file as it already is in .clip file") return True - def _fix_xml_data(self, xml_data): - xml_root = xml_data.getroot() - self._clear_handler(xml_root) - return xml_root - def _create_openclip_backup_file(self, file): bck_file = "{}.bak".format(file) # if backup does not exist From 9a8c41371bc3c7a7c3be37e655021005547b9a30 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 15:12:16 +0200 Subject: [PATCH 249/337] use current project for query asset --- openpype/tools/utils/lib.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 422d0f5389..efaf671915 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -409,6 +409,7 @@ class FamilyConfigCache: project_name = os.environ.get("AVALON_PROJECT") asset_name = os.environ.get("AVALON_ASSET") task_name = os.environ.get("AVALON_TASK") + host_name = os.environ.get("AVALON_APP") if not all((project_name, asset_name, task_name)): return @@ -422,15 +423,18 @@ class FamilyConfigCache: ["family_filter_profiles"] ) if profiles: - asset_doc = self.dbcon.find_one( + # Make sure connection is installed + # - accessing attribute which does not have auto-install + self.dbcon.install() + asset_doc = self.dbcon.database[project_name].find_one( {"type": "asset", "name": asset_name}, {"data.tasks": True} - ) + ) or {} tasks_info = asset_doc.get("data", {}).get("tasks") or {} task_type = tasks_info.get(task_name, {}).get("type") profiles_filter = { "task_types": task_type, - "hosts": os.environ["AVALON_APP"] + "hosts": host_name } matching_item = filter_profiles(profiles, profiles_filter) From 8420a164c2c5e7537c584f30e1e3b8469417cacf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 15:13:05 +0200 Subject: [PATCH 250/337] flame: temp reverse commit bfae95a8475981e014561c1eec8b9b54de5a8424 --- openpype/hosts/flame/api/plugin.py | 176 +++++++++++++++++++++++------ 1 file changed, 139 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 6136c4922c..bd0f9f1a81 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,11 +1,15 @@ +import itertools import os import re import shutil +import sys +import xml.etree.cElementTree as cET from copy import deepcopy from xml.etree import ElementTree as ET import openpype.api as openpype import qargparse +import six from openpype import style from openpype.pipeline import LegacyCreator, LoaderPlugin from Qt import QtCore, QtWidgets @@ -675,39 +679,53 @@ class ClipLoader(LoaderPlugin): ] -class OpenClipSolver(flib.MediaInfoFile): +# TODO: inheritance from flame.api.lib.MediaInfoFile +class OpenClipSolver: + media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" + tmp_name = "_tmp.clip" + tmp_file = None create_new_clip = False + out_feed_nb_ticks = None + out_feed_fps = None + out_feed_drop_mode = None + log = log def __init__(self, openclip_file_path, feed_data): - self.out_file = openclip_file_path + # test if media script paht exists + self._validate_media_script_path() # new feed variables: - feed_path = feed_data.pop("path") - - # initialize parent class - super(OpenClipSolver, self).__init__( - feed_path, - **feed_data - ) - - # get logger if any - if feed_data.get("logger"): - self.log = feed_data["logger"] - - # get other metadata + feed_path = feed_data["path"] self.feed_version_name = feed_data["version"] self.feed_colorspace = feed_data.get("colorspace") + if feed_data.get("logger"): + self.log = feed_data["logger"] + # derivate other feed variables self.feed_basename = os.path.basename(feed_path) self.feed_dir = os.path.dirname(feed_path) self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - if not self._is_valid_tmp_file(self.out_file): + if not self._is_valid_tmp_file(openclip_file_path): + # openclip does not exist yet and will be created + self.tmp_file = self.out_file = openclip_file_path self.create_new_clip = True + else: + # update already created clip + # output a temp file + self.out_file = openclip_file_path + self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) + + # remove previously generated temp files + # it will be regenerated + self._clear_tmp_file() + + self.log.info("Temp File: {}".format(self.tmp_file)) + def _is_valid_tmp_file(self, file): # check if file exists if os.path.isfile(file): @@ -722,6 +740,7 @@ class OpenClipSolver(flib.MediaInfoFile): return False def make(self): + self._generate_media_info_file() if self.create_new_clip: # New openClip @@ -729,6 +748,58 @@ class OpenClipSolver(flib.MediaInfoFile): else: self._update_open_clip() + def _validate_media_script_path(self): + if not os.path.isfile(self.media_script_path): + raise IOError("Media Scirpt does not exist: `{}`".format( + self.media_script_path)) + + def _generate_media_info_file(self): + # Create cmd arguments for gettig xml file info file + cmd_args = [ + self.media_script_path, + "-e", self.feed_ext, + "-o", self.tmp_file, + self.feed_dir + ] + + # execute creation of clip xml template data + try: + openpype.run_subprocess(cmd_args) + self._make_single_clip_media_info() + except TypeError: + self.log.error("Error creating self.tmp_file") + six.reraise(*sys.exc_info()) + + def _make_single_clip_media_info(self): + with open(self.tmp_file) as f: + lines = f.readlines() + _added_root = itertools.chain( + "", deepcopy(lines)[1:], "") + new_root = ET.fromstringlist(_added_root) + + # find the clip which is matching to my input name + xml_clips = new_root.findall("clip") + matching_clip = None + for xml_clip in xml_clips: + if xml_clip.find("name").text in self.feed_basename: + matching_clip = xml_clip + + if matching_clip is None: + # return warning there is missing clip + raise ET.ParseError( + "Missing clip in `{}`. Available clips {}".format( + self.feed_basename, [ + xml_clip.find("name").text + for xml_clip in xml_clips + ] + )) + + self._write_result_xml_to_file(self.tmp_file, matching_clip) + + def _clear_tmp_file(self): + if os.path.isfile(self.tmp_file): + os.remove(self.tmp_file) + def _clear_handler(self, xml_object): for handler in xml_object.findall("./handler"): self.log.debug("Handler found") @@ -736,10 +807,10 @@ class OpenClipSolver(flib.MediaInfoFile): def _create_new_open_clip(self): self.log.info("Building new openClip") - self.log.debug(">> self.clip_data: {}".format(self.clip_data)) - # clip data comming from MediaInfoFile - tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') + tmp_xml = ET.parse(self.tmp_file) + + tmp_xml_feeds = tmp_xml.find('tracks/track/feeds') tmp_xml_feeds.set('currentVersion', self.feed_version_name) for tmp_feed in tmp_xml_feeds: tmp_feed.set('vuid', self.feed_version_name) @@ -750,47 +821,46 @@ class OpenClipSolver(flib.MediaInfoFile): self._clear_handler(tmp_feed) - tmp_xml_versions_obj = self.clip_data.find('versions') + tmp_xml_versions_obj = tmp_xml.find('versions') tmp_xml_versions_obj.set('currentVersion', self.feed_version_name) for xml_new_version in tmp_xml_versions_obj: xml_new_version.set('uid', self.feed_version_name) xml_new_version.set('type', 'version') - xml_data = self._clear_handler(self.clip_data) + xml_data = self._fix_xml_data(tmp_xml) self.log.info("Adding feed version: {}".format(self.feed_basename)) - self.write_clip_data_to_file(self.out_file, xml_data) + self._write_result_xml_to_file(self.out_file, xml_data) + + self.log.info("openClip Updated: {}".format(self.tmp_file)) def _update_open_clip(self): self.log.info("Updating openClip ..") out_xml = ET.parse(self.out_file) + tmp_xml = ET.parse(self.tmp_file) self.log.debug(">> out_xml: {}".format(out_xml)) - self.log.debug(">> self.clip_data: {}".format(self.clip_data)) + self.log.debug(">> tmp_xml: {}".format(tmp_xml)) # Get new feed from tmp file - tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed') + tmp_xml_feed = tmp_xml.find('tracks/track/feeds/feed') self._clear_handler(tmp_xml_feed) + self._get_time_info_from_origin(out_xml) - # update fps from MediaInfoFile class - if self.fps: + if self.out_feed_fps: tmp_feed_fps_obj = tmp_xml_feed.find( "startTimecode/rate") - tmp_feed_fps_obj.text = self.fps - - # update start_frame from MediaInfoFile class - if self.start_frame: + tmp_feed_fps_obj.text = self.out_feed_fps + if self.out_feed_nb_ticks: tmp_feed_nb_ticks_obj = tmp_xml_feed.find( "startTimecode/nbTicks") - tmp_feed_nb_ticks_obj.text = self.start_frame - - # update drop_mode from MediaInfoFile class - if self.drop_mode: + tmp_feed_nb_ticks_obj.text = self.out_feed_nb_ticks + if self.out_feed_drop_mode: tmp_feed_drop_mode_obj = tmp_xml_feed.find( "startTimecode/dropMode") - tmp_feed_drop_mode_obj.text = self.drop_mode + tmp_feed_drop_mode_obj.text = self.out_feed_drop_mode new_path_obj = tmp_xml_feed.find( "spans/span/path") @@ -823,7 +893,7 @@ class OpenClipSolver(flib.MediaInfoFile): "version", {"type": "version", "uid": self.feed_version_name}) out_xml_versions_obj.insert(0, new_version_obj) - xml_data = self._clear_handler(out_xml) + xml_data = self._fix_xml_data(out_xml) # fist create backup self._create_openclip_backup_file(self.out_file) @@ -831,10 +901,31 @@ class OpenClipSolver(flib.MediaInfoFile): self.log.info("Adding feed version: {}".format( self.feed_version_name)) - self.write_clip_data_to_file(self.out_file, xml_data) + self._write_result_xml_to_file(self.out_file, xml_data) self.log.info("openClip Updated: {}".format(self.out_file)) + self._clear_tmp_file() + + def _get_time_info_from_origin(self, xml_data): + try: + for out_track in xml_data.iter('track'): + for out_feed in out_track.iter('feed'): + out_feed_nb_ticks_obj = out_feed.find( + 'startTimecode/nbTicks') + self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text + out_feed_fps_obj = out_feed.find( + 'startTimecode/rate') + self.out_feed_fps = out_feed_fps_obj.text + out_feed_drop_mode_obj = out_feed.find( + 'startTimecode/dropMode') + self.out_feed_drop_mode = out_feed_drop_mode_obj.text + break + else: + continue + except Exception as msg: + self.log.warning(msg) + def _feed_exists(self, xml_data, path): # loop all available feed paths and check if # the path is not already in file @@ -844,6 +935,17 @@ class OpenClipSolver(flib.MediaInfoFile): "Not appending file as it already is in .clip file") return True + def _fix_xml_data(self, xml_data): + xml_root = xml_data.getroot() + self._clear_handler(xml_root) + return xml_root + + def _write_result_xml_to_file(self, file, xml_data): + # save it as new file + tree = cET.ElementTree(xml_data) + tree.write(file, xml_declaration=True, + method='xml', encoding='UTF-8') + def _create_openclip_backup_file(self, file): bck_file = "{}.bak".format(file) # if backup does not exist From 2a618081fdf9a6f81e6b63b8c020fefa84a30586 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 15:58:59 +0200 Subject: [PATCH 251/337] added settings for white list of environment variables --- .../settings/defaults/system_settings/general.json | 1 + .../entities/schemas/system_schema/schema_general.json | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index 5a3e39e5b6..e1785f8709 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -12,6 +12,7 @@ "linux": [], "darwin": [] }, + "local_env_white_list": [], "openpype_path": { "windows": [], "darwin": [], diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 6306317df8..997404b2e6 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -110,6 +110,16 @@ { "type": "splitter" }, + { + "type": "list", + "key": "local_env_white_list", + "label": "White list of local environment variables", + "use_label_wrap": true, + "object_type": "text" + }, + { + "type": "splitter" + }, { "type": "collapsible-wrap", "label": "OpenPype deployment control", From e0db71ba07edafe3e23caf01c77d0101df47c337 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 16:57:01 +0200 Subject: [PATCH 252/337] Refactor - changed to defaultdict --- openpype/modules/sync_server/sync_server_module.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index ebdcffdab7..596aeb8b39 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -4,7 +4,7 @@ from datetime import datetime import threading import platform import copy -from collections import deque +from collections import deque, defaultdict from avalon.api import AvalonMongoDB @@ -300,18 +300,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Returns: (dict): {'site': [alternative sites]...} """ - alt_site_pairs = {} + alt_site_pairs = defaultdict(list) for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) - if not alt_site_pairs.get(site_name): - alt_site_pairs[site_name] = [] - alt_site_pairs[site_name].extend(alt_sites) for alt_site in alt_sites: - if not alt_site_pairs.get(alt_site): - alt_site_pairs[alt_site] = [] - alt_site_pairs[alt_site].extend([site_name]) + alt_site_pairs[alt_site].append(site_name) for site_name, alt_sites in alt_site_pairs.items(): sites_queue = deque(alt_sites) From 29dca65202d45a79e66c619b95d3408e227a9c05 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 16:59:34 +0200 Subject: [PATCH 253/337] Refactor - changed to defaultdict --- openpype/plugins/publish/integrate_new.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3eca460ba3..5dcbb8fabd 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -8,7 +8,7 @@ import errno import six import re import shutil -from collections import deque +from collections import deque, defaultdict from bson.objectid import ObjectId from pymongo import DeleteOne, InsertOne @@ -1185,21 +1185,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Returns: (dict): {'site': [alternative sites]...} """ - alt_site_pairs = {} + alt_site_pairs = defaultdict(list) for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) - if not alt_site_pairs.get(site_name): - alt_site_pairs[site_name] = [] - alt_site_pairs[site_name].extend(alt_sites) for alt_site in alt_sites: - if not alt_site_pairs.get(alt_site): - alt_site_pairs[alt_site] = [] - alt_site_pairs[alt_site].extend([site_name]) + alt_site_pairs[alt_site].append(site_name) - # transitive relationship, eg site is alternative to another which is - # alternative to nex site for site_name, alt_sites in alt_site_pairs.items(): sites_queue = deque(alt_sites) while sites_queue: From 0ab101a5b12b755f07551d32203779103ee059f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 17:11:16 +0200 Subject: [PATCH 254/337] added widgets for environments settings --- .../settings/local_settings/constants.py | 1 + .../local_settings/environments_widget.py | 80 +++++++++++++++++++ .../tools/settings/local_settings/window.py | 31 ++++++- 3 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 openpype/tools/settings/local_settings/environments_widget.py diff --git a/openpype/tools/settings/local_settings/constants.py b/openpype/tools/settings/local_settings/constants.py index 1836c579af..16f87b6f05 100644 --- a/openpype/tools/settings/local_settings/constants.py +++ b/openpype/tools/settings/local_settings/constants.py @@ -9,6 +9,7 @@ LABEL_DISCARD_CHANGES = "Discard changes" # TODO move to settings constants LOCAL_GENERAL_KEY = "general" LOCAL_PROJECTS_KEY = "projects" +LOCAL_ENV_KEY = "environments" LOCAL_APPS_KEY = "applications" # Roots key constant diff --git a/openpype/tools/settings/local_settings/environments_widget.py b/openpype/tools/settings/local_settings/environments_widget.py new file mode 100644 index 0000000000..70631e8f29 --- /dev/null +++ b/openpype/tools/settings/local_settings/environments_widget.py @@ -0,0 +1,80 @@ +from Qt import QtWidgets + +from openpype.tools.utils import PlaceholderLineEdit + + +class LocalEnvironmentsWidgets(QtWidgets.QWidget): + def __init__(self, system_settings_entity, parent): + super(LocalEnvironmentsWidgets, self).__init__(parent) + + self._widgets_by_env_key = {} + self.system_settings_entity = system_settings_entity + + content_widget = QtWidgets.QWidget(self) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self._layout = layout + self._content_layout = content_layout + self._content_widget = content_widget + + def _clear_layout(self, layout): + while layout.count() > 0: + item = layout.itemAt(0) + widget = item.widget() + layout.removeItem(item) + if widget is not None: + widget.setVisible(False) + widget.deleteLater() + + def _reset_env_widgets(self): + self._clear_layout(self._content_layout) + self._clear_layout(self._layout) + + content_widget = QtWidgets.QWidget(self) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + white_list_entity = ( + self.system_settings_entity["general"]["local_env_white_list"] + ) + + for row, item in enumerate(white_list_entity): + key = item.value + label_widget = QtWidgets.QLabel(key, self) + input_widget = PlaceholderLineEdit(self) + input_widget.setPlaceholderText("< Keep studio value >") + + content_layout.addWidget(label_widget, row, 0) + content_layout.addWidget(input_widget, row, 1) + + self._widgets_by_env_key[key] = input_widget + + self._layout.addWidget(content_widget, 1) + + self._content_layout = content_layout + self._content_widget = content_widget + + def update_local_settings(self, value): + if not value: + value = {} + + self._reset_env_widgets() + + for env_key, widget in self._widgets_by_env_key.items(): + env_value = value.get(env_key) or "" + widget.setText(env_value) + + def settings_value(self): + output = {} + for env_key, widget in self._widgets_by_env_key.items(): + value = widget.text() + if value: + output[env_key] = value + if not output: + return None + return output diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index fb47e69a17..4db0e01476 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -25,11 +25,13 @@ from .experimental_widget import ( LOCAL_EXPERIMENTAL_KEY ) from .apps_widget import LocalApplicationsWidgets +from .environments_widget import LocalEnvironmentsWidgets from .projects_widget import ProjectSettingsWidget from .constants import ( LOCAL_GENERAL_KEY, LOCAL_PROJECTS_KEY, + LOCAL_ENV_KEY, LOCAL_APPS_KEY ) @@ -49,18 +51,20 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.pype_mongo_widget = None self.general_widget = None self.experimental_widget = None + self.envs_widget = None self.apps_widget = None self.projects_widget = None - self._create_pype_mongo_ui() + self._create_mongo_url_ui() self._create_general_ui() self._create_experimental_ui() + self._create_environments_ui() self._create_app_ui() self._create_project_ui() self.main_layout.addStretch(1) - def _create_pype_mongo_ui(self): + def _create_mongo_url_ui(self): pype_mongo_expand_widget = ExpandingWidget("OpenPype Mongo URL", self) pype_mongo_content = QtWidgets.QWidget(self) pype_mongo_layout = QtWidgets.QVBoxLayout(pype_mongo_content) @@ -110,6 +114,22 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.experimental_widget = experimental_widget + def _create_environments_ui(self): + envs_expand_widget = ExpandingWidget("Environments", self) + envs_content = QtWidgets.QWidget(self) + envs_layout = QtWidgets.QVBoxLayout(envs_content) + envs_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + envs_expand_widget.set_content_widget(envs_content) + + envs_widget = LocalEnvironmentsWidgets( + self.system_settings, envs_content + ) + envs_layout.addWidget(envs_widget) + + self.main_layout.addWidget(envs_expand_widget) + + self.envs_widget = envs_widget + def _create_app_ui(self): # Applications app_expand_widget = ExpandingWidget("Applications", self) @@ -154,6 +174,9 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.general_widget.update_local_settings( value.get(LOCAL_GENERAL_KEY) ) + self.envs_widget.update_local_settings( + value.get(LOCAL_ENV_KEY) + ) self.app_widget.update_local_settings( value.get(LOCAL_APPS_KEY) ) @@ -170,6 +193,10 @@ class LocalSettingsWidget(QtWidgets.QWidget): if general_value: output[LOCAL_GENERAL_KEY] = general_value + envs_value = self.envs_widget.settings_value() + if envs_value: + output[LOCAL_ENV_KEY] = envs_value + app_value = self.app_widget.settings_value() if app_value: output[LOCAL_APPS_KEY] = app_value From d7262446f82d6adc08650853c7649cf133d0895c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 17:11:48 +0200 Subject: [PATCH 255/337] use local settings to override environments during launching of application --- openpype/lib/applications.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 5821c863d7..049658a548 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -13,7 +13,8 @@ import six from openpype.settings import ( get_system_settings, - get_project_settings + get_project_settings, + get_local_settings ) from openpype.settings.constants import ( METADATA_KEYS, @@ -1272,6 +1273,9 @@ class EnvironmentPrepData(dict): if data.get("env") is None: data["env"] = os.environ.copy() + if "system_settings" not in data: + data["system_settings"] = get_system_settings() + super(EnvironmentPrepData, self).__init__(data) @@ -1434,6 +1438,19 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): ) ) + # Use environments from local settings + filtered_local_envs = {} + system_settings = data["system_settings"] + whitelist_envs = system_settings["general"].get("local_env_white_list") + if whitelist_envs: + local_settings = get_local_settings() + local_envs = local_settings.get("environments") or {} + filtered_local_envs = { + key: value + for key, value in local_envs.items() + if key in whitelist_envs + } + env_values = {} for _env_values in environments: if not _env_values: @@ -1441,6 +1458,10 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): # Choose right platform tool_env = parse_environments(_env_values, env_group) + for key, value in filtered_local_envs.items(): + if key in tool_env: + tool_env[key] = value + # Merge dictionaries env_values = _merge_env(tool_env, env_values) @@ -1611,7 +1632,6 @@ def _prepare_last_workfile(data, workdir): result will be stored. workdir (str): Path to folder where workfiles should be stored. """ - import avalon.api from openpype.pipeline import HOST_WORKFILE_EXTENSIONS log = data["log"] From 483d97f71b4655a6c0391b52eedf2b5952780a35 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Apr 2022 17:37:01 +0200 Subject: [PATCH 256/337] OP-3073 - fix removed wrong hardcoded family --- .../webpublisher/plugins/publish/collect_published_files.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 65cef14703..56b2ef6e20 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -209,7 +209,6 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): msg = "No family found for combination of " +\ "task_type: {}, is_sequence:{}, extension: {}".format( task_type, is_sequence, extension) - found_family = "render" assert found_family, msg return (found_family, From 99c3cb50330decc5cdb8666b74eb6a04dc7577fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 17:48:58 +0200 Subject: [PATCH 257/337] fix dicionary loop --- .../modules/ftrack/plugins/publish/integrate_ftrack_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 7ebf807f55..650c59fae8 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -263,7 +263,9 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): self.log.info("Creating asset types with short names: {}".format( ", ".join(asset_type_names_by_missing_shorts.keys()) )) - for missing_short, type_name in asset_type_names_by_missing_shorts: + for missing_short, type_name in ( + asset_type_names_by_missing_shorts.items() + ): # Use short for name if name is not defined if not type_name: type_name = missing_short From d4182e5cea64f6374b2a8ef443215f678b0c40bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 18:02:48 +0200 Subject: [PATCH 258/337] dev_test_plugin --- openpype/hosts/flame/api/test_plugin.py | 428 ++++++++++++++++++++++++ openpype/version.py | 9 +- 2 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/flame/api/test_plugin.py diff --git a/openpype/hosts/flame/api/test_plugin.py b/openpype/hosts/flame/api/test_plugin.py new file mode 100644 index 0000000000..d75819a9e3 --- /dev/null +++ b/openpype/hosts/flame/api/test_plugin.py @@ -0,0 +1,428 @@ +import os +import tempfile +import itertools +import contextlib +import xml.etree.cElementTree as cET +from copy import deepcopy +import shutil +from xml.etree import ElementTree as ET + +import openpype.api as openpype + +import logging + +log = logging.getLogger(__name__) + + +@contextlib.contextmanager +def maintained_temp_file_path(suffix=None): + _suffix = suffix or "" + + try: + # Store dumped json to temporary file + temporary_file = tempfile.mktemp( + suffix=_suffix, prefix="flame_maintained_") + yield temporary_file.replace("\\", "/") + + except IOError as _error: + raise IOError( + "Not able to create temp json file: {}".format(_error)) + + finally: + # Remove the temporary json + os.remove(temporary_file) + + +class MediaInfoFile(object): + """Class to get media info file clip data + + Raises: + IOError: MEDIA_SCRIPT_PATH path doesn't exists + TypeError: Not able to generate clip xml data file + ET.ParseError: Missing clip in xml clip data + IOError: Not able to save xml clip data to file + + Attributes: + str: `MEDIA_SCRIPT_PATH` path to flame binary + logging.Logger: `log` logger + + TODO: add method for getting metadata to dict + """ + MEDIA_SCRIPT_PATH = "/opt/Autodesk/mio/current/dl_get_media_info" + + log = log + + _clip_data = None + _start_frame = None + _fps = None + _drop_mode = None + + def __init__(self, path, **kwargs): + + # replace log if any + if kwargs.get("logger"): + self.log = kwargs["logger"] + + # test if `dl_get_media_info` paht exists + self._validate_media_script_path() + + # derivate other feed variables + self.feed_basename = os.path.basename(path) + self.feed_dir = os.path.dirname(path) + self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() + + with maintained_temp_file_path(".clip") as tmp_path: + self.log.info("Temp File: {}".format(tmp_path)) + self._generate_media_info_file(tmp_path) + + # get clip data and make them single if there is multiple + # clips data + xml_data = self._make_single_clip_media_info(tmp_path) + self.log.info("xml_data: {}".format(xml_data)) + self.log.info("type: {}".format(type(xml_data))) + + # get all time related data and assign them + self._get_time_info_from_origin(xml_data) + self.log.info("start_frame: {}".format(self.start_frame)) + self.log.info("fps: {}".format(self.fps)) + self.log.info("drop frame: {}".format(self.drop_mode)) + self.clip_data = xml_data + + @property + def clip_data(self): + """Clip's xml clip data + + Returns: + xml.etree.ElementTree: xml data + """ + return self._clip_data + + @clip_data.setter + def clip_data(self, data): + self._clip_data = data + + @property + def start_frame(self): + """ Clip's starting frame found in timecode + + Returns: + int: number of frames + """ + return self._start_frame + + @start_frame.setter + def start_frame(self, number): + self._start_frame = int(number) + + @property + def fps(self): + """ Clip's frame rate + + Returns: + float: frame rate + """ + return self._fps + + @fps.setter + def fps(self, fl_number): + self._fps = float(fl_number) + + @property + def drop_mode(self): + """ Clip's drop frame mode + + Returns: + str: drop frame flag + """ + return self._drop_mode + + @drop_mode.setter + def drop_mode(self, text): + self._drop_mode = str(text) + + def _validate_media_script_path(self): + if not os.path.isfile(self.MEDIA_SCRIPT_PATH): + raise IOError("Media Scirpt does not exist: `{}`".format( + self.MEDIA_SCRIPT_PATH)) + + def _generate_media_info_file(self, fpath): + # Create cmd arguments for gettig xml file info file + cmd_args = [ + self.MEDIA_SCRIPT_PATH, + "-e", self.feed_ext, + "-o", fpath, + self.feed_dir + ] + + try: + # execute creation of clip xml template data + openpype.run_subprocess(cmd_args) + except TypeError as error: + raise TypeError( + "Error creating `{}` due: {}".format(fpath, error)) + + def _make_single_clip_media_info(self, fpath): + with open(fpath) as f: + lines = f.readlines() + _added_root = itertools.chain( + "", deepcopy(lines)[1:], "") + new_root = ET.fromstringlist(_added_root) + + # find the clip which is matching to my input name + xml_clips = new_root.findall("clip") + matching_clip = None + for xml_clip in xml_clips: + if xml_clip.find("name").text in self.feed_basename: + matching_clip = xml_clip + + if matching_clip is None: + # return warning there is missing clip + raise ET.ParseError( + "Missing clip in `{}`. Available clips {}".format( + self.feed_basename, [ + xml_clip.find("name").text + for xml_clip in xml_clips + ] + )) + + return matching_clip + + def _get_time_info_from_origin(self, xml_data): + try: + 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') + self.start_frame = out_feed_nb_ticks_obj.text + + # fps + out_feed_fps_obj = out_feed.find( + 'startTimecode/rate') + self.fps = out_feed_fps_obj.text + + # drop frame mode + out_feed_drop_mode_obj = out_feed.find( + 'startTimecode/dropMode') + self.drop_mode = out_feed_drop_mode_obj.text + break + else: + continue + except Exception as msg: + self.log.warning(msg) + + @staticmethod + def write_clip_data_to_file(fpath, xml_data): + log.info(">>> type of xml_data: {}".format(type(xml_data))) + if isinstance(xml_data, ET.ElementTree): + xml_data = xml_data.getroot() + try: + # save it as new file + tree = cET.ElementTree(xml_data) + tree.write( + fpath, xml_declaration=True, + method='xml', encoding='UTF-8' + ) + except IOError as error: + raise IOError( + "Not able to write data to file: {}".format(error)) + + +class OpenClipSolver(MediaInfoFile): + create_new_clip = False + + log = log + + def __init__(self, openclip_file_path, feed_data): + self.out_file = openclip_file_path + + # new feed variables: + feed_path = feed_data.pop("path") + + # initialize parent class + super(OpenClipSolver, self).__init__( + feed_path, + **feed_data + ) + + # get other metadata + self.feed_version_name = feed_data["version"] + self.feed_colorspace = feed_data.get("colorspace") + self.log.info("feed_version_name: {}".format(self.feed_version_name)) + + # derivate other feed variables + self.feed_basename = os.path.basename(feed_path) + self.feed_dir = os.path.dirname(feed_path) + self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() + self.log.info("feed_ext: {}".format(self.feed_ext)) + self.log.info("out_file: {}".format(self.out_file)) + if not self._is_valid_tmp_file(self.out_file): + self.create_new_clip = True + + def _is_valid_tmp_file(self, file): + # check if file exists + if os.path.isfile(file): + # test also if file is not empty + with open(file) as f: + lines = f.readlines() + if len(lines) > 2: + return True + + # file is probably corrupted + os.remove(file) + return False + + def make(self): + + if self.create_new_clip: + # New openClip + self._create_new_open_clip() + else: + self._update_open_clip() + + def _clear_handler(self, xml_object): + for handler in xml_object.findall("./handler"): + self.log.info("Handler found") + xml_object.remove(handler) + + def _create_new_open_clip(self): + self.log.info("Building new openClip") + self.log.info(">> self.clip_data: {}".format(self.clip_data)) + + # clip data comming from MediaInfoFile + tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') + tmp_xml_feeds.set('currentVersion', self.feed_version_name) + for tmp_feed in tmp_xml_feeds: + tmp_feed.set('vuid', self.feed_version_name) + + # add colorspace if any is set + if self.feed_colorspace: + self._add_colorspace(tmp_feed, self.feed_colorspace) + + self._clear_handler(tmp_feed) + + tmp_xml_versions_obj = self.clip_data.find('versions') + tmp_xml_versions_obj.set('currentVersion', self.feed_version_name) + for xml_new_version in tmp_xml_versions_obj: + xml_new_version.set('uid', self.feed_version_name) + xml_new_version.set('type', 'version') + + self._clear_handler(self.clip_data) + self.log.info("Adding feed version: {}".format(self.feed_basename)) + + self.write_clip_data_to_file(self.out_file, self.clip_data) + + def _update_open_clip(self): + self.log.info("Updating openClip ..") + + out_xml = ET.parse(self.out_file) + + self.log.info(">> out_xml: {}".format(out_xml)) + self.log.info(">> self.clip_data: {}".format(self.clip_data)) + + # Get new feed from tmp file + tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed') + + self._clear_handler(tmp_xml_feed) + + # update fps from MediaInfoFile class + if self.fps: + tmp_feed_fps_obj = tmp_xml_feed.find( + "startTimecode/rate") + tmp_feed_fps_obj.text = str(self.fps) + + # update start_frame from MediaInfoFile class + if self.start_frame: + tmp_feed_nb_ticks_obj = tmp_xml_feed.find( + "startTimecode/nbTicks") + tmp_feed_nb_ticks_obj.text = str(self.start_frame) + + # update drop_mode from MediaInfoFile class + if self.drop_mode: + tmp_feed_drop_mode_obj = tmp_xml_feed.find( + "startTimecode/dropMode") + tmp_feed_drop_mode_obj.text = str(self.drop_mode) + + new_path_obj = tmp_xml_feed.find( + "spans/span/path") + new_path = new_path_obj.text + + feed_added = False + if not self._feed_exists(out_xml, new_path): + tmp_xml_feed.set('vuid', self.feed_version_name) + # Append new temp file feed to .clip source out xml + out_track = out_xml.find("tracks/track") + # add colorspace if any is set + if self.feed_colorspace: + self._add_colorspace(tmp_xml_feed, self.feed_colorspace) + + out_feeds = out_track.find('feeds') + out_feeds.set('currentVersion', self.feed_version_name) + out_feeds.append(tmp_xml_feed) + + self.log.info( + "Appending new feed: {}".format( + self.feed_version_name)) + feed_added = True + + if feed_added: + # Append vUID to versions + out_xml_versions_obj = out_xml.find('versions') + out_xml_versions_obj.set( + 'currentVersion', self.feed_version_name) + new_version_obj = ET.Element( + "version", {"type": "version", "uid": self.feed_version_name}) + out_xml_versions_obj.insert(0, new_version_obj) + + self._clear_handler(out_xml) + + # fist create backup + self._create_openclip_backup_file(self.out_file) + + self.log.info("Adding feed version: {}".format( + self.feed_version_name)) + + self.write_clip_data_to_file(self.out_file, out_xml) + + self.log.info("openClip Updated: {}".format(self.out_file)) + + def _feed_exists(self, xml_data, path): + # loop all available feed paths and check if + # the path is not already in file + for src_path in xml_data.iter('path'): + if path == src_path.text: + self.log.warning( + "Not appending file as it already is in .clip file") + return True + + def _create_openclip_backup_file(self, file): + bck_file = "{}.bak".format(file) + # if backup does not exist + if not os.path.isfile(bck_file): + shutil.copy2(file, bck_file) + else: + # in case it exists and is already multiplied + created = False + for _i in range(1, 99): + bck_file = "{name}.bak.{idx:0>2}".format( + name=file, + idx=_i) + # create numbered backup file + if not os.path.isfile(bck_file): + shutil.copy2(file, bck_file) + created = True + break + # in case numbered does not exists + if not created: + bck_file = "{}.bak.last".format(file) + shutil.copy2(file, bck_file) + + def _add_colorspace(self, feed_obj, profile_name): + feed_storage_obj = feed_obj.find("storageFormat") + feed_clr_obj = feed_storage_obj.find("colourSpace") + if feed_clr_obj is not None: + feed_clr_obj = ET.Element( + "colourSpace", {"type": "string"}) + feed_storage_obj.append(feed_clr_obj) + + feed_clr_obj.text = profile_name diff --git a/openpype/version.py b/openpype/version.py index 97aa585ca7..d447d27172 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,10 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.3" +__version__ = "3.9.3-nightly.1-upp220408" + + +''' +includes: + - Flame: integrate batch groups: + https://github.com/pypeclub/OpenPype/pull/2928 +''' From 06d2e898654b9aca82c9a8c979938cc44ee1f766 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 18:03:30 +0200 Subject: [PATCH 259/337] testing file --- openpype/hosts/flame/tests/flame_test.py | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 openpype/hosts/flame/tests/flame_test.py diff --git a/openpype/hosts/flame/tests/flame_test.py b/openpype/hosts/flame/tests/flame_test.py new file mode 100644 index 0000000000..402983eeba --- /dev/null +++ b/openpype/hosts/flame/tests/flame_test.py @@ -0,0 +1,30 @@ +from openpype.lib import import_filepath + +plugin = import_filepath( + "/Users/pype.club/code/openpype/openpype/hosts/flame/api/test_plugin.py") + +openclip_file_path = "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/test.clip" +# feed_datas = [ +# { +# "path": "/Users/pype.club/pype_club_root/OP02_VFX_demo/shots/a/a0000001/publish/plate/plateMain/v007/op02vfx_a0000001_plateMain_v007_exr16fpdwaaCl.0997.exr", +# "version": "v007" +# }, +# { +# "path": "/Users/pype.club/pype_club_root/OP02_VFX_demo/shots/a/a0000001/publish/plate/plateMain/v008/op02vfx_a0000001_plateMain_v008_exr16fpdwaaCl.0997.exr", +# "version": "v008" +# } +# ] + +feed_datas = [ + { + "path": "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/v001/file_name_v001.1001.exr", + "version": "v001" + }, + { + "path": "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/v002/file_name_v002.1001.exr", + "version": "v002" + } +] +for feed_data in feed_datas: + oclip = plugin.OpenClipSolver(openclip_file_path, feed_data) + oclip.make() From b9a7d810cc20949452a4e5e069497b08ca72f8ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 18:32:40 +0200 Subject: [PATCH 260/337] fix applying of env variables that are not defined by applications or tools --- openpype/lib/applications.py | 43 ++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 049658a548..07b91dda03 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1399,8 +1399,27 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): app = data["app"] log = data["log"] + source_env = data["env"].copy() - _add_python_version_paths(app, data["env"], log) + _add_python_version_paths(app, source_env, log) + + # Use environments from local settings + filtered_local_envs = {} + system_settings = data["system_settings"] + whitelist_envs = system_settings["general"].get("local_env_white_list") + if whitelist_envs: + local_settings = get_local_settings() + local_envs = local_settings.get("environments") or {} + filtered_local_envs = { + key: value + for key, value in local_envs.items() + if key in whitelist_envs + } + + # Apply local environment variables for already existing values + for key, value in filtered_local_envs.items(): + if key in source_env: + source_env[key] = value # `added_env_keys` has debug purpose added_env_keys = {app.group.name, app.name} @@ -1438,19 +1457,6 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): ) ) - # Use environments from local settings - filtered_local_envs = {} - system_settings = data["system_settings"] - whitelist_envs = system_settings["general"].get("local_env_white_list") - if whitelist_envs: - local_settings = get_local_settings() - local_envs = local_settings.get("environments") or {} - filtered_local_envs = { - key: value - for key, value in local_envs.items() - if key in whitelist_envs - } - env_values = {} for _env_values in environments: if not _env_values: @@ -1458,6 +1464,10 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): # Choose right platform tool_env = parse_environments(_env_values, env_group) + + # Apply local environment variables + # - must happen between all values because they may be used during + # merge for key, value in filtered_local_envs.items(): if key in tool_env: tool_env[key] = value @@ -1465,7 +1475,8 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): # Merge dictionaries env_values = _merge_env(tool_env, env_values) - merged_env = _merge_env(env_values, data["env"]) + merged_env = _merge_env(env_values, source_env) + loaded_env = acre.compute(merged_env, cleanup=False) final_env = None @@ -1485,7 +1496,7 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): if final_env is None: final_env = loaded_env - keys_to_remove = set(data["env"].keys()) - set(final_env.keys()) + keys_to_remove = set(source_env.keys()) - set(final_env.keys()) # Update env data["env"].update(final_env) From 2af6e7140ec391ace6dd310f7744e6da09b74583 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 18:51:55 +0200 Subject: [PATCH 261/337] changed label and added tooltip --- .../entities/schemas/system_schema/schema_general.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 997404b2e6..fcab4cd5d8 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -113,7 +113,8 @@ { "type": "list", "key": "local_env_white_list", - "label": "White list of local environment variables", + "label": "Local overrides of environment variable keys", + "tooltip": "Environment variable keys that can be changed per machine using Local settings UI.\nKey changes are applied only on applications and tools environments.", "use_label_wrap": true, "object_type": "text" }, From 9893903a5728bea368f033889bd352dd1854b1e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 8 Apr 2022 18:54:55 +0200 Subject: [PATCH 262/337] add label when there are not env keys to set --- .../local_settings/environments_widget.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/local_settings/environments_widget.py b/openpype/tools/settings/local_settings/environments_widget.py index 70631e8f29..14ca517851 100644 --- a/openpype/tools/settings/local_settings/environments_widget.py +++ b/openpype/tools/settings/local_settings/environments_widget.py @@ -37,12 +37,10 @@ class LocalEnvironmentsWidgets(QtWidgets.QWidget): content_widget = QtWidgets.QWidget(self) content_layout = QtWidgets.QGridLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setColumnStretch(0, 0) - content_layout.setColumnStretch(1, 1) white_list_entity = ( self.system_settings_entity["general"]["local_env_white_list"] ) - + row = -1 for row, item in enumerate(white_list_entity): key = item.value label_widget = QtWidgets.QLabel(key, self) @@ -54,6 +52,21 @@ class LocalEnvironmentsWidgets(QtWidgets.QWidget): self._widgets_by_env_key[key] = input_widget + if row < 0: + label_widget = QtWidgets.QLabel( + ( + "Your studio does not allow to change" + " Environment variables locally." + ), + self + ) + content_layout.addWidget(label_widget, 0, 0) + content_layout.setColumnStretch(0, 1) + + else: + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + self._layout.addWidget(content_widget, 1) self._content_layout = content_layout From a103eba505d9358c0c7058f614e36dd14b2edd4b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 19:03:54 +0200 Subject: [PATCH 263/337] flame: fixing OpenClipSolver --- openpype/hosts/flame/api/lib.py | 18 +- openpype/hosts/flame/api/plugin.py | 183 +++------- openpype/hosts/flame/api/test_plugin.py | 428 ----------------------- openpype/hosts/flame/tests/flame_test.py | 30 -- 4 files changed, 56 insertions(+), 603 deletions(-) delete mode 100644 openpype/hosts/flame/api/test_plugin.py delete mode 100644 openpype/hosts/flame/tests/flame_test.py diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 6ee0eb6a82..c7c444c1fb 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -784,9 +784,14 @@ class MediaInfoFile(object): # get clip data and make them single if there is multiple # clips data xml_data = self._make_single_clip_media_info(tmp_path) + self.log.debug("xml_data: {}".format(xml_data)) + self.log.debug("type: {}".format(type(xml_data))) # get all time related data and assign them self._get_time_info_from_origin(xml_data) + self.log.debug("start_frame: {}".format(self.start_frame)) + self.log.debug("fps: {}".format(self.fps)) + self.log.debug("drop frame: {}".format(self.drop_mode)) self.clip_data = xml_data @property @@ -913,10 +918,19 @@ class MediaInfoFile(object): self.log.warning(msg) @staticmethod - def write_clip_data_to_file(fpath, xml_data): + def write_clip_data_to_file(fpath, xml_element_data): + """ Write xml element of clip data to file + + Args: + fpath (string): file path + xml_element_data (xml.etree.ElementTree.Element): xml data + + Raises: + IOError: If data could not be written to file + """ try: # save it as new file - tree = cET.ElementTree(xml_data) + tree = cET.ElementTree(xml_element_data) tree.write( fpath, xml_declaration=True, method='xml', encoding='UTF-8' diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index bd0f9f1a81..42e6e19931 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,15 +1,11 @@ -import itertools import os import re import shutil -import sys -import xml.etree.cElementTree as cET from copy import deepcopy from xml.etree import ElementTree as ET import openpype.api as openpype import qargparse -import six from openpype import style from openpype.pipeline import LegacyCreator, LoaderPlugin from Qt import QtCore, QtWidgets @@ -658,8 +654,8 @@ class PublishableClip: # Publishing plugin functions -# Loader plugin functions +# Loader plugin functions class ClipLoader(LoaderPlugin): """A basic clip loader for Flame @@ -679,53 +675,37 @@ class ClipLoader(LoaderPlugin): ] -# TODO: inheritance from flame.api.lib.MediaInfoFile -class OpenClipSolver: - media_script_path = "/opt/Autodesk/mio/current/dl_get_media_info" - tmp_name = "_tmp.clip" - tmp_file = None +class OpenClipSolver(flib.MediaInfoFile): create_new_clip = False - out_feed_nb_ticks = None - out_feed_fps = None - out_feed_drop_mode = None - log = log def __init__(self, openclip_file_path, feed_data): - # test if media script paht exists - self._validate_media_script_path() + self.out_file = openclip_file_path # new feed variables: - feed_path = feed_data["path"] + feed_path = feed_data.pop("path") + + # initialize parent class + super(OpenClipSolver, self).__init__( + feed_path, + **feed_data + ) + + # get other metadata self.feed_version_name = feed_data["version"] self.feed_colorspace = feed_data.get("colorspace") - - if feed_data.get("logger"): - self.log = feed_data["logger"] + self.log.debug("feed_version_name: {}".format(self.feed_version_name)) # derivate other feed variables self.feed_basename = os.path.basename(feed_path) self.feed_dir = os.path.dirname(feed_path) self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - - if not self._is_valid_tmp_file(openclip_file_path): - # openclip does not exist yet and will be created - self.tmp_file = self.out_file = openclip_file_path + self.log.debug("feed_ext: {}".format(self.feed_ext)) + self.log.debug("out_file: {}".format(self.out_file)) + if not self._is_valid_tmp_file(self.out_file): self.create_new_clip = True - else: - # update already created clip - # output a temp file - self.out_file = openclip_file_path - self.tmp_file = os.path.join(self.feed_dir, self.tmp_name) - - # remove previously generated temp files - # it will be regenerated - self._clear_tmp_file() - - self.log.info("Temp File: {}".format(self.tmp_file)) - def _is_valid_tmp_file(self, file): # check if file exists if os.path.isfile(file): @@ -740,7 +720,6 @@ class OpenClipSolver: return False def make(self): - self._generate_media_info_file() if self.create_new_clip: # New openClip @@ -748,69 +727,17 @@ class OpenClipSolver: else: self._update_open_clip() - def _validate_media_script_path(self): - if not os.path.isfile(self.media_script_path): - raise IOError("Media Scirpt does not exist: `{}`".format( - self.media_script_path)) - - def _generate_media_info_file(self): - # Create cmd arguments for gettig xml file info file - cmd_args = [ - self.media_script_path, - "-e", self.feed_ext, - "-o", self.tmp_file, - self.feed_dir - ] - - # execute creation of clip xml template data - try: - openpype.run_subprocess(cmd_args) - self._make_single_clip_media_info() - except TypeError: - self.log.error("Error creating self.tmp_file") - six.reraise(*sys.exc_info()) - - def _make_single_clip_media_info(self): - with open(self.tmp_file) as f: - lines = f.readlines() - _added_root = itertools.chain( - "", deepcopy(lines)[1:], "") - new_root = ET.fromstringlist(_added_root) - - # find the clip which is matching to my input name - xml_clips = new_root.findall("clip") - matching_clip = None - for xml_clip in xml_clips: - if xml_clip.find("name").text in self.feed_basename: - matching_clip = xml_clip - - if matching_clip is None: - # return warning there is missing clip - raise ET.ParseError( - "Missing clip in `{}`. Available clips {}".format( - self.feed_basename, [ - xml_clip.find("name").text - for xml_clip in xml_clips - ] - )) - - self._write_result_xml_to_file(self.tmp_file, matching_clip) - - def _clear_tmp_file(self): - if os.path.isfile(self.tmp_file): - os.remove(self.tmp_file) - def _clear_handler(self, xml_object): for handler in xml_object.findall("./handler"): - self.log.debug("Handler found") + self.log.info("Handler found") xml_object.remove(handler) def _create_new_open_clip(self): self.log.info("Building new openClip") + self.log.debug(">> self.clip_data: {}".format(self.clip_data)) - tmp_xml = ET.parse(self.tmp_file) - - tmp_xml_feeds = tmp_xml.find('tracks/track/feeds') + # clip data comming from MediaInfoFile + tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') tmp_xml_feeds.set('currentVersion', self.feed_version_name) for tmp_feed in tmp_xml_feeds: tmp_feed.set('vuid', self.feed_version_name) @@ -821,46 +748,48 @@ class OpenClipSolver: self._clear_handler(tmp_feed) - tmp_xml_versions_obj = tmp_xml.find('versions') + tmp_xml_versions_obj = self.clip_data.find('versions') tmp_xml_versions_obj.set('currentVersion', self.feed_version_name) for xml_new_version in tmp_xml_versions_obj: xml_new_version.set('uid', self.feed_version_name) xml_new_version.set('type', 'version') - xml_data = self._fix_xml_data(tmp_xml) + self._clear_handler(self.clip_data) self.log.info("Adding feed version: {}".format(self.feed_basename)) - self._write_result_xml_to_file(self.out_file, xml_data) - - self.log.info("openClip Updated: {}".format(self.tmp_file)) + self.write_clip_data_to_file(self.out_file, self.clip_data) def _update_open_clip(self): self.log.info("Updating openClip ..") out_xml = ET.parse(self.out_file) - tmp_xml = ET.parse(self.tmp_file) + out_xml = out_xml.getroot() self.log.debug(">> out_xml: {}".format(out_xml)) - self.log.debug(">> tmp_xml: {}".format(tmp_xml)) + self.log.debug(">> self.clip_data: {}".format(self.clip_data)) # Get new feed from tmp file - tmp_xml_feed = tmp_xml.find('tracks/track/feeds/feed') + tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed') self._clear_handler(tmp_xml_feed) - self._get_time_info_from_origin(out_xml) - if self.out_feed_fps: + # update fps from MediaInfoFile class + if self.fps: tmp_feed_fps_obj = tmp_xml_feed.find( "startTimecode/rate") - tmp_feed_fps_obj.text = self.out_feed_fps - if self.out_feed_nb_ticks: + tmp_feed_fps_obj.text = str(self.fps) + + # update start_frame from MediaInfoFile class + if self.start_frame: tmp_feed_nb_ticks_obj = tmp_xml_feed.find( "startTimecode/nbTicks") - tmp_feed_nb_ticks_obj.text = self.out_feed_nb_ticks - if self.out_feed_drop_mode: + tmp_feed_nb_ticks_obj.text = str(self.start_frame) + + # update drop_mode from MediaInfoFile class + if self.drop_mode: tmp_feed_drop_mode_obj = tmp_xml_feed.find( "startTimecode/dropMode") - tmp_feed_drop_mode_obj.text = self.out_feed_drop_mode + tmp_feed_drop_mode_obj.text = str(self.drop_mode) new_path_obj = tmp_xml_feed.find( "spans/span/path") @@ -893,7 +822,7 @@ class OpenClipSolver: "version", {"type": "version", "uid": self.feed_version_name}) out_xml_versions_obj.insert(0, new_version_obj) - xml_data = self._fix_xml_data(out_xml) + self._clear_handler(out_xml) # fist create backup self._create_openclip_backup_file(self.out_file) @@ -901,30 +830,9 @@ class OpenClipSolver: self.log.info("Adding feed version: {}".format( self.feed_version_name)) - self._write_result_xml_to_file(self.out_file, xml_data) + self.write_clip_data_to_file(self.out_file, out_xml) - self.log.info("openClip Updated: {}".format(self.out_file)) - - self._clear_tmp_file() - - def _get_time_info_from_origin(self, xml_data): - try: - for out_track in xml_data.iter('track'): - for out_feed in out_track.iter('feed'): - out_feed_nb_ticks_obj = out_feed.find( - 'startTimecode/nbTicks') - self.out_feed_nb_ticks = out_feed_nb_ticks_obj.text - out_feed_fps_obj = out_feed.find( - 'startTimecode/rate') - self.out_feed_fps = out_feed_fps_obj.text - out_feed_drop_mode_obj = out_feed.find( - 'startTimecode/dropMode') - self.out_feed_drop_mode = out_feed_drop_mode_obj.text - break - else: - continue - except Exception as msg: - self.log.warning(msg) + self.log.debug("OpenClip Updated: {}".format(self.out_file)) def _feed_exists(self, xml_data, path): # loop all available feed paths and check if @@ -935,17 +843,6 @@ class OpenClipSolver: "Not appending file as it already is in .clip file") return True - def _fix_xml_data(self, xml_data): - xml_root = xml_data.getroot() - self._clear_handler(xml_root) - return xml_root - - def _write_result_xml_to_file(self, file, xml_data): - # save it as new file - tree = cET.ElementTree(xml_data) - tree.write(file, xml_declaration=True, - method='xml', encoding='UTF-8') - def _create_openclip_backup_file(self, file): bck_file = "{}.bak".format(file) # if backup does not exist diff --git a/openpype/hosts/flame/api/test_plugin.py b/openpype/hosts/flame/api/test_plugin.py deleted file mode 100644 index d75819a9e3..0000000000 --- a/openpype/hosts/flame/api/test_plugin.py +++ /dev/null @@ -1,428 +0,0 @@ -import os -import tempfile -import itertools -import contextlib -import xml.etree.cElementTree as cET -from copy import deepcopy -import shutil -from xml.etree import ElementTree as ET - -import openpype.api as openpype - -import logging - -log = logging.getLogger(__name__) - - -@contextlib.contextmanager -def maintained_temp_file_path(suffix=None): - _suffix = suffix or "" - - try: - # Store dumped json to temporary file - temporary_file = tempfile.mktemp( - suffix=_suffix, prefix="flame_maintained_") - yield temporary_file.replace("\\", "/") - - except IOError as _error: - raise IOError( - "Not able to create temp json file: {}".format(_error)) - - finally: - # Remove the temporary json - os.remove(temporary_file) - - -class MediaInfoFile(object): - """Class to get media info file clip data - - Raises: - IOError: MEDIA_SCRIPT_PATH path doesn't exists - TypeError: Not able to generate clip xml data file - ET.ParseError: Missing clip in xml clip data - IOError: Not able to save xml clip data to file - - Attributes: - str: `MEDIA_SCRIPT_PATH` path to flame binary - logging.Logger: `log` logger - - TODO: add method for getting metadata to dict - """ - MEDIA_SCRIPT_PATH = "/opt/Autodesk/mio/current/dl_get_media_info" - - log = log - - _clip_data = None - _start_frame = None - _fps = None - _drop_mode = None - - def __init__(self, path, **kwargs): - - # replace log if any - if kwargs.get("logger"): - self.log = kwargs["logger"] - - # test if `dl_get_media_info` paht exists - self._validate_media_script_path() - - # derivate other feed variables - self.feed_basename = os.path.basename(path) - self.feed_dir = os.path.dirname(path) - self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - - with maintained_temp_file_path(".clip") as tmp_path: - self.log.info("Temp File: {}".format(tmp_path)) - self._generate_media_info_file(tmp_path) - - # get clip data and make them single if there is multiple - # clips data - xml_data = self._make_single_clip_media_info(tmp_path) - self.log.info("xml_data: {}".format(xml_data)) - self.log.info("type: {}".format(type(xml_data))) - - # get all time related data and assign them - self._get_time_info_from_origin(xml_data) - self.log.info("start_frame: {}".format(self.start_frame)) - self.log.info("fps: {}".format(self.fps)) - self.log.info("drop frame: {}".format(self.drop_mode)) - self.clip_data = xml_data - - @property - def clip_data(self): - """Clip's xml clip data - - Returns: - xml.etree.ElementTree: xml data - """ - return self._clip_data - - @clip_data.setter - def clip_data(self, data): - self._clip_data = data - - @property - def start_frame(self): - """ Clip's starting frame found in timecode - - Returns: - int: number of frames - """ - return self._start_frame - - @start_frame.setter - def start_frame(self, number): - self._start_frame = int(number) - - @property - def fps(self): - """ Clip's frame rate - - Returns: - float: frame rate - """ - return self._fps - - @fps.setter - def fps(self, fl_number): - self._fps = float(fl_number) - - @property - def drop_mode(self): - """ Clip's drop frame mode - - Returns: - str: drop frame flag - """ - return self._drop_mode - - @drop_mode.setter - def drop_mode(self, text): - self._drop_mode = str(text) - - def _validate_media_script_path(self): - if not os.path.isfile(self.MEDIA_SCRIPT_PATH): - raise IOError("Media Scirpt does not exist: `{}`".format( - self.MEDIA_SCRIPT_PATH)) - - def _generate_media_info_file(self, fpath): - # Create cmd arguments for gettig xml file info file - cmd_args = [ - self.MEDIA_SCRIPT_PATH, - "-e", self.feed_ext, - "-o", fpath, - self.feed_dir - ] - - try: - # execute creation of clip xml template data - openpype.run_subprocess(cmd_args) - except TypeError as error: - raise TypeError( - "Error creating `{}` due: {}".format(fpath, error)) - - def _make_single_clip_media_info(self, fpath): - with open(fpath) as f: - lines = f.readlines() - _added_root = itertools.chain( - "", deepcopy(lines)[1:], "") - new_root = ET.fromstringlist(_added_root) - - # find the clip which is matching to my input name - xml_clips = new_root.findall("clip") - matching_clip = None - for xml_clip in xml_clips: - if xml_clip.find("name").text in self.feed_basename: - matching_clip = xml_clip - - if matching_clip is None: - # return warning there is missing clip - raise ET.ParseError( - "Missing clip in `{}`. Available clips {}".format( - self.feed_basename, [ - xml_clip.find("name").text - for xml_clip in xml_clips - ] - )) - - return matching_clip - - def _get_time_info_from_origin(self, xml_data): - try: - 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') - self.start_frame = out_feed_nb_ticks_obj.text - - # fps - out_feed_fps_obj = out_feed.find( - 'startTimecode/rate') - self.fps = out_feed_fps_obj.text - - # drop frame mode - out_feed_drop_mode_obj = out_feed.find( - 'startTimecode/dropMode') - self.drop_mode = out_feed_drop_mode_obj.text - break - else: - continue - except Exception as msg: - self.log.warning(msg) - - @staticmethod - def write_clip_data_to_file(fpath, xml_data): - log.info(">>> type of xml_data: {}".format(type(xml_data))) - if isinstance(xml_data, ET.ElementTree): - xml_data = xml_data.getroot() - try: - # save it as new file - tree = cET.ElementTree(xml_data) - tree.write( - fpath, xml_declaration=True, - method='xml', encoding='UTF-8' - ) - except IOError as error: - raise IOError( - "Not able to write data to file: {}".format(error)) - - -class OpenClipSolver(MediaInfoFile): - create_new_clip = False - - log = log - - def __init__(self, openclip_file_path, feed_data): - self.out_file = openclip_file_path - - # new feed variables: - feed_path = feed_data.pop("path") - - # initialize parent class - super(OpenClipSolver, self).__init__( - feed_path, - **feed_data - ) - - # get other metadata - self.feed_version_name = feed_data["version"] - self.feed_colorspace = feed_data.get("colorspace") - self.log.info("feed_version_name: {}".format(self.feed_version_name)) - - # derivate other feed variables - self.feed_basename = os.path.basename(feed_path) - self.feed_dir = os.path.dirname(feed_path) - self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() - self.log.info("feed_ext: {}".format(self.feed_ext)) - self.log.info("out_file: {}".format(self.out_file)) - if not self._is_valid_tmp_file(self.out_file): - self.create_new_clip = True - - def _is_valid_tmp_file(self, file): - # check if file exists - if os.path.isfile(file): - # test also if file is not empty - with open(file) as f: - lines = f.readlines() - if len(lines) > 2: - return True - - # file is probably corrupted - os.remove(file) - return False - - def make(self): - - if self.create_new_clip: - # New openClip - self._create_new_open_clip() - else: - self._update_open_clip() - - def _clear_handler(self, xml_object): - for handler in xml_object.findall("./handler"): - self.log.info("Handler found") - xml_object.remove(handler) - - def _create_new_open_clip(self): - self.log.info("Building new openClip") - self.log.info(">> self.clip_data: {}".format(self.clip_data)) - - # clip data comming from MediaInfoFile - tmp_xml_feeds = self.clip_data.find('tracks/track/feeds') - tmp_xml_feeds.set('currentVersion', self.feed_version_name) - for tmp_feed in tmp_xml_feeds: - tmp_feed.set('vuid', self.feed_version_name) - - # add colorspace if any is set - if self.feed_colorspace: - self._add_colorspace(tmp_feed, self.feed_colorspace) - - self._clear_handler(tmp_feed) - - tmp_xml_versions_obj = self.clip_data.find('versions') - tmp_xml_versions_obj.set('currentVersion', self.feed_version_name) - for xml_new_version in tmp_xml_versions_obj: - xml_new_version.set('uid', self.feed_version_name) - xml_new_version.set('type', 'version') - - self._clear_handler(self.clip_data) - self.log.info("Adding feed version: {}".format(self.feed_basename)) - - self.write_clip_data_to_file(self.out_file, self.clip_data) - - def _update_open_clip(self): - self.log.info("Updating openClip ..") - - out_xml = ET.parse(self.out_file) - - self.log.info(">> out_xml: {}".format(out_xml)) - self.log.info(">> self.clip_data: {}".format(self.clip_data)) - - # Get new feed from tmp file - tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed') - - self._clear_handler(tmp_xml_feed) - - # update fps from MediaInfoFile class - if self.fps: - tmp_feed_fps_obj = tmp_xml_feed.find( - "startTimecode/rate") - tmp_feed_fps_obj.text = str(self.fps) - - # update start_frame from MediaInfoFile class - if self.start_frame: - tmp_feed_nb_ticks_obj = tmp_xml_feed.find( - "startTimecode/nbTicks") - tmp_feed_nb_ticks_obj.text = str(self.start_frame) - - # update drop_mode from MediaInfoFile class - if self.drop_mode: - tmp_feed_drop_mode_obj = tmp_xml_feed.find( - "startTimecode/dropMode") - tmp_feed_drop_mode_obj.text = str(self.drop_mode) - - new_path_obj = tmp_xml_feed.find( - "spans/span/path") - new_path = new_path_obj.text - - feed_added = False - if not self._feed_exists(out_xml, new_path): - tmp_xml_feed.set('vuid', self.feed_version_name) - # Append new temp file feed to .clip source out xml - out_track = out_xml.find("tracks/track") - # add colorspace if any is set - if self.feed_colorspace: - self._add_colorspace(tmp_xml_feed, self.feed_colorspace) - - out_feeds = out_track.find('feeds') - out_feeds.set('currentVersion', self.feed_version_name) - out_feeds.append(tmp_xml_feed) - - self.log.info( - "Appending new feed: {}".format( - self.feed_version_name)) - feed_added = True - - if feed_added: - # Append vUID to versions - out_xml_versions_obj = out_xml.find('versions') - out_xml_versions_obj.set( - 'currentVersion', self.feed_version_name) - new_version_obj = ET.Element( - "version", {"type": "version", "uid": self.feed_version_name}) - out_xml_versions_obj.insert(0, new_version_obj) - - self._clear_handler(out_xml) - - # fist create backup - self._create_openclip_backup_file(self.out_file) - - self.log.info("Adding feed version: {}".format( - self.feed_version_name)) - - self.write_clip_data_to_file(self.out_file, out_xml) - - self.log.info("openClip Updated: {}".format(self.out_file)) - - def _feed_exists(self, xml_data, path): - # loop all available feed paths and check if - # the path is not already in file - for src_path in xml_data.iter('path'): - if path == src_path.text: - self.log.warning( - "Not appending file as it already is in .clip file") - return True - - def _create_openclip_backup_file(self, file): - bck_file = "{}.bak".format(file) - # if backup does not exist - if not os.path.isfile(bck_file): - shutil.copy2(file, bck_file) - else: - # in case it exists and is already multiplied - created = False - for _i in range(1, 99): - bck_file = "{name}.bak.{idx:0>2}".format( - name=file, - idx=_i) - # create numbered backup file - if not os.path.isfile(bck_file): - shutil.copy2(file, bck_file) - created = True - break - # in case numbered does not exists - if not created: - bck_file = "{}.bak.last".format(file) - shutil.copy2(file, bck_file) - - def _add_colorspace(self, feed_obj, profile_name): - feed_storage_obj = feed_obj.find("storageFormat") - feed_clr_obj = feed_storage_obj.find("colourSpace") - if feed_clr_obj is not None: - feed_clr_obj = ET.Element( - "colourSpace", {"type": "string"}) - feed_storage_obj.append(feed_clr_obj) - - feed_clr_obj.text = profile_name diff --git a/openpype/hosts/flame/tests/flame_test.py b/openpype/hosts/flame/tests/flame_test.py deleted file mode 100644 index 402983eeba..0000000000 --- a/openpype/hosts/flame/tests/flame_test.py +++ /dev/null @@ -1,30 +0,0 @@ -from openpype.lib import import_filepath - -plugin = import_filepath( - "/Users/pype.club/code/openpype/openpype/hosts/flame/api/test_plugin.py") - -openclip_file_path = "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/test.clip" -# feed_datas = [ -# { -# "path": "/Users/pype.club/pype_club_root/OP02_VFX_demo/shots/a/a0000001/publish/plate/plateMain/v007/op02vfx_a0000001_plateMain_v007_exr16fpdwaaCl.0997.exr", -# "version": "v007" -# }, -# { -# "path": "/Users/pype.club/pype_club_root/OP02_VFX_demo/shots/a/a0000001/publish/plate/plateMain/v008/op02vfx_a0000001_plateMain_v008_exr16fpdwaaCl.0997.exr", -# "version": "v008" -# } -# ] - -feed_datas = [ - { - "path": "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/v001/file_name_v001.1001.exr", - "version": "v001" - }, - { - "path": "/Users/pype.club/FLAME_STORAGE/test_shot_fps_float/v002/file_name_v002.1001.exr", - "version": "v002" - } -] -for feed_data in feed_datas: - oclip = plugin.OpenClipSolver(openclip_file_path, feed_data) - oclip.make() From bb222642fa295e46357f1a6cc364ac34b5f6bce1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 19:06:32 +0200 Subject: [PATCH 264/337] reversing version rename --- openpype/version.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/version.py b/openpype/version.py index d447d27172..97aa585ca7 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,10 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.3-nightly.1-upp220408" - - -''' -includes: - - Flame: integrate batch groups: - https://github.com/pypeclub/OpenPype/pull/2928 -''' +__version__ = "3.9.3" From c5683f50d44f99469cc6393c68c2047dd40efd49 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 8 Apr 2022 20:07:53 +0200 Subject: [PATCH 265/337] flame: fixing skip condition --- openpype/hosts/flame/otio/flame_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 0b9c9ce817..9f0bec62ea 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -558,7 +558,7 @@ def create_otio_timeline(sequence): len(track.segments) == 0 or track.hidden.get_value() ): - return None + continue # convert track to otio otio_track = create_otio_track( From 22918ac0d6c10a9ba827e0d2d8a94fa821e2f80c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 9 Apr 2022 03:39:12 +0000 Subject: [PATCH 266/337] [Automated] Bump version --- CHANGELOG.md | 38 ++++++++++++++++++++++++-------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e7d5d9e0..c216dd0595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,27 @@ # Changelog +## [3.9.4-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...HEAD) + +### 📖 Documentation + +- Documentation: Python requirements to 3.7.9 [\#3035](https://github.com/pypeclub/OpenPype/pull/3035) +- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) + +**🚀 Enhancements** + +- Resolve environment variable in google drive credential path [\#3008](https://github.com/pypeclub/OpenPype/pull/3008) + +**🐛 Bug fixes** + +- Ftrack: Integrate ftrack api fix [\#3044](https://github.com/pypeclub/OpenPype/pull/3044) +- Webpublisher - removed wrong hardcoded family [\#3043](https://github.com/pypeclub/OpenPype/pull/3043) +- Unreal: Creator import fixes [\#3040](https://github.com/pypeclub/OpenPype/pull/3040) + ## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...3.9.3) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.3-nightly.2...3.9.3) ### 📖 Documentation @@ -20,7 +39,6 @@ - Console Interpreter: Changed how console splitter size are reused on show [\#3016](https://github.com/pypeclub/OpenPype/pull/3016) - Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) - Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) -- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) @@ -32,10 +50,12 @@ - Harmony: Added creating subset name for workfile from template [\#3024](https://github.com/pypeclub/OpenPype/pull/3024) - AfterEffects: Added creating subset name for workfile from template [\#3023](https://github.com/pypeclub/OpenPype/pull/3023) - General: Add example addons to ignored [\#3022](https://github.com/pypeclub/OpenPype/pull/3022) +- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\#3018](https://github.com/pypeclub/OpenPype/pull/3018) - Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) - Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) - Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) - Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) +- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) **Merged pull requests:** @@ -51,7 +71,6 @@ - Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999) - Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) -- Documentation: New publisher develop docs [\#2896](https://github.com/pypeclub/OpenPype/pull/2896) **🆕 New features** @@ -60,6 +79,7 @@ **🚀 Enhancements** +- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) - Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) - General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) @@ -70,13 +90,11 @@ - TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942) - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) -- Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) - Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) **🐛 Bug fixes** - Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) -- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) - Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) - AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989) @@ -120,21 +138,13 @@ **🚀 Enhancements** +- Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) - General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) -- nuke: imageio adding ocio config version 1.2 [\#2897](https://github.com/pypeclub/OpenPype/pull/2897) -- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) **🐛 Bug fixes** - General: Fix use of Anatomy roots [\#2904](https://github.com/pypeclub/OpenPype/pull/2904) - Fixing gap detection in extract review [\#2902](https://github.com/pypeclub/OpenPype/pull/2902) -- Pyblish Pype - ensure current state is correct when entering new group order [\#2899](https://github.com/pypeclub/OpenPype/pull/2899) -- SceneInventory: Fix import of load function [\#2894](https://github.com/pypeclub/OpenPype/pull/2894) -- Harmony - fixed creator issue [\#2891](https://github.com/pypeclub/OpenPype/pull/2891) - -**🔀 Refactored code** - -- General: Reduce style usage to OpenPype repository [\#2889](https://github.com/pypeclub/OpenPype/pull/2889) ## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14) diff --git a/openpype/version.py b/openpype/version.py index 97aa585ca7..08dcbb5aed 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.3" +__version__ = "3.9.4-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 006f6eb4e5..adec7ab158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.3" # OpenPype +version = "3.9.4-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 16e84073329b5dbc992dfadfd0818233f794cb0d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Apr 2022 10:50:34 +0200 Subject: [PATCH 267/337] flame: processing comments --- openpype/hosts/flame/api/plugin.py | 14 ++++++++------ openpype/hosts/flame/api/scripts/wiretap_com.py | 4 ++++ openpype/hosts/flame/otio/flame_export.py | 16 ++++++---------- .../plugins/publish/integrate_batch_group.py | 9 ++------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 42e6e19931..c87445fdd3 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -4,11 +4,12 @@ import shutil from copy import deepcopy from xml.etree import ElementTree as ET +from Qt import QtCore, QtWidgets + import openpype.api as openpype import qargparse from openpype import style from openpype.pipeline import LegacyCreator, LoaderPlugin -from Qt import QtCore, QtWidgets from . import constants from . import lib as flib @@ -712,12 +713,13 @@ class OpenClipSolver(flib.MediaInfoFile): # test also if file is not empty with open(file) as f: lines = f.readlines() - if len(lines) > 2: - return True - # file is probably corrupted - os.remove(file) - return False + if len(lines) > 2: + return True + + # file is probably corrupted + os.remove(file) + return False def make(self): diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index f78102c0a1..4825ff4386 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -461,6 +461,10 @@ class WireTapCom(object): def _subprocess_preexec_fn(): + """ Helper function + + Setting permission mask to 0777 + """ os.setpgrp() os.umask(0o000) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 9f0bec62ea..4fe05ec1d8 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -455,15 +455,10 @@ def add_otio_metadata(otio_item, item, **kwargs): otio_item.metadata.update({key: value}) -def _get_shot_tokens_values(clip, tokens, from_clip=False): +def _get_shot_tokens_values(clip, tokens): old_value = None output = {} - # in case it is segment from reel clip - # avoiding duplicity of segement data - if from_clip: - return {} - old_value = clip.shot_name.get_value() for token in tokens: @@ -480,7 +475,7 @@ def _get_shot_tokens_values(clip, tokens, from_clip=False): return output -def _get_segment_attributes(segment, from_clip=False): +def _get_segment_attributes(segment): log.debug("Segment name|hidden: {}|{}".format( segment.name.get_value(), segment.hidden @@ -503,9 +498,10 @@ def _get_segment_attributes(segment, from_clip=False): } # add all available shot tokens - shot_tokens = _get_shot_tokens_values(segment, [ - "", "", "", "", - ], from_clip) + shot_tokens = _get_shot_tokens_values( + segment, + ["", "", "", ""] + ) clip_data.update(shot_tokens) # populate shot source metadata diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index cac99a25ac..3615f06a3d 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -299,10 +299,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): version_name = "v" version_padding = 3 - # return it as ordered dict - reutrn_dict = OrderedDict() # need to make sure the order of keys is correct - for item in ( + return OrderedDict( ("name", name), ("media_path", media_path), ("media_path_pattern", media_path_pattern), @@ -320,10 +318,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): ("version_mode", version_mode), ("version_name", version_name), ("version_padding", version_padding) - ): - reutrn_dict.update({item[0]: item[1]}) - - return reutrn_dict + ) def _get_shot_task_dir_path(self, instance, task_data): project_doc = instance.data["projectEntity"] From e8ffb06434b26b6f9245362dd71a457dcab828e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 11 Apr 2022 11:42:27 +0200 Subject: [PATCH 268/337] Fix publishing tvp workfile in webpublisher --- .../publish/collect_published_files.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 56b2ef6e20..8edaf4f67b 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -108,15 +108,18 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["representations"] = self._get_single_repre( task_dir, task_data["files"], tags ) - file_url = os.path.join(task_dir, task_data["files"][0]) - no_of_frames = self._get_number_of_frames(file_url) - if no_of_frames: + if family != 'workfile': + file_url = os.path.join(task_dir, task_data["files"][0]) try: - frame_end = int(frame_start) + math.ceil(no_of_frames) - instance.data["frameEnd"] = math.ceil(frame_end) - 1 - self.log.debug("frameEnd:: {}".format( - instance.data["frameEnd"])) - except ValueError: + no_of_frames = self._get_number_of_frames(file_url) + if no_of_frames: + frame_end = int(frame_start) + \ + math.ceil(no_of_frames) + frame_end = math.ceil(frame_end) - 1 + instance.data["frameEnd"] = frame_end + self.log.debug("frameEnd:: {}".format( + instance.data["frameEnd"])) + except Exception: self.log.warning("Unable to count frames " "duration {}".format(no_of_frames)) From a198b7f3f6ef04f48fe43165f7ea17d32134876d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 12:02:08 +0200 Subject: [PATCH 269/337] add format arguments to concatenation arguments --- .../plugins/publish/extract_review_slate.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 505ae75169..49f0eac41d 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -158,13 +158,15 @@ class ExtractReviewSlate(openpype.api.Extractor): ]) if use_legacy_code: + format_args = [] codec_args = repre["_profile"].get('codec', []) output_args.extend(codec_args) # preset's output data output_args.extend(repre["_profile"].get('output', [])) else: # Codecs are copied from source for whole input - codec_args = self._get_codec_args(repre) + format_args, codec_args = self._get_format_codec_args(repre) + output_args.extend(format_args) output_args.extend(codec_args) # make sure colors are correct @@ -266,8 +268,14 @@ class ExtractReviewSlate(openpype.api.Extractor): "-safe", "0", "-i", conc_text_path, "-c", "copy", - output_path ] + # NOTE: Added because of OP Atom demuxers + # Add format arguments if there are any + # - keep format of output + if format_args: + concat_args.extend(format_args) + # Add final output path + concat_args.append(output_path) # ffmpeg concat subprocess self.log.debug( @@ -338,7 +346,7 @@ class ExtractReviewSlate(openpype.api.Extractor): return vf_back - def _get_codec_args(self, repre): + def _get_format_codec_args(self, repre): """Detect possible codec arguments from representation.""" codec_args = [] @@ -361,13 +369,9 @@ class ExtractReviewSlate(openpype.api.Extractor): return codec_args source_ffmpeg_cmd = repre.get("ffmpeg_cmd") - codec_args.extend( - get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd) - ) - codec_args.extend( - get_ffmpeg_codec_args( - ffprobe_data, source_ffmpeg_cmd, logger=self.log - ) + format_args = get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd) + codec_args = get_ffmpeg_codec_args( + ffprobe_data, source_ffmpeg_cmd, logger=self.log ) - return codec_args + return format_args, codec_args From aefaac6e5cd4530185656077567e38586e0586b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 14:49:58 +0200 Subject: [PATCH 270/337] added ability to change general environments --- openpype/settings/lib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 54502292dc..937329b417 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -1113,6 +1113,14 @@ def get_general_environments(): clear_metadata_from_settings(environments) + whitelist_envs = result["general"].get("local_env_white_list") + if whitelist_envs: + local_settings = get_local_settings() + local_envs = local_settings.get("environments") or {} + for key, value in local_envs.items(): + if key in whitelist_envs and key in environments: + environments[key] = value + return environments From fe3758581bbbd2f3249ebccdc20e30aa613a071b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 14:55:59 +0200 Subject: [PATCH 271/337] disable maintain selection on unreal creator plugin --- openpype/hosts/unreal/api/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index b24bab831d..d8d2f2420d 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -10,6 +10,7 @@ from openpype.pipeline import ( class Creator(LegacyCreator): """This serves as skeleton for future OpenPype specific functionality""" defaults = ['Main'] + maintain_selection = False class Loader(LoaderPlugin, ABC): From 2b65b2d4381be1776176678e644ba6e50b8f4272 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 17:11:00 +0200 Subject: [PATCH 272/337] don't check AOVs regex for multipart exrs --- .../deadline/plugins/publish/submit_publish_job.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index e5bda43d07..63f9e35720 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -536,15 +536,17 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # should be review made. # - "review" tag is never added when is set to 'False' if instance["useSequenceForReview"]: - render_file_name = list(collection[0]) - app = os.environ.get("AVALON_APP", "") - aov_patterns = self.aov_filter - # if filtered aov name is found in filename, toggle it for - # preview video rendering - preview = match_aov_pattern(app, aov_patterns, render_file_name) # toggle preview on if multipart is on if instance.get("multipartExr", False): preview = True + else: + render_file_name = list(collection[0]) + host_name = os.environ.get("AVALON_APP", "") + # if filtered aov name is found in filename, toggle it for + # preview video rendering + preview = match_aov_pattern( + host_name, self.aov_filter, render_file_name + ) staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( From 7a40cc269239e522155692c4f4899f8bcc9f6cc4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Apr 2022 17:19:36 +0200 Subject: [PATCH 273/337] fix orderdict --- openpype/hosts/flame/plugins/publish/integrate_batch_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 3615f06a3d..da9553cc2a 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -300,7 +300,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): version_padding = 3 # need to make sure the order of keys is correct - return OrderedDict( + return OrderedDict(( ("name", name), ("media_path", media_path), ("media_path_pattern", media_path_pattern), @@ -318,7 +318,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): ("version_mode", version_mode), ("version_name", version_name), ("version_padding", version_padding) - ) + )) def _get_shot_task_dir_path(self, instance, task_data): project_doc = instance.data["projectEntity"] From 10ce2511f0be2a5627de0f1527e6128e66304dc1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 17:40:44 +0200 Subject: [PATCH 274/337] make sure temporary staging dir is removed --- openpype/plugins/publish/extract_review.py | 238 +++++++++++---------- 1 file changed, 127 insertions(+), 111 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 3ecea1f8bd..d569d82762 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -188,8 +188,7 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs_per_repres = self._get_outputs_per_representations( instance, profile_outputs ) - fill_data = copy.deepcopy(instance.data["anatomyData"]) - for repre, outputs in outputs_per_repres: + for repre, outpu_defs in outputs_per_repres: # Check if input should be preconverted before processing # Store original staging dir (it's value may change) src_repre_staging_dir = repre["stagingDir"] @@ -241,126 +240,143 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log ) - for _output_def in outputs: - output_def = copy.deepcopy(_output_def) - # Make sure output definition has "tags" key - if "tags" not in output_def: - output_def["tags"] = [] - - if "burnins" not in output_def: - output_def["burnins"] = [] - - # Create copy of representation - new_repre = copy.deepcopy(repre) - # Make sure new representation has origin staging dir - # - this is because source representation may change - # it's staging dir because of ffmpeg conversion - new_repre["stagingDir"] = src_repre_staging_dir - - # Remove "delete" tag from new repre if there is - if "delete" in new_repre["tags"]: - new_repre["tags"].remove("delete") - - # Add additional tags from output definition to representation - for tag in output_def["tags"]: - if tag not in new_repre["tags"]: - new_repre["tags"].append(tag) - - # Add burnin link from output definition to representation - for burnin in output_def["burnins"]: - if burnin not in new_repre.get("burnins", []): - if not new_repre.get("burnins"): - new_repre["burnins"] = [] - new_repre["burnins"].append(str(burnin)) - - self.log.debug( - "Linked burnins: `{}`".format(new_repre.get("burnins")) + try: + self._render_output_definitions( + instance, repre, src_repre_staging_dir, outpu_defs ) - self.log.debug( - "New representation tags: `{}`".format( - new_repre.get("tags")) + finally: + # Make sure temporary staging is cleaned up and representation + # has set origin stagingDir + if do_convert: + # Set staging dir of source representation back to previous + # value + repre["stagingDir"] = src_repre_staging_dir + if os.path.exists(new_staging_dir): + shutil.rmtree(new_staging_dir) + + def _render_output_definitions( + self, instance, repre, src_repre_staging_dir, outpu_defs + ): + fill_data = copy.deepcopy(instance.data["anatomyData"]) + for _output_def in outpu_defs: + output_def = copy.deepcopy(_output_def) + # Make sure output definition has "tags" key + if "tags" not in output_def: + output_def["tags"] = [] + + if "burnins" not in output_def: + output_def["burnins"] = [] + + # Create copy of representation + new_repre = copy.deepcopy(repre) + # Make sure new representation has origin staging dir + # - this is because source representation may change + # it's staging dir because of ffmpeg conversion + new_repre["stagingDir"] = src_repre_staging_dir + + # Remove "delete" tag from new repre if there is + if "delete" in new_repre["tags"]: + new_repre["tags"].remove("delete") + + # Add additional tags from output definition to representation + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + # Add burnin link from output definition to representation + for burnin in output_def["burnins"]: + if burnin not in new_repre.get("burnins", []): + if not new_repre.get("burnins"): + new_repre["burnins"] = [] + new_repre["burnins"].append(str(burnin)) + + self.log.debug( + "Linked burnins: `{}`".format(new_repre.get("burnins")) + ) + + self.log.debug( + "New representation tags: `{}`".format( + new_repre.get("tags")) + ) + + temp_data = self.prepare_temp_data(instance, repre, output_def) + files_to_clean = [] + if temp_data["input_is_sequence"]: + self.log.info("Filling gaps in sequence.") + files_to_clean = self.fill_sequence_gaps( + temp_data["origin_repre"]["files"], + new_repre["stagingDir"], + temp_data["frame_start"], + temp_data["frame_end"]) + + # create or update outputName + output_name = new_repre.get("outputName", "") + output_ext = new_repre["ext"] + if output_name: + output_name += "_" + output_name += output_def["filename_suffix"] + if temp_data["without_handles"]: + output_name += "_noHandles" + + # add outputName to anatomy format fill_data + fill_data.update({ + "output": output_name, + "ext": output_ext + }) + + try: # temporary until oiiotool is supported cross platform + ffmpeg_args = self._ffmpeg_arguments( + output_def, instance, new_repre, temp_data, fill_data ) - - temp_data = self.prepare_temp_data( - instance, repre, output_def) - files_to_clean = [] - if temp_data["input_is_sequence"]: - self.log.info("Filling gaps in sequence.") - files_to_clean = self.fill_sequence_gaps( - temp_data["origin_repre"]["files"], - new_repre["stagingDir"], - temp_data["frame_start"], - temp_data["frame_end"]) - - # create or update outputName - output_name = new_repre.get("outputName", "") - output_ext = new_repre["ext"] - if output_name: - output_name += "_" - output_name += output_def["filename_suffix"] - if temp_data["without_handles"]: - output_name += "_noHandles" - - # add outputName to anatomy format fill_data - fill_data.update({ - "output": output_name, - "ext": output_ext - }) - - try: # temporary until oiiotool is supported cross platform - ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data, fill_data + except ZeroDivisionError: + # TODO recalculate width and height using OIIO before + # conversion + if 'exr' in temp_data["origin_repre"]["ext"]: + self.log.warning( + ( + "Unsupported compression on input files." + " Skipping!!!" + ), + exc_info=True ) - except ZeroDivisionError: - if 'exr' in temp_data["origin_repre"]["ext"]: - self.log.debug("Unsupported compression on input " + - "files. Skipping!!!") - return - raise NotImplementedError + return + raise NotImplementedError - subprcs_cmd = " ".join(ffmpeg_args) + subprcs_cmd = " ".join(ffmpeg_args) - # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) + # run subprocess + self.log.debug("Executing: {}".format(subprcs_cmd)) - openpype.api.run_subprocess( - subprcs_cmd, shell=True, logger=self.log - ) + openpype.api.run_subprocess( + subprcs_cmd, shell=True, logger=self.log + ) - # delete files added to fill gaps - if files_to_clean: - for f in files_to_clean: - os.unlink(f) + # delete files added to fill gaps + if files_to_clean: + for f in files_to_clean: + os.unlink(f) - new_repre.update({ - "name": "{}_{}".format(output_name, output_ext), - "outputName": output_name, - "outputDef": output_def, - "frameStartFtrack": temp_data["output_frame_start"], - "frameEndFtrack": temp_data["output_frame_end"], - "ffmpeg_cmd": subprcs_cmd - }) + new_repre.update({ + "name": "{}_{}".format(output_name, output_ext), + "outputName": output_name, + "outputDef": output_def, + "frameStartFtrack": temp_data["output_frame_start"], + "frameEndFtrack": temp_data["output_frame_end"], + "ffmpeg_cmd": subprcs_cmd + }) - # Force to pop these key if are in new repre - new_repre.pop("preview", None) - new_repre.pop("thumbnail", None) - if "clean_name" in new_repre.get("tags", []): - new_repre.pop("outputName") + # Force to pop these key if are in new repre + new_repre.pop("preview", None) + new_repre.pop("thumbnail", None) + if "clean_name" in new_repre.get("tags", []): + new_repre.pop("outputName") - # adding representation - self.log.debug( - "Adding new representation: {}".format(new_repre) - ) - instance.data["representations"].append(new_repre) - - # Cleanup temp staging dir after procesisng of output definitions - if do_convert: - temp_dir = repre["stagingDir"] - shutil.rmtree(temp_dir) - # Set staging dir of source representation back to previous - # value - repre["stagingDir"] = src_repre_staging_dir + # adding representation + self.log.debug( + "Adding new representation: {}".format(new_repre) + ) + instance.data["representations"].append(new_repre) def input_is_sequence(self, repre): """Deduce from representation data if input is sequence.""" From 1bd174c37557dbe06c3c65d1cc621491bf7b2947 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 17:50:27 +0200 Subject: [PATCH 275/337] simplified how to avoid adding attributes during oiio procesing --- openpype/lib/transcoding.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 8e79aba0ae..448c9eefe0 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -422,7 +422,12 @@ def convert_for_ffmpeg( compression = "none" # Prepare subprocess arguments - oiio_cmd = [get_oiio_tools_path()] + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + ] # Add input compression if available if compression: oiio_cmd.extend(["--compression", compression]) @@ -458,7 +463,6 @@ def convert_for_ffmpeg( "--frames", "{}-{}".format(input_frame_start, input_frame_end) ]) - ignore_attr_changes_added = False for attr_name, attr_value in input_info["attribs"].items(): if not isinstance(attr_value, str): continue @@ -466,10 +470,6 @@ def convert_for_ffmpeg( # Remove attributes that have string value longer than allowed length # for ffmpeg if len(attr_value) > MAX_FFMPEG_STRING_LEN: - if not ignore_attr_changes_added: - # Attrite changes won't be added to attributes itself - ignore_attr_changes_added = True - oiio_cmd.append("--sansattrib") # Set attribute to empty string logger.info(( "Removed attribute \"{}\" from metadata" From b40e5ba0b8bb697e15ff14e0f50ef45da61759c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Apr 2022 17:50:53 +0200 Subject: [PATCH 276/337] added check for invalid characters in attribute value --- openpype/lib/transcoding.py | 38 +++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 448c9eefe0..c2fecf6628 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -17,6 +17,9 @@ from .vendor_bin_utils import ( # Max length of string that is supported by ffmpeg MAX_FFMPEG_STRING_LEN = 8196 +# Not allowed symbols in attributes for ffmpeg +NOT_ALLOWED_FFMPEG_CHARS = ("\"", ) + # OIIO known xml tags STRING_TAGS = { "format" @@ -367,11 +370,15 @@ def should_convert_for_ffmpeg(src_filepath): return None for attr_value in input_info["attribs"].values(): - if ( - isinstance(attr_value, str) - and len(attr_value) > MAX_FFMPEG_STRING_LEN - ): + if not isinstance(attr_value, str): + continue + + if len(attr_value) > MAX_FFMPEG_STRING_LEN: return True + + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char in attr_value: + return True return False @@ -468,13 +475,28 @@ def convert_for_ffmpeg( continue # Remove attributes that have string value longer than allowed length - # for ffmpeg + # for ffmpeg or when containt unallowed symbols + erase_reason = "Missing reason" + erase_attribute = False if len(attr_value) > MAX_FFMPEG_STRING_LEN: + erase_reason = "has too long value ({} chars).".format( + len(attr_value) + ) + + if erase_attribute: + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char in attr_value: + erase_attribute = True + erase_reason = ( + "contains unsupported character \"{}\"." + ).format(char) + break + + if erase_attribute: # Set attribute to empty string logger.info(( - "Removed attribute \"{}\" from metadata" - " because has too long value ({} chars)." - ).format(attr_name, len(attr_value))) + "Removed attribute \"{}\" from metadata because {}." + ).format(attr_name, erase_reason)) oiio_cmd.extend(["--eraseattrib", attr_name]) # Add last argument - path to output From 87fa3bae64f7d380eddac5c1bf2498f0c381ce32 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 11:44:34 +0200 Subject: [PATCH 277/337] OP-2005 - refactor yanked _requests_get, _requests_post into lib file --- .../maya/plugins/create/create_render.py | 79 ++--------------- .../maya/plugins/create/create_vrayscene.py | 86 +++---------------- .../plugins/publish/submit_maya_muster.py | 19 +--- .../publish/validate_muster_connection.py | 34 +------- openpype/lib/__init__.py | 9 ++ openpype/lib/connections.py | 38 ++++++++ .../plugins/publish/submit_maya_deadline.py | 47 ++-------- 7 files changed, 78 insertions(+), 234 deletions(-) create mode 100644 openpype/lib/connections.py diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 4f0a394f85..2ded7c720d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -4,8 +4,6 @@ import os import json import appdirs import requests -import six -import sys from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -14,6 +12,7 @@ from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import requests_get from openpype.api import ( get_system_settings, get_project_settings, @@ -117,6 +116,8 @@ class CreateRender(plugin.Creator): except KeyError: self.aov_separator = "_" + manager = ModulesManager() + self.deadline_module = manager.modules_by_name["deadline"] try: default_servers = deadline_settings["deadline_urls"] project_servers = ( @@ -133,10 +134,8 @@ class CreateRender(plugin.Creator): except AttributeError: # Handle situation were we had only one url for deadline. - manager = ModulesManager() - deadline_module = manager.modules_by_name["deadline"] # get default deadline webservice url from deadline module - self.deadline_servers = deadline_module.deadline_urls + self.deadline_servers = self.deadline_module.deadline_urls def process(self): """Entry point.""" @@ -211,7 +210,7 @@ class CreateRender(plugin.Creator): cmds.getAttr("{}.deadlineServers".format(self.instance)) ] ] - pools = self._get_deadline_pools(webservice) + pools = self.deadline_module.get_deadline_pools(webservice, self.log) cmds.deleteAttr("{}.primaryPool".format(self.instance)) cmds.deleteAttr("{}.secondaryPool".format(self.instance)) cmds.addAttr(self.instance, longName="primaryPool", @@ -221,33 +220,6 @@ class CreateRender(plugin.Creator): attributeType="enum", enumName=":".join(["-"] + pools)) - def _get_deadline_pools(self, webservice): - # type: (str) -> list - """Get pools from Deadline. - Args: - webservice (str): Server url. - Returns: - list: Pools. - Throws: - RuntimeError: If deadline webservice is unreachable. - - """ - argument = "{}/api/pools?NamesOnly=true".format(webservice) - try: - response = self._requests_get(argument) - except requests.exceptions.ConnectionError as exc: - msg = 'Cannot connect to deadline web service' - self.log.error(msg) - six.reraise( - RuntimeError, - RuntimeError('{} - {}'.format(msg, exc)), - sys.exc_info()[2]) - if not response.ok: - self.log.warning("No pools retrieved") - return [] - - return response.json() - def _create_render_settings(self): """Create instance settings.""" # get pools @@ -295,7 +267,8 @@ class CreateRender(plugin.Creator): # use first one for initial list of pools. deadline_url = next(iter(self.deadline_servers.values())) - 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( @@ -366,7 +339,7 @@ class CreateRender(plugin.Creator): """ params = {"authToken": self._token} api_entry = "/api/pools/list" - response = self._requests_get(self.MUSTER_REST_URL + api_entry, + response = requests_get(self.MUSTER_REST_URL + api_entry, params=params) if response.status_code != 200: if response.status_code == 401: @@ -392,45 +365,11 @@ class CreateRender(plugin.Creator): api_url = "{}/muster/show_login".format( os.environ["OPENPYPE_WEBSERVER_URL"]) self.log.debug(api_url) - login_response = self._requests_get(api_url, timeout=1) + login_response = requests_get(api_url, timeout=1) if login_response.status_code != 200: self.log.error("Cannot show login form to Muster") raise Exception("Cannot show login form to Muster") - def _requests_post(self, *args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if "verify" not in kwargs: - kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - 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. diff --git a/openpype/hosts/maya/plugins/create/create_vrayscene.py b/openpype/hosts/maya/plugins/create/create_vrayscene.py index fa9c59e016..98dfabbbcb 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayscene.py +++ b/openpype/hosts/maya/plugins/create/create_vrayscene.py @@ -4,8 +4,6 @@ import os import json import appdirs import requests -import six -import sys from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -19,6 +17,7 @@ from openpype.api import ( get_project_settings ) +from openpype.lib import requests_get from openpype.pipeline import CreatorError from openpype.modules import ModulesManager @@ -40,6 +39,10 @@ class CreateVRayScene(plugin.Creator): self._rs = renderSetup.instance() self.data["exportOnFarm"] = False deadline_settings = get_system_settings()["modules"]["deadline"] + + manager = ModulesManager() + self.deadline_module = manager.modules_by_name["deadline"] + if not deadline_settings["enabled"]: self.deadline_servers = {} return @@ -62,10 +65,8 @@ class CreateVRayScene(plugin.Creator): except AttributeError: # Handle situation were we had only one url for deadline. - manager = ModulesManager() - deadline_module = manager.modules_by_name["deadline"] # get default deadline webservice url from deadline module - self.deadline_servers = deadline_module.deadline_urls + self.deadline_servers = self.deadline_module.deadline_urls def process(self): """Entry point.""" @@ -128,7 +129,7 @@ class CreateVRayScene(plugin.Creator): cmds.getAttr("{}.deadlineServers".format(self.instance)) ] ] - pools = self._get_deadline_pools(webservice) + pools = self.deadline_module.get_deadline_pools(webservice) cmds.deleteAttr("{}.primaryPool".format(self.instance)) cmds.deleteAttr("{}.secondaryPool".format(self.instance)) cmds.addAttr(self.instance, longName="primaryPool", @@ -138,33 +139,6 @@ class CreateVRayScene(plugin.Creator): attributeType="enum", enumName=":".join(["-"] + pools)) - def _get_deadline_pools(self, webservice): - # type: (str) -> list - """Get pools from Deadline. - Args: - webservice (str): Server url. - Returns: - list: Pools. - Throws: - RuntimeError: If deadline webservice is unreachable. - - """ - argument = "{}/api/pools?NamesOnly=true".format(webservice) - try: - response = self._requests_get(argument) - except requests.exceptions.ConnectionError as exc: - msg = 'Cannot connect to deadline web service' - self.log.error(msg) - six.reraise( - CreatorError, - CreatorError('{} - {}'.format(msg, exc)), - sys.exc_info()[2]) - if not response.ok: - self.log.warning("No pools retrieved") - return [] - - return response.json() - def _create_vray_instance_settings(self): # get pools pools = [] @@ -195,7 +169,7 @@ class CreateVRayScene(plugin.Creator): for k in self.deadline_servers.keys() ][0] - pool_names = self._get_deadline_pools(deadline_url) + pool_names = self.deadline_module.get_deadline_pools(deadline_url) if muster_enabled: self.log.info(">>> Loading Muster credentials ...") @@ -259,8 +233,8 @@ class CreateVRayScene(plugin.Creator): """ params = {"authToken": self._token} api_entry = "/api/pools/list" - response = self._requests_get(self.MUSTER_REST_URL + api_entry, - params=params) + response = requests_get(self.MUSTER_REST_URL + api_entry, + params=params) if response.status_code != 200: if response.status_code == 401: self.log.warning("Authentication token expired.") @@ -285,45 +259,7 @@ class CreateVRayScene(plugin.Creator): api_url = "{}/muster/show_login".format( os.environ["OPENPYPE_WEBSERVER_URL"]) self.log.debug(api_url) - login_response = self._requests_get(api_url, timeout=1) + login_response = requests_get(api_url, timeout=1) if login_response.status_code != 200: self.log.error("Cannot show login form to Muster") raise CreatorError("Cannot show login form to Muster") - - def _requests_post(self, *args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if "verify" not in kwargs: - kwargs["verify"] = ( - False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True - ) # noqa - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if "verify" not in kwargs: - kwargs["verify"] = ( - False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True - ) # noqa - return requests.get(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index f852904580..255ed96901 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -4,13 +4,13 @@ import getpass import platform import appdirs -import requests from maya import cmds from avalon import api import pyblish.api +from openpype.lib import requests_post from openpype.hosts.maya.api import lib from openpype.api import get_system_settings @@ -184,7 +184,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): "select": "name" } api_entry = '/api/templates/list' - response = self._requests_post( + response = requests_post( self.MUSTER_REST_URL + api_entry, params=params) if response.status_code != 200: self.log.error( @@ -235,7 +235,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): "name": "submit" } api_entry = '/api/queue/actions' - response = self._requests_post( + response = requests_post( self.MUSTER_REST_URL + api_entry, params=params, json=payload) if response.status_code != 200: @@ -549,16 +549,3 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): % (value, int(value)) ) - def _requests_post(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. - - WARNING: disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - return requests.post(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/publish/validate_muster_connection.py b/openpype/hosts/maya/plugins/publish/validate_muster_connection.py index af32c82f97..6dc7bd3bc4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_muster_connection.py +++ b/openpype/hosts/maya/plugins/publish/validate_muster_connection.py @@ -2,9 +2,9 @@ import os import json import appdirs -import requests import pyblish.api +from openpype.lib import requests_get from openpype.plugin import contextplugin_should_run import openpype.hosts.maya.api.action @@ -51,7 +51,7 @@ class ValidateMusterConnection(pyblish.api.ContextPlugin): 'authToken': self._token } api_entry = '/api/pools/list' - response = self._requests_get( + response = requests_get( MUSTER_REST_URL + api_entry, params=params) assert response.status_code == 200, "invalid response from server" assert response.json()['ResponseData'], "invalid data in response" @@ -88,35 +88,7 @@ class ValidateMusterConnection(pyblish.api.ContextPlugin): api_url = "{}/muster/show_login".format( os.environ["OPENPYPE_WEBSERVER_URL"]) cls.log.debug(api_url) - response = cls._requests_get(api_url, timeout=1) + response = requests_get(api_url, timeout=1) if response.status_code != 200: cls.log.error('Cannot show login form to Muster') raise Exception('Cannot show login form to Muster') - - def _requests_post(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. - - WARNING: disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. - - WARNING: disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - return requests.get(*args, **kwargs) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index e8b6d18f4e..b57e469f5b 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -221,6 +221,12 @@ from .openpype_version import ( is_current_version_higher_than_expected ) + +from .connections import ( + requests_get, + requests_post +) + terminal = Terminal __all__ = [ @@ -390,4 +396,7 @@ __all__ = [ "is_running_from_build", "is_running_staging", "is_current_version_studio_latest", + + "requests_get", + "requests_post" ] diff --git a/openpype/lib/connections.py b/openpype/lib/connections.py new file mode 100644 index 0000000000..91b745a4c1 --- /dev/null +++ b/openpype/lib/connections.py @@ -0,0 +1,38 @@ +import requests +import os + + +def requests_post(*args, **kwargs): + """Wrap request post method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if "verify" not in kwargs: + kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) + return requests.post(*args, **kwargs) + + +def requests_get(*args, **kwargs): + """Wrap request get method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if "verify" not in kwargs: + kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) + return requests.get(*args, **kwargs) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 34147712bc..02e89edd1e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -35,6 +35,7 @@ from maya import cmds from avalon import api import pyblish.api +from openpype.lib import requests_post from openpype.hosts.maya.api import lib # Documentation for keys available at: @@ -700,7 +701,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): tiles_count = instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 for tile_job in frame_payloads: - response = self._requests_post(url, json=tile_job) + response = requests_post(url, json=tile_job) if not response.ok: raise Exception(response.text) @@ -763,7 +764,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): job_idx, len(assembly_payloads) )) self.log.debug(json.dumps(ass_job, indent=4, sort_keys=True)) - response = self._requests_post(url, json=ass_job) + response = requests_post(url, json=ass_job) if not response.ok: raise Exception(response.text) @@ -781,7 +782,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # E.g. http://192.168.0.1:8082/api/jobs url = "{}/api/jobs".format(self.deadline_url) - response = self._requests_post(url, json=payload) + response = requests_post(url, json=payload) if not response.ok: raise Exception(response.text) instance.data["deadlineSubmissionJob"] = response.json() @@ -989,7 +990,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.log.info("Submitting ass export job.") url = "{}/api/jobs".format(self.deadline_url) - response = self._requests_post(url, json=payload) + response = requests_post(url, json=payload) if not response.ok: self.log.error("Submition failed!") self.log.error(response.status_code) @@ -1013,44 +1014,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): % (value, int(value)) ) - def _requests_post(self, *args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if 'verify' not in kwargs: - kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if 'verify' not in kwargs: - kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.get(*args, **kwargs) - def format_vray_output_filename(self, filename, template, dir=False): """Format the expected output file of the Export job. From f637db72d3da547085c94a867f92e591c5969da7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 11:45:58 +0200 Subject: [PATCH 278/337] OP-2005 - added new collector and validator for Deadline pools Refactored old usage --- .../deadline/plugins/publish/collect_pools.py | 24 ++++++++++ .../publish/help/validate_deadline_pools.xml | 24 ++++++++++ .../publish/submit_aftereffects_deadline.py | 6 +-- .../publish/submit_harmony_deadline.py | 6 +-- .../publish/submit_houdini_render_deadline.py | 5 +- .../plugins/publish/submit_nuke_deadline.py | 6 +-- .../plugins/publish/submit_publish_job.py | 4 +- .../publish/validate_deadline_pools.py | 47 ++++++++++++++++++ .../defaults/project_settings/deadline.json | 10 ++-- .../schema_project_deadline.json | 48 +++++++------------ 10 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 openpype/modules/deadline/plugins/publish/collect_pools.py create mode 100644 openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml create mode 100644 openpype/modules/deadline/plugins/publish/validate_deadline_pools.py diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py new file mode 100644 index 0000000000..4f54cdf211 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""Collect Deadline pools. Choose default one from Settings + +""" +import pyblish.api + + +class CollectDeadlinePools(pyblish.api.InstancePlugin): + """Collect pools from Deadline, if set on instance use these.""" + + order = pyblish.api.CollectorOrder + 0.04 + label = "Deadline Webservice from the Instance" + families = ["rendering", "render.farm", "renderFarm"] + + primary_pool = None + secondary_pool = None + + def process(self, instance): + + if not instance.data.get("primaryPool"): + self.instance.data["primaryPool"] = self.primary_pool + + if not instance.data.get("secondaryPool"): + self.instance.data["secondaryPool"] = self.secondary_pool diff --git a/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml new file mode 100644 index 0000000000..5478ce08e0 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml @@ -0,0 +1,24 @@ + + + +Scene setting + +## Invalid Deadline pools found + +Configured pools don't match what is set in Deadline. + +{invalid_setting_str} + +### How to repair? + + If your instance had deadline pools set on creation, remove or change them.
+In other cases inform admin to change them in Settings. + + Available deadline pools {pools_str}. + +### __Detailed Info__ + +This error is shown when deadline pool is not on Deadline anymore. It could happen in case of republish old workfile which was created with previous deadline pools, + or someone changed pools on Deadline side, but didn't modify Openpype Settings. + +
\ No newline at end of file diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index c499c14d40..1295d40654 100644 --- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -37,8 +37,6 @@ class AfterEffectsSubmitDeadline( priority = 50 chunk_size = 1000000 - primary_pool = None - secondary_pool = None group = None department = None multiprocess = True @@ -62,8 +60,8 @@ class AfterEffectsSubmitDeadline( dln_job_info.Frames = frame_range dln_job_info.Priority = self.priority - dln_job_info.Pool = self.primary_pool - dln_job_info.SecondaryPool = self.secondary_pool + dln_job_info.Pool = self._instance.data.get("primaryPool") + dln_job_info.SecondaryPool = self._instance.data.get("secondaryPool") dln_job_info.Group = self.group dln_job_info.Department = self.department dln_job_info.ChunkSize = self.chunk_size diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index 918efb6630..e320b6df4b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -241,8 +241,6 @@ class HarmonySubmitDeadline( optional = True use_published = False - primary_pool = "" - secondary_pool = "" priority = 50 chunk_size = 1000000 group = "none" @@ -259,8 +257,8 @@ class HarmonySubmitDeadline( # for now, get those from presets. Later on it should be # configurable in Harmony UI directly. job_info.Priority = self.priority - job_info.Pool = self.primary_pool - job_info.SecondaryPool = self.secondary_pool + job_info.Pool = self._instance.data.get("primaryPool") + job_info.SecondaryPool = self._instance.data.get("secondaryPool") job_info.ChunkSize = self.chunk_size job_info.BatchName = os.path.basename(self._instance.data["source"]) job_info.Department = self.department 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 59aeb68b79..82ff723e84 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -7,7 +7,7 @@ from avalon import api import pyblish.api -import hou +# import hou ??? class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): @@ -71,7 +71,8 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": "Houdini", - "Pool": "houdini_redshift", # todo: remove hardcoded pool + "Pool": instance.data.get("primaryPool"), + "secondaryPool": instance.data.get("secondaryPool"), "Frames": frames, "ChunkSize": instance.data.get("chunkSize", 10), diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 9b5800c33f..2980193254 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -28,8 +28,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): priority = 50 chunk_size = 1 concurrent_tasks = 1 - primary_pool = "" - secondary_pool = "" group = "" department = "" limit_groups = {} @@ -187,8 +185,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "Department": self.department, - "Pool": self.primary_pool, - "SecondaryPool": self.secondary_pool, + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), "Group": self.group, "Plugin": "Nuke", diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 3c4e0d2913..74592e4552 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -259,8 +259,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Priority": priority, "Group": self.deadline_group, - "Pool": self.deadline_pool, - "SecondaryPool": self.deadline_pool_secondary, + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), "OutputDirectory0": output_dir }, diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py new file mode 100644 index 0000000000..147829456f --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -0,0 +1,47 @@ +import pyblish.api + +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin +) +from openpype.modules.deadline.deadline_module import DeadlineModule + + +class ValidateDeadlinePools(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): + """Validate primaryPool and secondaryPool on instance. + + Values are on instance based on value insertion when Creating instance or + by Settings in CollectDeadlinePools. + """ + + label = "Validate Deadline Web Service" + order = pyblish.api.ValidatorOrder + families = ["rendering", "render.farm", "renderFarm"] + optional = True + + def process(self, instance): + # get default deadline webservice url from deadline module + deadline_url = instance.context.data["defaultDeadline"] + self.log.info("deadline_url::{}".format(deadline_url)) + pools = DeadlineModule.get_deadline_pools(deadline_url, log=self.log) + self.log.info("pools::{}".format(pools)) + + formatting_data = { + "pools_str": ",".join(pools) + } + + primary_pool = instance.data.get("primaryPool") + if primary_pool and primary_pool not in pools: + msg = "Configured primary '{}' not present on Deadline".format( + instance.data["primaryPool"]) + + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + secondary_pool = instance.data.get("secondaryPool") + if secondary_pool and secondary_pool not in pools: + msg = "Configured secondary '{}' not present on Deadline".format( + instance.data["secondaryPool"]) + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1ef169e387..ef017dd709 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -4,6 +4,10 @@ "CollectDefaultDeadlineServer": { "pass_mongo_url": false }, + "CollectDeadlinePools": { + "primary_pool": "", + "secondary_pool": "" + }, "ValidateExpectedFiles": { "enabled": true, "active": true, @@ -38,8 +42,6 @@ "priority": 50, "chunk_size": 10, "concurrent_tasks": 1, - "primary_pool": "", - "secondary_pool": "", "group": "", "department": "", "use_gpu": true, @@ -54,8 +56,6 @@ "use_published": true, "priority": 50, "chunk_size": 10000, - "primary_pool": "", - "secondary_pool": "", "group": "", "department": "" }, @@ -66,8 +66,6 @@ "use_published": true, "priority": 50, "chunk_size": 10000, - "primary_pool": "", - "secondary_pool": "", "group": "", "department": "", "multiprocess": true diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 5bf0a81a4d..cd1741ba8b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -30,6 +30,24 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectDeadlinePools", + "label": "Default Deadline Pools", + "children": [ + { + "type": "text", + "key": "primary_pool", + "label": "Primary Pool" + }, + { + "type": "text", + "key": "secondary_pool", + "label": "Secondary Pool" + } + ] + }, { "type": "dict", "collapsible": true, @@ -223,16 +241,6 @@ { "type": "splitter" }, - { - "type": "text", - "key": "primary_pool", - "label": "Primary Pool" - }, - { - "type": "text", - "key": "secondary_pool", - "label": "Secondary Pool" - }, { "type": "text", "key": "group", @@ -313,16 +321,6 @@ "key": "chunk_size", "label": "Chunk Size" }, - { - "type": "text", - "key": "primary_pool", - "label": "Primary Pool" - }, - { - "type": "text", - "key": "secondary_pool", - "label": "Secondary Pool" - }, { "type": "text", "key": "group", @@ -372,16 +370,6 @@ "key": "chunk_size", "label": "Chunk Size" }, - { - "type": "text", - "key": "primary_pool", - "label": "Primary Pool" - }, - { - "type": "text", - "key": "secondary_pool", - "label": "Secondary Pool" - }, { "type": "text", "key": "group", From 71d2185593eab6e619e5412763fdd479643b6448 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 11:47:06 +0200 Subject: [PATCH 279/337] OP-2005 - added new get_deadline_pools method --- openpype/modules/deadline/deadline_module.py | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/modules/deadline/deadline_module.py b/openpype/modules/deadline/deadline_module.py index 1a179e9aaf..0ac41ca874 100644 --- a/openpype/modules/deadline/deadline_module.py +++ b/openpype/modules/deadline/deadline_module.py @@ -1,8 +1,20 @@ import os +import requests +import six +import sys + + +from openpype.lib import requests_get from openpype.modules import OpenPypeModule from openpype_interfaces import IPluginPaths +class DeadlineWebserviceError(Exception): + """ + Exception to throw when connection to Deadline server fails. + """ + + class DeadlineModule(OpenPypeModule, IPluginPaths): name = "deadline" @@ -32,3 +44,37 @@ class DeadlineModule(OpenPypeModule, IPluginPaths): return { "publish": [os.path.join(current_dir, "plugins", "publish")] } + + @staticmethod + def get_deadline_pools(webservice, log=None): + # type: (str) -> list + """Get pools from Deadline. + Args: + webservice (str): Server url. + log (Logger) + Returns: + list: Pools. + Throws: + RuntimeError: If deadline webservice is unreachable. + + """ + if not log: + from openpype.lib import PypeLogger + + log = PypeLogger().get_logger(__name__) + + argument = "{}/api/pools?NamesOnly=true".format(webservice) + try: + response = requests_get(argument) + except requests.exceptions.ConnectionError as exc: + msg = 'Cannot connect to DL web service {}'.format(webservice) + log.error(msg) + six.reraise( + DeadlineWebserviceError, + DeadlineWebserviceError('{} - {}'.format(msg, exc)), + sys.exc_info()[2]) + if not response.ok: + log.warning("No pools retrieved") + return [] + + return response.json() From 92d30d5d19fb77d9fdaf3f0c288871dd080969eb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 12:48:26 +0200 Subject: [PATCH 280/337] OP-2005 - push through pool values to renderlayer instance --- openpype/hosts/maya/plugins/publish/collect_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index a525b562f3..5a8ea7efaf 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -342,6 +342,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, + "primaryPool": render_instance.data.get("primaryPool"), + "secondaryPool": render_instance.data.get("secondaryPool"), "priority": render_instance.data.get("priority"), "convertToScanline": render_instance.data.get( "convertToScanline") or False, From de9e762fc08d837c9a0bbb83107310dda1e2596f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 13:27:33 +0200 Subject: [PATCH 281/337] OP-2005 - get string values for pool instead enum index --- openpype/hosts/maya/plugins/publish/collect_render.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 5a8ea7efaf..0e4e27ab51 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -342,8 +342,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, - "primaryPool": render_instance.data.get("primaryPool"), - "secondaryPool": render_instance.data.get("secondaryPool"), "priority": render_instance.data.get("priority"), "convertToScanline": render_instance.data.get( "convertToScanline") or False, @@ -388,6 +386,12 @@ class CollectMayaRender(pyblish.api.ContextPlugin): overrides = self.parse_options(str(render_globals)) data.update(**overrides) + # get string values for pools + primary_pool = overrides["renderGlobals"]["Pool"] + secondary_pool = overrides["renderGlobals"]["SecondaryPool"] + data["primaryPool"] = primary_pool + data["secondaryPool"] = secondary_pool + # Define nice label label = "{0} ({1})".format(expected_layer_name, data["asset"]) label += " [{0}-{1}]".format( From 4a8fb100013d7a8b8b16b4a0452b6be634431f6c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 13:28:22 +0200 Subject: [PATCH 282/337] OP-2005 - fix content of validation xml --- .../publish/help/validate_deadline_pools.xml | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml index 5478ce08e0..0e7d72910e 100644 --- a/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml +++ b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml @@ -1,24 +1,31 @@ - -Scene setting - -## Invalid Deadline pools found + + Scene setting + + ## Invalid Deadline pools found -Configured pools don't match what is set in Deadline. + Configured pools don't match what is set in Deadline. -{invalid_setting_str} + {invalid_value_str} -### How to repair? + ### How to repair? - If your instance had deadline pools set on creation, remove or change them.
-In other cases inform admin to change them in Settings. + If your instance had deadline pools set on creation, remove or + change them. - Available deadline pools {pools_str}. - -### __Detailed Info__ + In other cases inform admin to change them in Settings. -This error is shown when deadline pool is not on Deadline anymore. It could happen in case of republish old workfile which was created with previous deadline pools, - or someone changed pools on Deadline side, but didn't modify Openpype Settings. - + Available deadline pools {pools_str}. +
+ + ### __Detailed Info__ + + This error is shown when deadline pool is not on Deadline anymore. It + could happen in case of republish old workfile which was created with + previous deadline pools, + or someone changed pools on Deadline side, but didn't modify Openpype + Settings. + +
\ No newline at end of file From e9794d0367f1d14013f7005354735d5f3c4ce886 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 13:54:25 +0200 Subject: [PATCH 283/337] OP-2005 - refactor - moved deadline plugins later in order --- .../collect_deadline_server_from_instance.py | 2 +- .../publish/collect_default_deadline_server.py | 2 +- .../deadline/plugins/publish/collect_pools.py | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 1bc4eaa067..a7035cd99f 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -11,7 +11,7 @@ import pyblish.api class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): """Collect Deadline Webservice URL from instance.""" - order = pyblish.api.CollectorOrder + 0.02 + order = pyblish.api.CollectorOrder + 0.415 label = "Deadline Webservice from the Instance" families = ["rendering"] diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index fc056342a8..e6ad6a9aa1 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -6,7 +6,7 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): """Collect default Deadline Webservice URL.""" - order = pyblish.api.CollectorOrder + 0.01 + order = pyblish.api.CollectorOrder + 0.410 label = "Default Deadline Webservice" pass_mongo_url = False diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 4f54cdf211..48130848d5 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -6,19 +6,18 @@ import pyblish.api class CollectDeadlinePools(pyblish.api.InstancePlugin): - """Collect pools from Deadline, if set on instance use these.""" + """Collect pools from instance if present, from Setting otherwise.""" - order = pyblish.api.CollectorOrder + 0.04 - label = "Deadline Webservice from the Instance" - families = ["rendering", "render.farm", "renderFarm"] + order = pyblish.api.CollectorOrder + 0.420 + label = "Collect Deadline Pools" + families = ["rendering", "render.farm", "renderFarm", "renderlayer"] primary_pool = None secondary_pool = None def process(self, instance): - if not instance.data.get("primaryPool"): - self.instance.data["primaryPool"] = self.primary_pool + instance.data["primaryPool"] = self.primary_pool or "none" if not instance.data.get("secondaryPool"): - self.instance.data["secondaryPool"] = self.secondary_pool + instance.data["secondaryPool"] = self.secondary_pool or "none" From 62c30d499bdd881b4054391601539a0e5177b7d3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 13:55:50 +0200 Subject: [PATCH 284/337] OP-2005 - updated validation content --- .../deadline/plugins/publish/validate_deadline_pools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 147829456f..e8f0d95ffb 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -15,9 +15,9 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, by Settings in CollectDeadlinePools. """ - label = "Validate Deadline Web Service" + label = "Validate Deadline Pools" order = pyblish.api.ValidatorOrder - families = ["rendering", "render.farm", "renderFarm"] + families = ["rendering", "render.farm", "renderFarm", "renderlayer"] optional = True def process(self, instance): @@ -35,7 +35,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, if primary_pool and primary_pool not in pools: msg = "Configured primary '{}' not present on Deadline".format( instance.data["primaryPool"]) - + formatting_data["invalid_value_str"] = msg raise PublishXmlValidationError(self, msg, formatting_data=formatting_data) @@ -43,5 +43,6 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, if secondary_pool and secondary_pool not in pools: msg = "Configured secondary '{}' not present on Deadline".format( instance.data["secondaryPool"]) + formatting_data["invalid_value_str"] = msg raise PublishXmlValidationError(self, msg, formatting_data=formatting_data) From aa78ddf5be8937dce5d1c9cb2762a8be84e49e27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 13:57:15 +0200 Subject: [PATCH 285/337] OP-2005 - changed order of create render for AE It should be before DL plugins as they react on family (render.farm in this case). --- openpype/hosts/aftereffects/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 2a4b773681..3e44acd7e9 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -25,7 +25,7 @@ class AERenderInstance(RenderInstance): class CollectAERender(abstract_collect_render.AbstractCollectRender): - order = pyblish.api.CollectorOrder + 0.498 + order = pyblish.api.CollectorOrder + 0.400 label = "Collect After Effects Render Layers" hosts = ["aftereffects"] From b4f8e28e4a4d6f26ba6b5c0cd79115105059dc11 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 14:06:06 +0200 Subject: [PATCH 286/337] OP-2005 - Hound --- .../deadline/plugins/publish/validate_deadline_pools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index e8f0d95ffb..78eed17c98 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -34,7 +34,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, primary_pool = instance.data.get("primaryPool") if primary_pool and primary_pool not in pools: msg = "Configured primary '{}' not present on Deadline".format( - instance.data["primaryPool"]) + instance.data["primaryPool"]) formatting_data["invalid_value_str"] = msg raise PublishXmlValidationError(self, msg, formatting_data=formatting_data) @@ -42,7 +42,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, secondary_pool = instance.data.get("secondaryPool") if secondary_pool and secondary_pool not in pools: msg = "Configured secondary '{}' not present on Deadline".format( - instance.data["secondaryPool"]) + instance.data["secondaryPool"]) formatting_data["invalid_value_str"] = msg raise PublishXmlValidationError(self, msg, formatting_data=formatting_data) From 1f5be56ae2a2e930b2451fd91e120194b14d8d18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Apr 2022 14:39:56 +0200 Subject: [PATCH 287/337] added more logs --- .../hosts/nuke/plugins/publish/extract_slate_frame.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 6935afe144..fb52fc18b4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -189,6 +189,7 @@ class ExtractSlateFrame(openpype.api.Extractor): for key, value in self.key_value_mapping.items(): enabled, template = value if not enabled: + self.log.debug("Key \"{}\" is disabled".format(key)) continue try: @@ -205,13 +206,19 @@ class ExtractSlateFrame(openpype.api.Extractor): except KeyError: self.log.warning( - "Template contains unknown key", + ( + "Template contains unknown key." + " Template \"{}\" Data: {}" + ).format(template, fill_data), exc_info=True ) continue try: node[key].setValue(value) + self.log.info("Change key \"{}\" to value \"{}\"".format( + key, value + )) except NameError: self.log.warning(( "Failed to set value \"{}\" on node attribute \"{}\"" From 786a6494eb32e9bbe0dd448482067647d6fddd12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Apr 2022 14:40:08 +0200 Subject: [PATCH 288/337] fixed settings --- .../defaults/project_settings/nuke.json | 26 ++++---- .../schemas/schema_nuke_publish.json | 66 ++++++++++--------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index bdccb9b38e..ab015271ff 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -161,18 +161,20 @@ }, "ExtractSlateFrame": { "viewer_lut_raw": false, - "f_submission_note": [ - true, - "{comment}" - ], - "f_submitting_for": [ - true, - "{intent[value]}" - ], - "f_vfx_scope_of_work": [ - false, - "" - ] + "key_value_mapping": { + "f_submission_note": [ + true, + "{comment}" + ], + "f_submitting_for": [ + true, + "{intent[value]}" + ], + "f_vfx_scope_of_work": [ + false, + "" + ] + } }, "IncrementScriptVersion": { "enabled": 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 3bf0eb3214..4a796f1933 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 @@ -399,41 +399,47 @@ "word_wrap": true }, { - "type": "list-strict", - "key": "f_submission_note", - "label": "Submission Note:", - "object_types": [ + "type": "dict", + "key": "key_value_mapping", + "children": [ { - "type": "boolean" + "type": "list-strict", + "key": "f_submission_note", + "label": "Submission Note:", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] }, { - "type": "text" - } - ] - }, - { - "type": "list-strict", - "key": "f_submitting_for", - "label": "Submission For:", - "object_types": [ - { - "type": "boolean" + "type": "list-strict", + "key": "f_submitting_for", + "label": "Submission For:", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] }, { - "type": "text" - } - ] - }, - { - "type": "list-strict", - "key": "f_vfx_scope_of_work", - "label": "VFX Scope Of Work:", - "object_types": [ - { - "type": "boolean" - }, - { - "type": "text" + "type": "list-strict", + "key": "f_vfx_scope_of_work", + "label": "VFX Scope Of Work:", + "object_types": [ + { + "type": "boolean" + }, + { + "type": "text" + } + ] } ] } From f49d01559a25e084236a5b5ed936eb16a9006545 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Apr 2022 14:40:27 +0200 Subject: [PATCH 289/337] fix database access in loader --- openpype/tools/utils/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index efaf671915..5abbe01144 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -426,7 +426,10 @@ class FamilyConfigCache: # Make sure connection is installed # - accessing attribute which does not have auto-install self.dbcon.install() - asset_doc = self.dbcon.database[project_name].find_one( + database = getattr(self.dbcon, "database", None) + if database is None: + database = self.dbcon._database + asset_doc = database[project_name].find_one( {"type": "asset", "name": asset_name}, {"data.tasks": True} ) or {} From 30cc08c1da6fe4e5a9b3656d4bd875be209d47b5 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Tue, 12 Apr 2022 14:57:02 +0200 Subject: [PATCH 290/337] Embed YouTube videos --- website/docs/artist_hosts_nuke_tut.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index eefb213dd2..77d9d27d1b 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -144,6 +144,8 @@ This tool will append all available subsets into an actual node graph. It will l This QuickStart is short introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. + + ### Launch Nuke - Shot and Task Context OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. @@ -315,6 +317,8 @@ Main disadvantage of this approach is that you can render only one version of yo When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. + + ### Managing Versions ![Versionless](assets/nuke_tut/nuke_ManageVersion.png) @@ -331,7 +335,11 @@ Use Manage to switch versions for loaded assets. If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version. + + ### Fixing Validate Version If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. -Or maybe you accidentally copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. \ No newline at end of file +Or maybe you accidentally copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. + + From 4b3412e6dd000199810a85a6c868d35651572535 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Tue, 12 Apr 2022 15:33:37 +0200 Subject: [PATCH 291/337] More videos --- website/docs/artist_hosts_hiero.md | 11 ++++++++++ website/docs/artist_hosts_nuke_tut.md | 30 +++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/website/docs/artist_hosts_hiero.md b/website/docs/artist_hosts_hiero.md index f516c3a6e0..dc6f1696e7 100644 --- a/website/docs/artist_hosts_hiero.md +++ b/website/docs/artist_hosts_hiero.md @@ -94,6 +94,8 @@ This tool will set any defined colorspace definition from OpenPype `Settings / P With OpenPype, you can use Hiero/NKS as a starting point for creating a project's **shots** as *assets* from timeline clips with its *hierarchycal parents* like **episodes**, **sequences**, **folders**, and its child **tasks**. Most importantly it will create **versions** of plate *subsets*, with or without **reference video**. Publishig is naturally creating clip's **thumbnails** and assigns it to shot *asset*. Hiero is also publishing **audio** *subset* and various **soft-effects** either as retiming component as part of published plates or **color-tranformations**, that will be evailable later on for compositor artists to use either as *viewport input-process* or *loaded nodes* in graph editor.



+ + ### Preparing timeline for conversion to instances Because we don't support on-fly data conversion so in case of working with raw camera sources or some other formats which need to be converted for 2D/3D work. We suggest to convert those before and reconform the timeline. Before any clips in timeline could be converted to publishable instances we recommend following. 1. Merge all tracks which supposed to be one and they are multiply only because of editor's style @@ -191,3 +193,12 @@ If you wish to change any individual properties of the shot then you are able to + +### Publishing Effects from Hiero to Nuke +This video shows a way to publish shot look as effect from Hiero to Nuke. + + + +### Assembling edit from published shot versions + + diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index 77d9d27d1b..296fdf44d5 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -89,6 +89,8 @@ This menu item will set correct Colorspace definitions for you. All has to be co - set preview LUT to your viewers - set correct colorspace to all discovered Read nodes (following expression set in settings) +See [Nuke Color Management](artist_hosts_nuke_tut.md#nuke-color-management) +
@@ -144,7 +146,7 @@ This tool will append all available subsets into an actual node graph. It will l This QuickStart is short introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. - + ### Launch Nuke - Shot and Task Context OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. @@ -228,6 +230,11 @@ This will create a Group with a Write node inside. You can configure write node parameters in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke → Nodes** ::: +### Create Prerender Node +Creating Prerender is very similar to creating OpenPype managed Write node. + + + #### What Nuke Publish Does From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. @@ -317,7 +324,7 @@ Main disadvantage of this approach is that you can render only one version of yo When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. - + ### Managing Versions @@ -327,19 +334,30 @@ OpenPype checks all the assets loaded to Nuke on script open. All out of date as Use Manage to switch versions for loaded assets. +### Loading Effects +This video show how to publish effect from Hiero / Nuke Studio, and use the effect in Nuke. + + + + + +### Nuke Color Management + + + ## Troubleshooting ### Fixing Validate Containers -![Versionless](assets/nuke_tut/nuke_ValidateContainers.png) - If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version. - +![Versionless](assets/nuke_tut/nuke_ValidateContainers.png) + + ### Fixing Validate Version If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. Or maybe you accidentally copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. - + From 0edeb4c6dc174c251e501f45e02ff37e6f95dbfc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Apr 2022 15:42:45 +0200 Subject: [PATCH 292/337] added init file for worker which triggers missing sound file dialog on worker open --- openpype/hosts/tvpaint/worker/init_file.tvpp | Bin 0 -> 59333 bytes openpype/hosts/tvpaint/worker/worker.py | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 openpype/hosts/tvpaint/worker/init_file.tvpp diff --git a/openpype/hosts/tvpaint/worker/init_file.tvpp b/openpype/hosts/tvpaint/worker/init_file.tvpp new file mode 100644 index 0000000000000000000000000000000000000000..572d278fdb49c619d3cf040f59238377d1062d6a GIT binary patch literal 59333 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 Date: Tue, 12 Apr 2022 17:04:54 +0200 Subject: [PATCH 293/337] fix docstring --- openpype/pipeline/farm/patterning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 5ba7a8df4b..457f3afbca 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -12,7 +12,7 @@ def match_aov_pattern(host_name, aov_patterns, render_file_name): Args: app (str): Host name. - aov_patterns (list): List of AOV patterns from AOV filters. + aov_patterns (dict): AOV patterns from AOV filters. render_file_name (str): Incoming file name to match against. Returns: From 052392ac353198d95c95ae89ea5baff1690e80e1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 12 Apr 2022 17:10:11 +0200 Subject: [PATCH 294/337] fixed default filter and list handling --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++-- openpype/pipeline/farm/patterning.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 63f9e35720..75c8edc8d4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -108,7 +108,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", "vrayscene"] - aov_filter = {"maya": [r".*(?:[\._-])*([Bb]eauty)(?:[\.|_])*.*"], + aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE "harmony": [r".*"], # for everything from AE "celaction": [r".*"]} @@ -130,7 +130,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "OPENPYPE_PUBLISH_JOB" ] - # custom deadline atributes + # custom deadline attributes deadline_department = "" deadline_pool = "" deadline_pool_secondary = "" diff --git a/openpype/pipeline/farm/patterning.py b/openpype/pipeline/farm/patterning.py index 457f3afbca..1e4b5bf37d 100644 --- a/openpype/pipeline/farm/patterning.py +++ b/openpype/pipeline/farm/patterning.py @@ -21,4 +21,4 @@ def match_aov_pattern(host_name, aov_patterns, render_file_name): aov_pattern = aov_patterns.get(host_name, []) if not aov_pattern: return False - return re.match(aov_pattern, render_file_name) is not None + return any(re.match(p, render_file_name) for p in aov_pattern) From 8c3e63c35dc8af7a2ceac5339e7963048ff5b773 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 17:54:56 +0200 Subject: [PATCH 295/337] OP-2005 - set default from Settings to dropdown --- .../maya/plugins/create/create_render.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 2ded7c720d..15230519d2 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -204,7 +204,6 @@ class CreateRender(plugin.Creator): def _deadline_webservice_changed(self): """Refresh Deadline server dependent options.""" # get selected server - from maya import cmds webservice = self.deadline_servers[ self.server_aliases[ cmds.getAttr("{}.deadlineServers".format(self.instance)) @@ -213,12 +212,23 @@ class CreateRender(plugin.Creator): pools = self.deadline_module.get_deadline_pools(webservice, self.log) cmds.deleteAttr("{}.primaryPool".format(self.instance)) cmds.deleteAttr("{}.secondaryPool".format(self.instance)) + + pool_setting = (self._project_settings["deadline"] + ["publish"] + ["CollectDeadlinePools"]) + + primary_pool = pool_setting["primary_pool"] + sorted_pools = self._set_default_pool(list(pools), primary_pool) cmds.addAttr(self.instance, longName="primaryPool", attributeType="enum", - enumName=":".join(pools)) - cmds.addAttr(self.instance, longName="secondaryPool", + enumName=":".join(sorted_pools)) + + pools = ["-"] + pools + secondary_pool = pool_setting["secondary_pool"] + sorted_pools = self._set_default_pool(list(pools), secondary_pool) + cmds.addAttr("{}.secondaryPool".format(self.instance), attributeType="enum", - enumName=":".join(["-"] + pools)) + enumName=":".join(sorted_pools)) def _create_render_settings(self): """Create instance settings.""" @@ -299,12 +309,27 @@ class CreateRender(plugin.Creator): self.log.info(" - pool: {}".format(pool["name"])) pool_names.append(pool["name"]) - self.data["primaryPool"] = pool_names + pool_setting = (self._project_settings["deadline"] + ["publish"] + ["CollectDeadlinePools"]) + primary_pool = pool_setting["primary_pool"] + self.data["primaryPool"] = self._set_default_pool(pool_names, + primary_pool) # We add a string "-" to allow the user to not # set any secondary pools - self.data["secondaryPool"] = ["-"] + pool_names + pool_names = ["-"] + pool_names + secondary_pool = pool_setting["secondary_pool"] + self.data["secondaryPool"] = self._set_default_pool(pool_names, + secondary_pool) self.options = {"useSelection": False} # Force no content + def _set_default_pool(self, pool_names, pool_value): + """Reorder pool names, default should come first""" + if pool_value and pool_value in pool_names: + pool_names.remove(pool_value) + pool_names = [pool_value] + pool_names + return pool_names + def _load_credentials(self): """Load Muster credentials. From 9b1a739e18fa1229d1ceb3e257979eba8e132c40 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 17:56:18 +0200 Subject: [PATCH 296/337] OP-2005 - set default from Settings to dropdown --- openpype/hosts/maya/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 0e4e27ab51..c229ca226f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -388,7 +388,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # get string values for pools primary_pool = overrides["renderGlobals"]["Pool"] - secondary_pool = overrides["renderGlobals"]["SecondaryPool"] + secondary_pool = overrides["renderGlobals"].get("SecondaryPool") data["primaryPool"] = primary_pool data["secondaryPool"] = secondary_pool From 9306314a1bd805ce8d64937e70526cfbc8c91d1e Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 12 Apr 2022 17:48:20 +0100 Subject: [PATCH 297/337] Update collect_render.py Better error message for non selected render camera edge case. --- openpype/hosts/maya/plugins/publish/collect_render.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index a525b562f3..7791169791 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -194,11 +194,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): assert render_products, "no render products generated" exp_files = [] multipart = False + render_cameras = [] for product in render_products: if product.multipart: multipart = True product_name = product.productName if product.camera and layer_render_products.has_camera_token(): + render_cameras.append(product.camera) product_name = "{}{}".format( product.camera, "_" + product_name if product_name else "") @@ -208,6 +210,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): product) }) + assert render_cameras, "No render cameras found." + self.log.info("multipart: {}".format( multipart)) assert exp_files, "no file names were generated, this is bug" From 432f1264066f7286ec1dd4a3d1e3ccfbf2903046 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 12 Apr 2022 17:51:51 +0100 Subject: [PATCH 298/337] Update collect_render.py Hound --- openpype/hosts/maya/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 7791169791..154f0376ae 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -211,7 +211,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): }) assert render_cameras, "No render cameras found." - + self.log.info("multipart: {}".format( multipart)) assert exp_files, "no file names were generated, this is bug" From a5dd9f4e0e1822b2314dbf9c13a0cd2e1abb034b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Apr 2022 18:55:57 +0200 Subject: [PATCH 299/337] renamed process_context to context_tools --- openpype/hosts/celaction/api/cli.py | 2 +- openpype/pipeline/__init__.py | 2 +- openpype/pipeline/{process_context.py => context_tools.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename openpype/pipeline/{process_context.py => context_tools.py} (100%) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index ef73c7457a..8c7b3a2e74 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -11,7 +11,7 @@ import openpype import openpype.hosts.celaction from openpype.hosts.celaction import api as celaction from openpype.tools.utils import host_tools -from openpype.pipeline.process_context import install_openpype_plugins +from openpype.pipeline import install_openpype_plugins log = Logger().get_logger("Celaction_cli_publisher") diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index de04548616..308be6da64 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -69,7 +69,7 @@ from .actions import ( deregister_inventory_action_path, ) -from .process_context import ( +from .context_tools import ( install_openpype_plugins, install_host, uninstall_host, diff --git a/openpype/pipeline/process_context.py b/openpype/pipeline/context_tools.py similarity index 100% rename from openpype/pipeline/process_context.py rename to openpype/pipeline/context_tools.py From e14a5eb98aff1eb61920fc70d08291ad3e8af119 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Apr 2022 20:02:43 +0200 Subject: [PATCH 300/337] OP-2005 - refactor - changed logger import --- openpype/modules/deadline/deadline_module.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/deadline/deadline_module.py b/openpype/modules/deadline/deadline_module.py index 0ac41ca874..c30db75188 100644 --- a/openpype/modules/deadline/deadline_module.py +++ b/openpype/modules/deadline/deadline_module.py @@ -3,8 +3,7 @@ import requests import six import sys - -from openpype.lib import requests_get +from openpype.lib import requests_get, PypeLogger from openpype.modules import OpenPypeModule from openpype_interfaces import IPluginPaths @@ -59,9 +58,7 @@ class DeadlineModule(OpenPypeModule, IPluginPaths): """ if not log: - from openpype.lib import PypeLogger - - log = PypeLogger().get_logger(__name__) + log = PypeLogger.get_logger(__name__) argument = "{}/api/pools?NamesOnly=true".format(webservice) try: From e4fe82914f2dcb20790f37e13e037f8585984c7d Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Tue, 12 Apr 2022 18:17:36 -0700 Subject: [PATCH 301/337] Add IGG logo to index page. --- website/src/pages/index.js | 5 +++++ website/static/img/igg-logo.png | Bin 0 -> 80982 bytes 2 files changed, 5 insertions(+) create mode 100644 website/static/img/igg-logo.png diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 791b309bbc..d9bbc3eaa0 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -144,6 +144,11 @@ const studios = [ title: "Ember Light", image: "/img/EmberLight_black.png", infoLink: "https://emberlight.se/", + }, + { + title: "IGG Canada", + image: "/img/igg-logo.png", + infoLink: "https://www.igg.com/", } ]; diff --git a/website/static/img/igg-logo.png b/website/static/img/igg-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a169600b758f1486a2f25698341acd8425252d1d GIT binary patch literal 80982 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 W~yss-9u~Hzxp4o=_|1S literal 0 HcmV?d00001 From c5069096d6dbe369fe1bb25cffec0888172ae62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 13 Apr 2022 13:38:05 +0200 Subject: [PATCH 302/337] fixing module file and list indexing --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- openpype/pipeline/farm/__init__.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 openpype/pipeline/farm/__init__.py diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 5befae0794..c444c3aa53 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -540,7 +540,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance.get("multipartExr", False): preview = True else: - render_file_name = list(collection[0]) + render_file_name = list(collection)[0] host_name = os.environ.get("AVALON_APP", "") # if filtered aov name is found in filename, toggle it for # preview video rendering diff --git a/openpype/pipeline/farm/__init__.py b/openpype/pipeline/farm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 4ceaa605c3522dcfadfb8926b6aea31b80a7be62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Apr 2022 13:50:31 +0200 Subject: [PATCH 303/337] fix tray publisher installation --- openpype/tools/traypublisher/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index d0453c4f23..a550c88ead 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -8,8 +8,8 @@ publishing plugins. from Qt import QtWidgets, QtCore -import avalon.api from avalon.api import AvalonMongoDB +from openpype.pipeline import install_host from openpype.hosts.traypublisher import ( api as traypublisher ) @@ -163,7 +163,7 @@ class TrayPublishWindow(PublisherWindow): def main(): - avalon.api.install(traypublisher) + install_host(traypublisher) app = QtWidgets.QApplication([]) window = TrayPublishWindow() window.show() From d2533a6a1b22c38a03b7841cf967ed238444d932 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Apr 2022 17:28:35 +0200 Subject: [PATCH 304/337] OP-2958 - add aov matching even for remainder Previously filtering used only for collections. --- .../deadline/plugins/publish/submit_publish_job.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c444c3aa53..af388dce99 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -524,6 +524,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ representations = [] + host_name = os.environ.get("AVALON_APP", "") collections, remainders = clique.assemble(exp_files) # create representation for every collected sequento ce @@ -541,7 +542,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = True else: render_file_name = list(collection)[0] - host_name = os.environ.get("AVALON_APP", "") # if filtered aov name is found in filename, toggle it for # preview video rendering preview = match_aov_pattern( @@ -610,7 +610,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "files": os.path.basename(remainder), "stagingDir": os.path.dirname(remainder), } - if "render" in instance.get("families"): + + preview = match_aov_pattern( + host_name, self.aov_filter, remainder + ) + if preview: rep.update({ "fps": instance.get("fps"), "tags": ["review"] From fe38ff5e230e89722800e95731cc09ef4da7b1db Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Apr 2022 18:21:46 +0200 Subject: [PATCH 305/337] OP-2958 - extended logging for profile filtering Updated logging for collect_ftrack_famiy --- openpype/lib/profiles_filtering.py | 27 ++++++++++--------- .../plugins/publish/collect_ftrack_family.py | 8 ++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/lib/profiles_filtering.py b/openpype/lib/profiles_filtering.py index 0bb901aff8..370703a68b 100644 --- a/openpype/lib/profiles_filtering.py +++ b/openpype/lib/profiles_filtering.py @@ -44,12 +44,6 @@ def _profile_exclusion(matching_profiles, logger): Returns: dict: Most matching profile. """ - - logger.info( - "Search for first most matching profile in match order:" - " Host name -> Task name -> Family." - ) - if not matching_profiles: return None @@ -168,6 +162,15 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): _keys_order.append(key) keys_order = tuple(_keys_order) + log_parts = " | ".join([ + "{}: \"{}\"".format(*item) + for item in key_values.items() + ]) + + logger.info( + "Looking for matching profile for: {}".format(log_parts) + ) + matching_profiles = None highest_profile_points = -1 # Each profile get 1 point for each matching filter. Profile with most @@ -205,11 +208,6 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): if profile_points == highest_profile_points: matching_profiles.append((profile, profile_scores)) - log_parts = " | ".join([ - "{}: \"{}\"".format(*item) - for item in key_values.items() - ]) - if not matching_profiles: logger.info( "None of profiles match your setup. {}".format(log_parts) @@ -221,4 +219,9 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): "More than one profile match your setup. {}".format(log_parts) ) - return _profile_exclusion(matching_profiles, logger) + profile = _profile_exclusion(matching_profiles, logger) + if profile: + logger.info( + "Profile selected: {}".format(profile) + ) + return profile diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 95987fe42e..5bfe0dea8b 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -34,6 +34,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): self.log.warning("No profiles present for adding Ftrack family") return + add_ftrack_family = False task_name = instance.data.get("task", avalon.api.Session["AVALON_TASK"]) host_name = avalon.api.Session["AVALON_APP"] @@ -69,6 +70,13 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): else: instance.data["families"] = ["ftrack"] + result_str = "Adding" + if not add_ftrack_family: + result_str = "Not adding" + self.log.info("{} 'ftrack' family for instance with '{}'".format( + result_str, family + )) + def _get_add_ftrack_f_from_addit_filters(self, additional_filters, families, From 7a4db3946eda42ce907c647dab9c713569992c3b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Apr 2022 18:27:51 +0200 Subject: [PATCH 306/337] OP-2958 - updated logging for collect_ftrack_famiy --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 5bfe0dea8b..158135c952 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -54,6 +54,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): additional_filters = profile.get("advanced_filtering") if additional_filters: + self.log.info("'{}' families used for additional filtering". + format(families)) add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( additional_filters, families, From 537198f32e63e919ed7cd21776d047ca4ebb86d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Apr 2022 18:31:59 +0200 Subject: [PATCH 307/337] OP-2958 - fix not using calculated preview --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index af388dce99..4f781de62d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -619,7 +619,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "fps": instance.get("fps"), "tags": ["review"] }) - self._solve_families(instance, True) + self._solve_families(instance, preview) already_there = False for repre in instance.get("representations", []): From 2b031ada8aa52c8a924800a84c6dd5b2aeb6440e Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 13 Apr 2022 10:38:03 -0700 Subject: [PATCH 308/337] Update icon to be B/W --- website/static/img/igg-logo.png | Bin 80982 -> 80331 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/static/img/igg-logo.png b/website/static/img/igg-logo.png index a169600b758f1486a2f25698341acd8425252d1d..3c7f7718f79ae866bd9f07bd51878f5d2a6eceea 100644 GIT binary patch literal 80331 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 z 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 CA{hw) literal 80982 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 W~yss-9u~Hzxp4o=_|1S From 7d521c15ea634476efca81f5e5ab16b3f764f8b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Apr 2022 22:21:56 +0200 Subject: [PATCH 309/337] fixed typos in imports from openpype.hosts.nuke.api.command --- openpype/hosts/nuke/plugins/inventory/select_containers.py | 2 +- openpype/hosts/nuke/plugins/load/load_backdrop.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/inventory/select_containers.py b/openpype/hosts/nuke/plugins/inventory/select_containers.py index d7d5f00b87..4e7a20fb26 100644 --- a/openpype/hosts/nuke/plugins/inventory/select_containers.py +++ b/openpype/hosts/nuke/plugins/inventory/select_containers.py @@ -1,5 +1,5 @@ from openpype.pipeline import InventoryAction -from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop +from openpype.hosts.nuke.api.command import viewer_update_and_undo_stop class SelectContainers(InventoryAction): diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 36cec6f4c5..d55dd4cf71 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -14,7 +14,7 @@ from openpype.hosts.nuke.api.lib import ( get_avalon_knob_data, set_avalon_knob_data ) -from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop +from openpype.hosts.nuke.api.command import viewer_update_and_undo_stop from openpype.hosts.nuke.api import containerise, update_container From 654d0e3ada68e38cf01f2b5df680be73226b26d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Apr 2022 11:30:01 +0200 Subject: [PATCH 310/337] Update tests/unit/openpype/modules/sync_server/test_module_api.py Co-authored-by: Roy Nieterau --- tests/unit/openpype/modules/sync_server/test_module_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/openpype/modules/sync_server/test_module_api.py b/tests/unit/openpype/modules/sync_server/test_module_api.py index b6ba2a01b6..14613604dd 100644 --- a/tests/unit/openpype/modules/sync_server/test_module_api.py +++ b/tests/unit/openpype/modules/sync_server/test_module_api.py @@ -33,7 +33,7 @@ class TestModuleApi(ModuleUnitTest): yield sync_server def test_get_alt_site_pairs(self, setup_sync_server_module): - conf_sites = {'SFTP': {"alternative_sites": ["studio"]}, + conf_sites = {"SFTP": {"alternative_sites": ["studio"]}, "studio2": {"alternative_sites": ["studio"]}} ret = setup_sync_server_module._get_alt_site_pairs(conf_sites) From 5e41fbeb91e6ba02242449330006f3b9abf940a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Apr 2022 11:39:44 +0200 Subject: [PATCH 311/337] Elaborated on task shor name Requested on discord --- website/docs/admin_settings_project_anatomy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index 98003dc381..1ed877bb8a 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -59,7 +59,7 @@ We have a few required anatomy templates for OpenPype to work properly, however | `asset` | Name of asset or shot | | `task[name]` | Name of task | | `task[type]` | Type of task | -| `task[short]` | Shortname of task | +| `task[short]` | Short name of task type (eg. 'Modeling' > 'mdl') | | `parent` | Name of hierarchical parent | | `version` | Version number | | `subset` | Subset name | From 18f9fa845099b3a7d21f2c7004cde6b49c10a8fe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Apr 2022 11:44:33 +0200 Subject: [PATCH 312/337] Added screenshot of tasks configuration --- website/docs/admin_settings_project_anatomy.md | 3 +++ website/docs/assets/settings/anatomy_tasks.png | Bin 0 -> 27116 bytes 2 files changed, 3 insertions(+) create mode 100644 website/docs/assets/settings/anatomy_tasks.png diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index 1ed877bb8a..b98819cd8a 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -105,5 +105,8 @@ We have a few required anatomy templates for OpenPype to work properly, however ## Task Types +Current state of default Task descriptors. + +![tasks](assets/settings/anatomy_tasks.png) ## Colour Management and Formats \ No newline at end of file diff --git a/website/docs/assets/settings/anatomy_tasks.png b/website/docs/assets/settings/anatomy_tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..16265cf8eb6d20a19720c33f734d009e379e3d8c GIT binary patch literal 27116 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

0AoXGxL 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 Date: Thu, 14 Apr 2022 12:39:34 +0200 Subject: [PATCH 313/337] fix host install in fusion script --- openpype/scripts/fusion_switch_shot.py | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index 85a5821c6e..3ba150902e 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -4,13 +4,16 @@ import sys import logging # Pipeline imports -from avalon import api, io -import avalon.fusion +from avalon import io +from openpype.hosts.fusion import api +import openpype.hosts.fusion.api.lib as fusion_lib # Config imports -import openpype.lib as pype -from openpype.pipeline import registered_host -import openpype.hosts.fusion.lib as fusion_lib +from openpype.lib import version_up +from openpype.pipeline import ( + install_host, + registered_host, +) from openpype.lib.avalon_context import get_workdir_from_session @@ -80,7 +83,7 @@ def _format_filepath(session): # Create new unqiue filepath if os.path.exists(new_filepath): - new_filepath = pype.version_up(new_filepath) + new_filepath = version_up(new_filepath) return new_filepath @@ -103,7 +106,7 @@ def _update_savers(comp, session): comp.Print("New renders to: %s\n" % renders) - with avalon.fusion.comp_lock_and_undo_chunk(comp): + with api.comp_lock_and_undo_chunk(comp): savers = comp.GetToolList(False, "Saver").values() for saver in savers: filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] @@ -165,12 +168,12 @@ def switch(asset_name, filepath=None, new=True): # Get current project self._project = io.find_one({ "type": "project", - "name": api.Session["AVALON_PROJECT"] + "name": io.Session["AVALON_PROJECT"] }) # Go to comp if not filepath: - current_comp = avalon.fusion.get_current_comp() + current_comp = api.get_current_comp() assert current_comp is not None, "Could not find current comp" else: fusion = _get_fusion_instance() @@ -195,7 +198,7 @@ def switch(asset_name, filepath=None, new=True): current_comp.Print(message) # Build the session to switch to - switch_to_session = api.Session.copy() + switch_to_session = io.Session.copy() switch_to_session["AVALON_ASSET"] = asset['name'] if new: @@ -204,7 +207,7 @@ def switch(asset_name, filepath=None, new=True): # Update savers output based on new session _update_savers(current_comp, switch_to_session) else: - comp_path = pype.version_up(filepath) + comp_path = version_up(filepath) current_comp.Print(comp_path) @@ -235,7 +238,7 @@ if __name__ == '__main__': args, unknown = parser.parse_args() - api.install(avalon.fusion) + install_host(api) switch(args.asset_name, args.file_path) sys.exit(0) From 0882589209344b672e03b22c87a80b1a9ae64c2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Apr 2022 12:41:27 +0200 Subject: [PATCH 314/337] fixed imports --- .../modules/clockify/launcher_actions/ClockifyStart.py | 8 +++++--- .../modules/clockify/launcher_actions/ClockifySync.py | 9 ++++++--- openpype/scripts/non_python_host_launch.py | 2 +- openpype/tools/stdout_broker/window.py | 8 +++++--- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/openpype/modules/clockify/launcher_actions/ClockifyStart.py b/openpype/modules/clockify/launcher_actions/ClockifyStart.py index db51964eb7..6428d5e7aa 100644 --- a/openpype/modules/clockify/launcher_actions/ClockifyStart.py +++ b/openpype/modules/clockify/launcher_actions/ClockifyStart.py @@ -1,12 +1,14 @@ -from avalon import api, io +from avalon import io + from openpype.api import Logger +from openpype.pipeline import LauncherAction from openpype_modules.clockify.clockify_api import ClockifyAPI -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) -class ClockifyStart(api.Action): +class ClockifyStart(LauncherAction): name = "clockify_start_timer" label = "Clockify - Start Timer" diff --git a/openpype/modules/clockify/launcher_actions/ClockifySync.py b/openpype/modules/clockify/launcher_actions/ClockifySync.py index 02982d373a..3c81e2766c 100644 --- a/openpype/modules/clockify/launcher_actions/ClockifySync.py +++ b/openpype/modules/clockify/launcher_actions/ClockifySync.py @@ -1,10 +1,13 @@ -from avalon import api, io +from avalon import io + from openpype_modules.clockify.clockify_api import ClockifyAPI from openpype.api import Logger -log = Logger().get_logger(__name__) +from openpype.pipeline import LauncherAction + +log = Logger.get_logger(__name__) -class ClockifySync(api.Action): +class ClockifySync(LauncherAction): name = "sync_to_clockify" label = "Sync to Clockify" diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 43921f0483..f795af7bb3 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -15,7 +15,7 @@ CURRENT_FILE = os.path.abspath(__file__) def show_error_messagebox(title, message, detail_message=None): """Function will show message and process ends after closing it.""" from Qt import QtWidgets, QtCore - from avalon import style + from openpype import style app = QtWidgets.QApplication([]) app.setStyleSheet(style.load_stylesheet()) diff --git a/openpype/tools/stdout_broker/window.py b/openpype/tools/stdout_broker/window.py index a2190e0491..f5720ca05b 100644 --- a/openpype/tools/stdout_broker/window.py +++ b/openpype/tools/stdout_broker/window.py @@ -1,7 +1,9 @@ -from avalon import style -from Qt import QtWidgets, QtCore -import collections import re +import collections + +from Qt import QtWidgets + +from openpype import style class ConsoleDialog(QtWidgets.QDialog): From f8b3d3e9b144e5c527b382da3bd19630523e77e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Apr 2022 17:30:22 +0200 Subject: [PATCH 315/337] updated push-protected action in github workflow --- .github/workflows/prerelease.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index d9b4d8089c..5acd20007c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -80,7 +80,7 @@ jobs: git tag -a $tag_name -m "nightly build" - name: Push to protected main branch - uses: CasperWA/push-protected@v2 + uses: CasperWA/push-protected@v2.10.0 with: token: ${{ secrets.ADMIN_TOKEN }} branch: main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 917e6c884c..85864b4442 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: - name: 🔏 Push to protected main branch if: steps.version.outputs.release_tag != 'skip' - uses: CasperWA/push-protected@v2 + uses: CasperWA/push-protected@v2.10.0 with: token: ${{ secrets.ADMIN_TOKEN }} branch: main From 88d20b8cfd7c3e8b4c157e2dc1e265364d2b8e4c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 14 Apr 2022 16:28:00 +0000 Subject: [PATCH 316/337] [Automated] Bump version --- CHANGELOG.md | 47 ++++++++++++++++++++++++++------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c216dd0595..8cca62cca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,45 @@ # Changelog -## [3.9.4-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.4-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...HEAD) ### 📖 Documentation +- Documentation: more info about Tasks [\#3062](https://github.com/pypeclub/OpenPype/pull/3062) - Documentation: Python requirements to 3.7.9 [\#3035](https://github.com/pypeclub/OpenPype/pull/3035) - Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) +**🆕 New features** + +- General: Local overrides for environment variables [\#3045](https://github.com/pypeclub/OpenPype/pull/3045) +- Flame: Flare integration preparation [\#2928](https://github.com/pypeclub/OpenPype/pull/2928) + **🚀 Enhancements** +- TVPaint: Added init file for worker to triggers missing sound file dialog [\#3053](https://github.com/pypeclub/OpenPype/pull/3053) +- Ftrack: Custom attributes can be filled in slate values [\#3036](https://github.com/pypeclub/OpenPype/pull/3036) - Resolve environment variable in google drive credential path [\#3008](https://github.com/pypeclub/OpenPype/pull/3008) **🐛 Bug fixes** +- GitHub: Updated push-protected action in github workflow [\#3064](https://github.com/pypeclub/OpenPype/pull/3064) +- Nuke: Typos in imports from Nuke implementation [\#3061](https://github.com/pypeclub/OpenPype/pull/3061) +- Hotfix: fixing deadline job publishing [\#3059](https://github.com/pypeclub/OpenPype/pull/3059) +- General: Extract Review handle invalid characters for ffmpeg [\#3050](https://github.com/pypeclub/OpenPype/pull/3050) +- Slate Review: Support to keep format on slate concatenation [\#3049](https://github.com/pypeclub/OpenPype/pull/3049) +- Webpublisher: fix processing of workfile [\#3048](https://github.com/pypeclub/OpenPype/pull/3048) - Ftrack: Integrate ftrack api fix [\#3044](https://github.com/pypeclub/OpenPype/pull/3044) - Webpublisher - removed wrong hardcoded family [\#3043](https://github.com/pypeclub/OpenPype/pull/3043) +- LibraryLoader: Use current project for asset query in families filter [\#3042](https://github.com/pypeclub/OpenPype/pull/3042) +- SiteSync: Providers ignore that site is disabled [\#3041](https://github.com/pypeclub/OpenPype/pull/3041) - Unreal: Creator import fixes [\#3040](https://github.com/pypeclub/OpenPype/pull/3040) +- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\#3018](https://github.com/pypeclub/OpenPype/pull/3018) + +**Merged pull requests:** + +- Deadline: reworked pools assignment [\#3051](https://github.com/pypeclub/OpenPype/pull/3051) +- Houdini: Avoid ImportError on `hdefereval` when Houdini runs without UI [\#2987](https://github.com/pypeclub/OpenPype/pull/2987) ## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07) @@ -38,24 +60,23 @@ - Ftrack: Add more options for note text of integrate ftrack note [\#3025](https://github.com/pypeclub/OpenPype/pull/3025) - Console Interpreter: Changed how console splitter size are reused on show [\#3016](https://github.com/pypeclub/OpenPype/pull/3016) - Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) +- General: default workfile subset name for workfile [\#3011](https://github.com/pypeclub/OpenPype/pull/3011) - Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) -- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) **🐛 Bug fixes** - Deadline: Fixed default value of use sequence for review [\#3033](https://github.com/pypeclub/OpenPype/pull/3033) - Settings UI: Version column can be extended so version are visible [\#3032](https://github.com/pypeclub/OpenPype/pull/3032) +- General: Fix validate asset docs plug-in filename and class name [\#3029](https://github.com/pypeclub/OpenPype/pull/3029) - General: Fix import after movements [\#3028](https://github.com/pypeclub/OpenPype/pull/3028) - Harmony: Added creating subset name for workfile from template [\#3024](https://github.com/pypeclub/OpenPype/pull/3024) - AfterEffects: Added creating subset name for workfile from template [\#3023](https://github.com/pypeclub/OpenPype/pull/3023) - General: Add example addons to ignored [\#3022](https://github.com/pypeclub/OpenPype/pull/3022) -- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\#3018](https://github.com/pypeclub/OpenPype/pull/3018) - Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) - Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) - Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) - Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) -- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) **Merged pull requests:** @@ -75,12 +96,12 @@ **🆕 New features** - nuke: bypass baking [\#2992](https://github.com/pypeclub/OpenPype/pull/2992) -- Multiverse: Initial Support [\#2908](https://github.com/pypeclub/OpenPype/pull/2908) **🚀 Enhancements** - Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) +- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) - General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) - General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975) @@ -89,12 +110,11 @@ - NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943) - TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942) - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) -- General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) -- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) **🐛 Bug fixes** - Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) +- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) - Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) - AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989) @@ -115,15 +135,12 @@ - Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934) - Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932) - General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926) -- Flame: centos related debugging [\#2922](https://github.com/pypeclub/OpenPype/pull/2922) **🔀 Refactored code** - General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935) - General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) - General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) -- General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918) -- General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912) **Merged pull requests:** @@ -136,16 +153,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.1-nightly.3...3.9.1) -**🚀 Enhancements** - -- Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) - -**🐛 Bug fixes** - -- General: Fix use of Anatomy roots [\#2904](https://github.com/pypeclub/OpenPype/pull/2904) -- Fixing gap detection in extract review [\#2902](https://github.com/pypeclub/OpenPype/pull/2902) - ## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.0-nightly.9...3.9.0) diff --git a/openpype/version.py b/openpype/version.py index 08dcbb5aed..5616e95677 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.4-nightly.1" +__version__ = "3.9.4-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index adec7ab158..d7951180ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.4-nightly.1" # OpenPype +version = "3.9.4-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From df9d4167362b77e0432fd59412d0bca3dd771183 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 15 Apr 2022 10:12:41 +0000 Subject: [PATCH 317/337] [Automated] Release --- CHANGELOG.md | 8 ++++---- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cca62cca3..57a4560667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,13 @@ # Changelog -## [3.9.4-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...3.9.4) ### 📖 Documentation - Documentation: more info about Tasks [\#3062](https://github.com/pypeclub/OpenPype/pull/3062) - Documentation: Python requirements to 3.7.9 [\#3035](https://github.com/pypeclub/OpenPype/pull/3035) -- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) **🆕 New features** @@ -62,6 +61,7 @@ - Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) - General: default workfile subset name for workfile [\#3011](https://github.com/pypeclub/OpenPype/pull/3011) - Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) +- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) **🐛 Bug fixes** @@ -91,6 +91,7 @@ ### 📖 Documentation - Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999) +- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) - Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) **🆕 New features** @@ -101,7 +102,6 @@ - Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) -- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) - General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) - General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975) diff --git a/openpype/version.py b/openpype/version.py index 5616e95677..fe68dcfd4a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.4-nightly.2" +__version__ = "3.9.4" diff --git a/pyproject.toml b/pyproject.toml index d7951180ce..156e17258e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.4-nightly.2" # OpenPype +version = "3.9.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c704e823fe8fb457ffe4ca87df54585a91324932 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 16 Apr 2022 03:39:27 +0000 Subject: [PATCH 318/337] [Automated] Bump version --- CHANGELOG.md | 14 +++++++++++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a4560667..8cd45f2cc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,22 @@ # Changelog +## [3.9.5-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.4...HEAD) + +**🐛 Bug fixes** + +- Nuke: Add aov matching even for remainder and prerender [\#3060](https://github.com/pypeclub/OpenPype/pull/3060) + ## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.3...3.9.4) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.4-nightly.2...3.9.4) ### 📖 Documentation - Documentation: more info about Tasks [\#3062](https://github.com/pypeclub/OpenPype/pull/3062) - Documentation: Python requirements to 3.7.9 [\#3035](https://github.com/pypeclub/OpenPype/pull/3035) +- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) **🆕 New features** @@ -76,7 +85,6 @@ - Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) - Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) - Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) -- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) - Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) **Merged pull requests:** @@ -91,7 +99,6 @@ ### 📖 Documentation - Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999) -- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) - Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) **🆕 New features** @@ -114,6 +121,7 @@ **🐛 Bug fixes** - Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) +- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) - Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) - Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) diff --git a/openpype/version.py b/openpype/version.py index fe68dcfd4a..a2916925b4 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.4" +__version__ = "3.9.5-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 156e17258e..0d1d5dc9a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.4" # OpenPype +version = "3.9.5-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 2c1114706721bad0465c7a76a046751c9665b5ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Apr 2022 10:15:16 +0200 Subject: [PATCH 319/337] Changed list to set --- openpype/modules/sync_server/sync_server_module.py | 8 ++++---- .../unit/openpype/modules/sync_server/test_module_api.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 596aeb8b39..3744a21b43 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -300,13 +300,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): 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) @@ -323,7 +323,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): 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 diff --git a/tests/unit/openpype/modules/sync_server/test_module_api.py b/tests/unit/openpype/modules/sync_server/test_module_api.py index 14613604dd..b7d3383c0b 100644 --- a/tests/unit/openpype/modules/sync_server/test_module_api.py +++ b/tests/unit/openpype/modules/sync_server/test_module_api.py @@ -37,9 +37,9 @@ class TestModuleApi(ModuleUnitTest): "studio2": {"alternative_sites": ["studio"]}} ret = setup_sync_server_module._get_alt_site_pairs(conf_sites) - expected = {"SFTP": ["studio", "studio2"], - "studio": ["SFTP", "studio2"], - "studio2": ["studio", "SFTP"]} + expected = {"SFTP": {"studio", "studio2"}, + "studio": {"SFTP", "studio2"}, + "studio2": {"studio", "SFTP"}} assert ret == expected, "Not matching result" From 687f769242025413e56908a47611b681186f8162 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Apr 2022 10:23:59 +0200 Subject: [PATCH 320/337] Added more complex test for deeper alternative site tree --- .../modules/sync_server/test_module_api.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/openpype/modules/sync_server/test_module_api.py b/tests/unit/openpype/modules/sync_server/test_module_api.py index b7d3383c0b..a484977758 100644 --- a/tests/unit/openpype/modules/sync_server/test_module_api.py +++ b/tests/unit/openpype/modules/sync_server/test_module_api.py @@ -42,5 +42,23 @@ class TestModuleApi(ModuleUnitTest): "studio2": {"studio", "SFTP"}} assert ret == expected, "Not matching result" + def test_get_alt_site_pairs_deep(self, setup_sync_server_module): + conf_sites = {"A": {"alternative_sites": ["C"]}, + "B": {"alternative_sites": ["C"]}, + "C": {"alternative_sites": ["D"]}, + "D": {"alternative_sites": ["A"]}, + "F": {"alternative_sites": ["G"]}, + "G": {"alternative_sites": ["F"]}, + } + + ret = setup_sync_server_module._get_alt_site_pairs(conf_sites) + expected = {"A": {"B", "C", "D"}, + "B": {"A", "C", "D"}, + "C": {"A", "B", "D"}, + "D": {"A", "B", "C"}, + "F": {"G"}, + "G": {"F"}} + assert ret == expected, "Not matching result" + test_case = TestModuleApi() From d87e70b3c90490e160d314373bf45da8c255fd93 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Apr 2022 15:21:05 +0200 Subject: [PATCH 321/337] OP-3103 - added plugin to parse batch file to PS Plugin in webpublisher folder doesn't get triggered in PS processing. --- .../plugins/publish/collect_batch_data.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_batch_data.py diff --git a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py new file mode 100644 index 0000000000..5e6e916611 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -0,0 +1,73 @@ +"""Parses batch context from json and continues in publish process. + +Provides: + context -> Loaded batch file. + - asset + - task (task name) + - taskType + - project_name + - variant + +Code is practically copy of `openype/hosts/webpublish/collect_batch_data` as +webpublisher should be eventually ejected as an addon, eg. mentioned plugin +shouldn't be pushed into general publish plugins. +""" + +import os + +import pyblish.api +from avalon import io +from openpype.lib.plugin_tools import ( + parse_json, + get_batch_asset_task_info +) + + +class CollectBatchData(pyblish.api.ContextPlugin): + """Collect batch data from json stored in 'OPENPYPE_PUBLISH_DATA' env dir. + + The directory must contain 'manifest.json' file where batch data should be + stored. + """ + # must be really early, context values are only in json file + order = pyblish.api.CollectorOrder - 0.495 + label = "Collect batch data" + hosts = ["photoshop"] + targets = ["remotepublish"] + + def process(self, context): + self.log.info("CollectBatchData") + batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") + + assert batch_dir, ( + "Missing `OPENPYPE_PUBLISH_DATA`") + + assert os.path.exists(batch_dir), \ + "Folder {} doesn't exist".format(batch_dir) + + 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." + ) + + batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) + + context.data["batchDir"] = batch_dir + context.data["batchData"] = batch_data + + asset_name, task_name, task_type = get_batch_asset_task_info( + batch_data["context"] + ) + + os.environ["AVALON_ASSET"] = asset_name + io.Session["AVALON_ASSET"] = asset_name + os.environ["AVALON_TASK"] = task_name + io.Session["AVALON_TASK"] = task_name + + context.data["asset"] = asset_name + context.data["task"] = task_name + context.data["taskType"] = task_type + context.data["project_name"] = project_name + context.data["variant"] = batch_data["variant"] From 40d426ed00abdc558797f193e5b2ae66b9b67a1b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Apr 2022 15:22:29 +0200 Subject: [PATCH 322/337] OP-3103 - fixes hosts, update variant location --- .../webpublisher/plugins/publish/collect_batch_data.py | 10 ++++++++-- .../plugins/publish/collect_published_files.py | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py index ca14538d7d..c9ba903007 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py @@ -1,7 +1,12 @@ -"""Loads batch context from json and continues in publish process. +"""Parses batch context from json and continues in publish process. Provides: context -> Loaded batch file. + - asset + - task (task name) + - taskType + - project_name + - variant """ import os @@ -24,7 +29,7 @@ class CollectBatchData(pyblish.api.ContextPlugin): # must be really early, context values are only in json file order = pyblish.api.CollectorOrder - 0.495 label = "Collect batch data" - host = ["webpublisher"] + hosts = ["webpublisher"] def process(self, context): batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") @@ -60,6 +65,7 @@ class CollectBatchData(pyblish.api.ContextPlugin): context.data["task"] = task_name context.data["taskType"] = task_type context.data["project_name"] = project_name + context.data["variant"] = batch_data["variant"] self._set_ctx_path(batch_data) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 8edaf4f67b..65db9d7e2e 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -40,7 +40,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): # must be really early, context values are only in json file order = pyblish.api.CollectorOrder - 0.490 label = "Collect rendered frames" - host = ["webpublisher"] + hosts = ["webpublisher"] targets = ["filespublish"] # from Settings @@ -61,6 +61,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): task_name = context.data["task"] task_type = context.data["taskType"] project_name = context.data["project_name"] + variant = context.data["variant"] for task_dir in task_subfolders: task_data = parse_json(os.path.join(task_dir, "manifest.json")) @@ -76,7 +77,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): extension.replace(".", '')) subset_name = get_subset_name_with_asset_doc( - family, task_data["variant"], task_name, asset_doc, + family, variant, task_name, asset_doc, project_name=project_name, host_name="webpublisher" ) version = self._get_last_version(asset_name, subset_name) + 1 From df2f7f7c590064a6d3d91d4c4593e20cc141cb2d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Apr 2022 15:24:09 +0200 Subject: [PATCH 323/337] OP-3103 - use variant from context for PS --- .../publish/collect_color_coded_instances.py | 25 +++---------------- .../plugins/publish/collect_instances.py | 3 ++- .../plugins/publish/collect_review.py | 2 +- 3 files changed, 7 insertions(+), 23 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 7d44d55a80..122428eea0 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -4,7 +4,6 @@ import re import pyblish.api from openpype.lib import prepare_template_data -from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info from openpype.hosts.photoshop import api as photoshop @@ -46,7 +45,10 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): existing_subset_names = self._get_existing_subset_names(context) - asset_name, task_name, variant = self._parse_batch(batch_dir) + # from CollectBatchData + asset_name = context.data["asset"] + task_name = context.data["task"] + variant = context.data["variant"] stub = photoshop.stub() layers = stub.get_layers() @@ -130,25 +132,6 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): return existing_subset_names - def _parse_batch(self, batch_dir): - """Parses asset_name, task_name, variant from batch manifest.""" - task_data = None - if batch_dir and os.path.exists(batch_dir): - task_data = parse_json(os.path.join(batch_dir, - "manifest.json")) - if not task_data: - raise ValueError( - "Cannot parse batch meta in {} folder".format(batch_dir)) - variant = task_data["variant"] - - asset, task_name, task_type = get_batch_asset_task_info( - task_data["context"]) - - if not task_name: - task_name = task_type - - return asset, task_name, variant - def _create_instance(self, context, layer, family, asset, subset, task_name): instance = context.create_instance(layer.name) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index 6198ed0156..9f95441e6f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -82,8 +82,9 @@ class CollectInstances(pyblish.api.ContextPlugin): task_name = api.Session["AVALON_TASK"] asset_name = context.data["assetEntity"]["name"] + variant = context.data.get("variant") or variants[0] fill_pairs = { - "variant": variants[0], + "variant": variant, "family": family, "task": task_name } diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index f3842b9ee5..89432553c5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -16,7 +16,7 @@ class CollectReview(pyblish.api.ContextPlugin): family = "review" subset = get_subset_name_with_asset_doc( family, - "", + context.data.get("variant", ''), context.data["anatomyData"]["task"]["name"], context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], From 68024932102bd80ef7f28e374654549338943dab Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 20 Apr 2022 03:58:37 +0000 Subject: [PATCH 324/337] [Automated] Bump version --- CHANGELOG.md | 31 +++++++++++++++++++------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd45f2cc0..a48e9ee806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,25 @@ # Changelog -## [3.9.5-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.4...HEAD) +### 📖 Documentation + +- Nuke docs with videos [\#3052](https://github.com/pypeclub/OpenPype/pull/3052) + +**🚀 Enhancements** + +- Update collect\_render.py [\#3055](https://github.com/pypeclub/OpenPype/pull/3055) + **🐛 Bug fixes** - Nuke: Add aov matching even for remainder and prerender [\#3060](https://github.com/pypeclub/OpenPype/pull/3060) +**🔀 Refactored code** + +- General: Move host install [\#3009](https://github.com/pypeclub/OpenPype/pull/3009) + ## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.4-nightly.2...3.9.4) @@ -21,7 +33,6 @@ **🆕 New features** - General: Local overrides for environment variables [\#3045](https://github.com/pypeclub/OpenPype/pull/3045) -- Flame: Flare integration preparation [\#2928](https://github.com/pypeclub/OpenPype/pull/2928) **🚀 Enhancements** @@ -85,8 +96,13 @@ - Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) - Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) - Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) +- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) - Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) +**🔀 Refactored code** + +- General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935) + **Merged pull requests:** - Maya: Allow to select invalid camera contents if no cameras found [\#3030](https://github.com/pypeclub/OpenPype/pull/3030) @@ -110,18 +126,17 @@ - Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) - TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) - Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) +- SiteSync: Added compute\_resource\_sync\_sites to sync\_server\_module [\#2983](https://github.com/pypeclub/OpenPype/pull/2983) - General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) - General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975) - Hero versions: Use custom templates [\#2967](https://github.com/pypeclub/OpenPype/pull/2967) - Slack: Added configurable maximum file size of review upload to Slack [\#2945](https://github.com/pypeclub/OpenPype/pull/2945) - NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943) - TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942) -- Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) **🐛 Bug fixes** - Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) -- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) - Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) - PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) - Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) @@ -141,14 +156,6 @@ - General: Python specific vendor paths on env injection [\#2939](https://github.com/pypeclub/OpenPype/pull/2939) - General: More fail safe delete old versions [\#2936](https://github.com/pypeclub/OpenPype/pull/2936) - Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934) -- Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932) -- General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926) - -**🔀 Refactored code** - -- General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935) -- General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) -- General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index a2916925b4..9e2525e3b8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.5-nightly.1" +__version__ = "3.10.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 0d1d5dc9a3..4c65ac9bda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.5-nightly.1" # OpenPype +version = "3.10.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e93fb7f4f3d72586da4e673c28b18881478e61ef Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Apr 2022 11:45:00 +0200 Subject: [PATCH 325/337] Removed obsolete openpype.install --- tests/lib/testing_classes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 0a9da1aca8..7dfbf6fd0d 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -273,8 +273,6 @@ class PublishTest(ModuleUnitTest): ) os.environ["AVALON_SCHEMA"] = schema_path - import openpype - openpype.install() os.environ["OPENPYPE_EXECUTABLE"] = sys.executable from openpype.lib import ApplicationManager From 76d50cf6aad40d0e066ea2510d127c27fe9f7edd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Apr 2022 11:45:39 +0200 Subject: [PATCH 326/337] Fixed unwanted pop Counted without type. --- tests/lib/assert_classes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/lib/assert_classes.py b/tests/lib/assert_classes.py index 7f4d8efc10..9a94f89fd0 100644 --- a/tests/lib/assert_classes.py +++ b/tests/lib/assert_classes.py @@ -24,13 +24,14 @@ class DBAssert: else: args[key] = val + no_of_docs = dbcon.count_documents(args) + + msg = None args.pop("type") detail_str = " " if args: detail_str = " with '{}'".format(args) - msg = None - no_of_docs = dbcon.count_documents(args) if expected != no_of_docs: msg = "Not expected no of '{}'{}."\ "Expected {}, found {}".format(queried_type, From 0dd46fe51329b15dd6a73e68534904d11d194587 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:40:22 +0200 Subject: [PATCH 327/337] added new function convert_input_paths_for_ffmpeg which converts list of input paths to output dir keeping the source filenames --- openpype/lib/transcoding.py | 122 ++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index c2fecf6628..f379f81dec 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -516,6 +516,128 @@ def convert_for_ffmpeg( run_subprocess(oiio_cmd, logger=logger) +def convert_input_paths_for_ffmpeg( + input_paths, + output_dir, + logger=None +): + """Contert source file to format supported in ffmpeg. + + Currently can convert only exrs. The input filepaths should be files + with same type. Information about input is loaded only from first found + file. + + Filenames of input files are kept so make sure that output directory + is not the same directory as input files have. + + Args: + input_paths (str): Paths that should be converted. It is expected that + contains single file or image sequence of samy type. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + logger (logging.Logger): Logger used for logging. + + Raises: + ValueError: If input filepath has extension not supported by function. + Currently is supported only ".exr" extension. + """ + if logger is None: + logger = logging.getLogger(__name__) + + first_input_path = input_paths[0] + ext = os.path.splitext(first_input_path)[1].lower() + if ext != ".exr": + raise ValueError(( + "Function 'convert_for_ffmpeg' currently support only" + " \".exr\" extension. Got \"{}\"." + ).format(ext)) + + input_info = get_oiio_info_for_input(first_input_path) + + # Change compression only if source compression is "dwaa" or "dwab" + # - they're not supported in ffmpeg + compression = input_info["attribs"].get("compression") + if compression in ("dwaa", "dwab"): + compression = "none" + + # Collect channels to export + channel_names = input_info["channelnames"] + review_channels = get_convert_rgb_channels(channel_names) + if review_channels is None: + raise ValueError( + "Couldn't find channels that can be used for conversion." + ) + + red, green, blue, alpha = review_channels + input_channels = [red, green, blue] + channels_arg = "R={},G={},B={}".format(red, green, blue) + if alpha is not None: + channels_arg += ",A={}".format(alpha) + input_channels.append(alpha) + input_channels_str = ",".join(input_channels) + + for input_path in input_paths: + # Prepare subprocess arguments + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + ] + # Add input compression if available + if compression: + oiio_cmd.extend(["--compression", compression]) + + oiio_cmd.extend([ + # Tell oiiotool which channels should be loaded + # - other channels are not loaded to memory so helps to + # avoid memory leak issues + "-i:ch={}".format(input_channels_str), input_path, + # Tell oiiotool which channels should be put to top stack + # (and output) + "--ch", channels_arg + ]) + + for attr_name, attr_value in input_info["attribs"].items(): + if not isinstance(attr_value, str): + continue + + # Remove attributes that have string value longer than allowed length + # for ffmpeg or when containt unallowed symbols + erase_reason = "Missing reason" + erase_attribute = False + if len(attr_value) > MAX_FFMPEG_STRING_LEN: + erase_reason = "has too long value ({} chars).".format( + len(attr_value) + ) + + if erase_attribute: + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char in attr_value: + erase_attribute = True + erase_reason = ( + "contains unsupported character \"{}\"." + ).format(char) + break + + if erase_attribute: + # Set attribute to empty string + logger.info(( + "Removed attribute \"{}\" from metadata because {}." + ).format(attr_name, erase_reason)) + oiio_cmd.extend(["--eraseattrib", attr_name]) + + # Add last argument - path to output + base_filename = os.path.basename(first_input_path) + output_path = os.path.join(output_dir, base_filename) + oiio_cmd.extend([ + "-o", output_path + ]) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) + + # FFMPEG functions def get_ffprobe_data(path_to_file, logger=None): """Load data about entered filepath via ffprobe. From 91d2eb7355a2934e1afeb2371cb9bb97b501f7ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:41:03 +0200 Subject: [PATCH 328/337] replace convert_for_ffmpeg with new function --- .../plugins/publish/extract_thumbnail.py | 8 +++----- openpype/lib/__init__.py | 2 ++ openpype/plugins/publish/extract_burnin.py | 13 ++++++++----- openpype/plugins/publish/extract_jpeg_exr.py | 8 +++----- openpype/plugins/publish/extract_review.py | 18 +++++++++--------- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py index cb6ed8481c..a56521891b 100644 --- a/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py @@ -8,7 +8,7 @@ from openpype.lib import ( run_subprocess, get_transcode_temp_directory, - convert_for_ffmpeg, + convert_input_paths_for_ffmpeg, should_convert_for_ffmpeg ) @@ -59,11 +59,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): if do_convert: convert_dir = get_transcode_temp_directory() filename = os.path.basename(full_input_path) - convert_for_ffmpeg( - full_input_path, + convert_input_paths_for_ffmpeg( + [full_input_path], convert_dir, - None, - None, self.log ) full_input_path = os.path.join(convert_dir, filename) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index b57e469f5b..29719b63bd 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -105,6 +105,7 @@ from .transcoding import ( get_transcode_temp_directory, should_convert_for_ffmpeg, convert_for_ffmpeg, + convert_input_paths_for_ffmpeg, get_ffprobe_data, get_ffprobe_streams, get_ffmpeg_codec_args, @@ -276,6 +277,7 @@ __all__ = [ "get_transcode_temp_directory", "should_convert_for_ffmpeg", "convert_for_ffmpeg", + "convert_input_paths_for_ffmpeg", "get_ffprobe_data", "get_ffprobe_streams", "get_ffmpeg_codec_args", diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 41c84103a6..544c763b52 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -16,7 +16,7 @@ from openpype.lib import ( run_openpype_process, get_transcode_temp_directory, - convert_for_ffmpeg, + convert_input_paths_for_ffmpeg, should_convert_for_ffmpeg, CREATE_NO_WINDOW @@ -187,8 +187,13 @@ class ExtractBurnin(openpype.api.Extractor): repre_files = repre["files"] if isinstance(repre_files, (tuple, list)): filename = repre_files[0] + src_filepaths = [ + os.path.join(src_repre_staging_dir, filename) + for filename in repre_files + ] else: filename = repre_files + src_filepaths = [os.path.join(src_repre_staging_dir, filename)] first_input_path = os.path.join(src_repre_staging_dir, filename) # Determine if representation requires pre conversion for ffmpeg @@ -209,11 +214,9 @@ class ExtractBurnin(openpype.api.Extractor): new_staging_dir = get_transcode_temp_directory() repre["stagingDir"] = new_staging_dir - convert_for_ffmpeg( - first_input_path, + convert_input_paths_for_ffmpeg( + src_filepaths, new_staging_dir, - _temp_data["frameStart"], - _temp_data["frameEnd"], self.log ) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 468ed96199..d6d6854092 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -8,7 +8,7 @@ from openpype.lib import ( path_to_subprocess_arg, get_transcode_temp_directory, - convert_for_ffmpeg, + convert_input_paths_for_ffmpeg, should_convert_for_ffmpeg ) @@ -79,11 +79,9 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if do_convert: convert_dir = get_transcode_temp_directory() filename = os.path.basename(full_input_path) - convert_for_ffmpeg( - full_input_path, + convert_input_paths_for_ffmpeg( + [full_input_path], convert_dir, - None, - None, self.log ) full_input_path = os.path.join(convert_dir, filename) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index d569d82762..f2473839d9 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -18,7 +18,7 @@ from openpype.lib import ( path_to_subprocess_arg, should_convert_for_ffmpeg, - convert_for_ffmpeg, + convert_input_paths_for_ffmpeg, get_transcode_temp_directory ) import speedcopy @@ -194,16 +194,20 @@ class ExtractReview(pyblish.api.InstancePlugin): src_repre_staging_dir = repre["stagingDir"] # Receive filepath to first file in representation first_input_path = None + input_filepaths = [] if not self.input_is_sequence(repre): first_input_path = os.path.join( src_repre_staging_dir, repre["files"] ) + input_filepaths.append(first_input_path) else: for filename in repre["files"]: - first_input_path = os.path.join( + filepath = os.path.join( src_repre_staging_dir, filename ) - break + input_filepaths.append(filepath) + if first_input_path is None: + first_input_path = filepath # Skip if file is not set if first_input_path is None: @@ -230,13 +234,9 @@ class ExtractReview(pyblish.api.InstancePlugin): new_staging_dir = get_transcode_temp_directory() repre["stagingDir"] = new_staging_dir - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - convert_for_ffmpeg( - first_input_path, + convert_input_paths_for_ffmpeg( + input_filepaths, new_staging_dir, - frame_start, - frame_end, self.log ) From d9d772db72f8e085817d7e93b267ea8f70d75399 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:41:15 +0200 Subject: [PATCH 329/337] added deprecation warning to convert_to_ffmpeg --- openpype/lib/transcoding.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f379f81dec..b75ae2baf1 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -409,6 +409,12 @@ def convert_for_ffmpeg( if logger is None: logger = logging.getLogger(__name__) + logger.warning(( + "DEPRECATED: 'openpype.lib.transcoding.convert_for_ffmpeg' is" + " deprecated function of conversion for FFMpeg. Please replace usage" + " with 'openpype.lib.transcoding.convert_input_paths_for_ffmpeg'" + )) + ext = os.path.splitext(first_input_path)[1].lower() if ext != ".exr": raise ValueError(( From 7deed5e74d8124b6f1d52cc45d1c9c06e4e0bd76 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 15:28:15 +0200 Subject: [PATCH 330/337] added few more comments --- openpype/lib/transcoding.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index b75ae2baf1..7cc345fcc0 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -382,6 +382,11 @@ def should_convert_for_ffmpeg(src_filepath): return False +# Deprecated since 2022 4 20 +# - Reason - Doesn't convert sequences right way: Can't handle gaps, reuse +# first frame for all frames and changes filenames when input +# is sequence. +# - use 'convert_input_paths_for_ffmpeg' instead def convert_for_ffmpeg( first_input_path, output_dir, @@ -535,6 +540,8 @@ def convert_input_paths_for_ffmpeg( Filenames of input files are kept so make sure that output directory is not the same directory as input files have. + - This way it can handle gaps and can keep input filenames without handling + frame template Args: input_paths (str): Paths that should be converted. It is expected that From 52a1450df2a4c9f6604899add111670aafe7bd79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 15:28:49 +0200 Subject: [PATCH 331/337] fix line length --- openpype/lib/transcoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 7cc345fcc0..cdfe240e68 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -615,8 +615,8 @@ def convert_input_paths_for_ffmpeg( if not isinstance(attr_value, str): continue - # Remove attributes that have string value longer than allowed length - # for ffmpeg or when containt unallowed symbols + # Remove attributes that have string value longer than allowed + # length for ffmpeg or when containt unallowed symbols erase_reason = "Missing reason" erase_attribute = False if len(attr_value) > MAX_FFMPEG_STRING_LEN: From 5e4b292a86ea7221d12776850b9680b47955b23f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 17:51:37 +0200 Subject: [PATCH 332/337] change openpype install to install openpype plugins --- openpype/tools/standalonepublish/publish.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/standalonepublish/publish.py b/openpype/tools/standalonepublish/publish.py index 582e7eccf8..e1e9edebb9 100644 --- a/openpype/tools/standalonepublish/publish.py +++ b/openpype/tools/standalonepublish/publish.py @@ -1,14 +1,14 @@ import os import sys -import openpype import pyblish.api +from openpype.pipeline import install_openpype_plugins from openpype.tools.utils.host_tools import show_publish def main(env): # Registers pype's Global pyblish plugins - openpype.install() + install_openpype_plugins() # Register additional paths addition_paths_str = env.get("PUBLISH_PATHS") or "" From fca3645a7afaa3725b81a37a5494f0eb100ec6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 20 Apr 2022 18:10:05 +0200 Subject: [PATCH 333/337] add support for bgeo and vdb add support for standalone publisher to publish bgeo and vdb sequences --- .../plugins/publish/collect_context.py | 3 ++- .../project_settings/standalonepublisher.json | 11 ++++++++++- .../standalonepublish/widgets/widget_drop_frame.py | 8 ++++++-- .../standalonepublish/widgets/widget_family_desc.py | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 6913e0836d..aabccc0328 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -247,7 +247,8 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): self.log.debug("collecting sequence: {}".format(collections)) instance.data["frameStart"] = int(component["frameStart"]) instance.data["frameEnd"] = int(component["frameEnd"]) - instance.data["fps"] = int(component["fps"]) + if component.get("fps"): + instance.data["fps"] = int(component["fps"]) ext = component["ext"] if ext.startswith("."): diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index bc91a5ea8a..e36232d3f7 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -141,6 +141,14 @@ "defaults": [], "help": "Texture files with Unreal naming convention" }, + "create_vdb": { + "name": "vdb", + "label": "VDB Volumetric Data", + "family": "vdbcache", + "icon": "cloud", + "defaults": [], + "help": "Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids" + }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", "create_model": "Model", @@ -154,7 +162,8 @@ "create_render": "Render", "create_mov_batch": "Batch Mov", "create_texture_batch": "Batch Texture", - "create_simple_unreal_texture": "Simple Unreal Texture" + "create_simple_unreal_texture": "Simple Unreal Texture", + "create_vdb": "VDB Cache" } }, "publish": { diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index c1c59d65b6..e6c7328e88 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -37,6 +37,10 @@ class DropDataFrame(QtWidgets.QFrame): "video_file": video_extensions } + sequence_types = [ + ".bgeo", ".vdb" + ] + def __init__(self, parent): super().__init__() self.parent_widget = parent @@ -176,7 +180,7 @@ class DropDataFrame(QtWidgets.QFrame): non_collectionable_paths = [] for path in in_paths: ext = os.path.splitext(path)[1] - if ext in self.image_extensions: + if ext in self.image_extensions or ext in self.sequence_types: collectionable_paths.append(path) else: non_collectionable_paths.append(path) @@ -289,7 +293,7 @@ class DropDataFrame(QtWidgets.QFrame): def get_file_data(self, data): filepath = data['files'][0] ext = data['ext'].lower() - output = {} + output = {"fps": None} file_info = None if 'file_info' in data: diff --git a/openpype/tools/standalonepublish/widgets/widget_family_desc.py b/openpype/tools/standalonepublish/widgets/widget_family_desc.py index 79681615b9..2095b332bd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family_desc.py +++ b/openpype/tools/standalonepublish/widgets/widget_family_desc.py @@ -52,6 +52,7 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) help = QtWidgets.QLabel("help") + help.setWordWrap(True) help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) label_layout.addWidget(family) From 5142cd613935e3e6b9c514c4a08371dad2555009 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 19:07:56 +0200 Subject: [PATCH 334/337] fix keeping of filenames --- openpype/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index cdfe240e68..fcec5d4216 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -641,7 +641,7 @@ def convert_input_paths_for_ffmpeg( oiio_cmd.extend(["--eraseattrib", attr_name]) # Add last argument - path to output - base_filename = os.path.basename(first_input_path) + base_filename = os.path.basename(input_path) output_path = os.path.join(output_dir, base_filename) oiio_cmd.extend([ "-o", output_path From 4e0a3259ed87971481bb33d8fbd1a077350ddfeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 10:05:28 +0200 Subject: [PATCH 335/337] query parent and data.parents from asset document --- .../tools/project_manager/project_manager/model.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 1c3ec089f6..b3fd7fa0c7 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1819,12 +1819,16 @@ class AssetItem(BaseItem): } query_projection = { "_id": 1, - "data.tasks": 1, - "data.visualParent": 1, - "schema": 1, - "name": 1, + "schema": 1, "type": 1, + "parent": 1, + + "data.visualParent": 1, + "data.parents": 1, + + "data.tasks": 1, + "data.frameStart": 1, "data.frameEnd": 1, "data.fps": 1, @@ -1835,7 +1839,7 @@ class AssetItem(BaseItem): "data.clipIn": 1, "data.clipOut": 1, "data.pixelAspect": 1, - "data.tools_env": 1 + "data.tools_env": 1, } def __init__(self, asset_doc): From 589666682c7a71b6d948c6c765fbfb7b433bcf95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 10:23:17 +0200 Subject: [PATCH 336/337] added info logs --- .../project_manager/project_manager/model.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b3fd7fa0c7..5fd06ef442 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -7,6 +7,11 @@ from pymongo import UpdateOne, DeleteOne from Qt import QtCore, QtGui +from openpype.lib import ( + CURRENT_DOC_SCHEMAS, + PypeLogger, +) + from .constants import ( IDENTIFIER_ROLE, ITEM_TYPE_ROLE, @@ -18,8 +23,6 @@ from .constants import ( ) from .style import ResourceCache -from openpype.lib import CURRENT_DOC_SCHEMAS - class ProjectModel(QtGui.QStandardItemModel): """Load possible projects to modify from MongoDB. @@ -185,6 +188,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): for key in self.multiselection_columns } + self._log = None # TODO Reset them on project change self._current_project = None self._root_item = None @@ -194,6 +198,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._reset_root_item() + @property + def log(self): + if self._log is None: + self._log = PypeLogger.get_logger("ProjectManagerModel") + return self._log + @property def items_by_id(self): return self._items_by_id @@ -1367,6 +1377,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process = collections.deque() to_process.append(project_item) + updated_count = 0 + created_count = 0 + removed_count = 0 bulk_writes = [] while to_process: parent = to_process.popleft() @@ -1378,9 +1391,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process.append(item) if item.is_new: + created_count += 1 insert_list.append(item) elif item.data(REMOVED_ROLE): + removed_count += 1 if item.data(HIERARCHY_CHANGE_ABLE_ROLE): bulk_writes.append(DeleteOne( {"_id": item.asset_id} @@ -1394,6 +1409,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): else: update_data = item.update_data() if update_data: + updated_count += 1 bulk_writes.append(UpdateOne( {"_id": item.asset_id}, update_data @@ -1408,8 +1424,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): for idx, mongo_id in enumerate(result.inserted_ids): insert_list[idx].mongo_id = mongo_id - if bulk_writes: - project_col.bulk_write(bulk_writes) + if not bulk_writes: + self.log.info("Nothing has changed") + return + + project_col.bulk_write(bulk_writes) + self.log.info(( + "Save finished." + " Created {} | Updated {} | Removed {} asset documents" + ).format(created_count, updated_count, removed_count)) self.refresh_project() From 7d2dc0b0ea0835c3ccde25cb41ba095811581982 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 11:34:53 +0200 Subject: [PATCH 337/337] fixed changes check --- .../tools/project_manager/project_manager/model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5fd06ef442..871704e13c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1377,8 +1377,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process = collections.deque() to_process.append(project_item) - updated_count = 0 created_count = 0 + updated_count = 0 removed_count = 0 bulk_writes = [] while to_process: @@ -1391,7 +1391,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process.append(item) if item.is_new: - created_count += 1 insert_list.append(item) elif item.data(REMOVED_ROLE): @@ -1422,13 +1421,16 @@ class HierarchyModel(QtCore.QAbstractItemModel): result = project_col.insert_many(new_docs) for idx, mongo_id in enumerate(result.inserted_ids): + created_count += 1 insert_list[idx].mongo_id = mongo_id - if not bulk_writes: + if sum([created_count, updated_count, removed_count]) == 0: self.log.info("Nothing has changed") return - project_col.bulk_write(bulk_writes) + if bulk_writes: + project_col.bulk_write(bulk_writes) + self.log.info(( "Save finished." " Created {} | Updated {} | Removed {} asset documents"